diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8059106a..a7d1071f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -6,10 +6,10 @@ jobs: runs-on: [self-hosted, linux, ARM64] steps: - - name: Set up Go 1.17 + - name: Set up Go 1.18 uses: actions/setup-go@v1 with: - go-version: 1.17 + go-version: 1.18 id: go - name: Check out code into the Go module directory @@ -27,10 +27,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.17 + - name: Set up Go 1.18 uses: actions/setup-go@v1 with: - go-version: 1.17 + go-version: 1.18 id: go - name: Check out code into the Go module directory diff --git a/.gitignore b/.gitignore index 39182116..1415900b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out -coverage.txt \ No newline at end of file +coverage.txt + +# Workspace configuration +.vscode \ No newline at end of file diff --git a/collision/attachSpace.go b/collision/attachSpace.go index 7cf1612d..e1a7a9e3 100644 --- a/collision/attachSpace.go +++ b/collision/attachSpace.go @@ -19,19 +19,32 @@ type AttachSpace struct { aSpace **Space tree *Tree offX, offY float64 + binding event.Binding } func (as *AttachSpace) getAttachSpace() *AttachSpace { return as } +func (as *AttachSpace) CID() event.CallerID { + return (*as.aSpace).CID +} + +var _ attachSpace = &AttachSpace{} + type attachSpace interface { + event.Caller getAttachSpace() *AttachSpace } // Attach attaches v to the given space with optional x,y offsets. See AttachSpace. func Attach(v physics.Vector, s *Space, tree *Tree, offsets ...float64) error { - if t, ok := s.CID.E().(attachSpace); ok { + return AttachWithBus(v, s, tree, event.DefaultBus, offsets...) +} + +func AttachWithBus(v physics.Vector, s *Space, tree *Tree, bus event.Handler, offsets ...float64) error { + en := bus.GetCallerMap().GetEntity(s.CID) + if t, ok := en.(attachSpace); ok { as := t.getAttachSpace() as.aSpace = &s as.follow = v @@ -39,7 +52,7 @@ func Attach(v physics.Vector, s *Space, tree *Tree, offsets ...float64) error { if as.tree == nil { as.tree = DefaultTree } - s.CID.Bind(event.Enter, attachSpaceEnter) + as.binding = event.Bind(bus, event.Enter, t, attachSpaceEnter) if len(offsets) > 0 { as.offX = offsets[0] if len(offsets) > 1 { @@ -54,30 +67,23 @@ func Attach(v physics.Vector, s *Space, tree *Tree, offsets ...float64) error { // Detach removes the attachSpaceEnter binding from an entity composed with // AttachSpace func Detach(s *Space) error { - en := s.CID.E() - if _, ok := en.(attachSpace); ok { - event.UnbindBindable( - event.UnbindOption{ - Event: event.Event{ - Name: event.Enter, - CallerID: s.CID, - }, - Fn: attachSpaceEnter, - }, - ) + return DetachWithBus(s, event.DefaultBus) +} + +func DetachWithBus(s *Space, bus event.Handler) error { + en := bus.GetCallerMap().GetEntity(s.CID) + if as, ok := en.(attachSpace); ok { + as.getAttachSpace().binding.Unbind() return nil } return errors.New("this space's entity is not composed of AttachSpace") } -func attachSpaceEnter(id event.CID, _ interface{}) int { - as := id.E().(attachSpace).getAttachSpace() +func attachSpaceEnter(asIface attachSpace, _ event.EnterPayload) event.Response { + as := asIface.(attachSpace).getAttachSpace() x, y := as.follow.X()+as.offX, as.follow.Y()+as.offY if x != (*as.aSpace).X() || y != (*as.aSpace).Y() { - - // If this was a nil pointer it would have already crashed but as of release 2.2.0 - // this could error from the space to delete not existing in the rtree. as.tree.UpdateSpace(x, y, (*as.aSpace).GetW(), (*as.aSpace).GetH(), *as.aSpace) } return 0 diff --git a/collision/attachSpace_test.go b/collision/attachSpace_test.go index 99d1f613..34eeb965 100644 --- a/collision/attachSpace_test.go +++ b/collision/attachSpace_test.go @@ -1,6 +1,7 @@ package collision import ( + "fmt" "testing" "time" @@ -12,24 +13,22 @@ type aspace struct { AttachSpace } -func (as *aspace) Init() event.CID { - return event.NextID(as) -} - func TestAttachSpace(t *testing.T) { Clear() - go event.ResolveChanges() + b := event.NewBus(event.NewCallerMap()) go func() { for { <-time.After(5 * time.Millisecond) - <-event.TriggerBack(event.Enter, nil) + <-event.TriggerOn(b, event.Enter, event.EnterPayload{}) } }() - as := aspace{} + as := &aspace{} + cid := b.GetCallerMap().Register(as) v := physics.NewVector(0, 0) - s := NewSpace(100, 100, 10, 10, as.Init()) + s := NewSpace(100, 100, 10, 10, cid) Add(s) - err := Attach(v, s, nil, 4, 4) + fmt.Println(s.CID) + err := AttachWithBus(v, s, nil, b, 4, 4) if err != nil { t.Fatalf("attach failed: %v", err) } @@ -42,7 +41,7 @@ func TestAttachSpace(t *testing.T) { t.Fatalf("expected attached space to have y of 9, was %v", s.Y()) } - err = Detach(s) + err = DetachWithBus(s, b) if err != nil { t.Fatalf("detach failed: %v", err) } @@ -60,10 +59,10 @@ func TestAttachSpace(t *testing.T) { s = NewUnassignedSpace(0, 0, 1, 1) err = Attach(v, s, nil) if err == nil { - t.Fatalf("unassinged space attach should have failed: %v", err) + t.Fatalf("unassigned space attach should have failed: %v", err) } err = Detach(s) if err == nil { - t.Fatalf("unassinged space detach should have failed: %v", err) + t.Fatalf("unassigned space detach should have failed: %v", err) } } diff --git a/collision/filter.go b/collision/filter.go index e7a07c3c..d0be173e 100644 --- a/collision/filter.go +++ b/collision/filter.go @@ -43,7 +43,7 @@ func Without(tossFn func(*Space) bool) Filter { } // WithoutCIDs will return no spaces with a CID in the input -func WithoutCIDs(cids ...event.CID) Filter { +func WithoutCIDs(cids ...event.CallerID) Filter { return Without(func(s *Space) bool { for _, c := range cids { if s.CID == c { diff --git a/collision/onCollision.go b/collision/onCollision.go index 784aa90c..8c82aacc 100644 --- a/collision/onCollision.go +++ b/collision/onCollision.go @@ -22,6 +22,10 @@ func (cp *Phase) getCollisionPhase() *Phase { return cp } +func (cp *Phase) CID() event.CallerID { + return cp.OnCollisionS.CID +} + type collisionPhase interface { getCollisionPhase() *Phase } @@ -31,13 +35,12 @@ type collisionPhase interface { // entities begin to collide or stop colliding with the space. // If tree is nil, it uses DefTree func PhaseCollision(s *Space, tree *Tree) error { - return PhaseCollisionWithBus(s, tree, event.DefaultBus, event.DefaultCallerMap) + return PhaseCollisionWithBus(s, tree, event.DefaultBus) } -// PhaseCollisionWithBus allows for a non-default bus and non-default entity mapping -// in a phase collision binding. -func PhaseCollisionWithBus(s *Space, tree *Tree, bus event.Handler, entities *event.CallerMap) error { - en := entities.GetEntity(s.CID) +// PhaseCollisionWithBus allows for a non-default bus in a phase collision binding. +func PhaseCollisionWithBus(s *Space, tree *Tree, bus event.Handler) error { + en := bus.GetCallerMap().GetEntity(s.CID) if cp, ok := en.(collisionPhase); ok { oc := cp.getCollisionPhase() oc.OnCollisionS = s @@ -46,46 +49,43 @@ func PhaseCollisionWithBus(s *Space, tree *Tree, bus event.Handler, entities *ev if oc.tree == nil { oc.tree = DefaultTree } - bus.Bind(event.Enter, s.CID, phaseCollisionEnter(entities)) + bus.UnsafeBind(event.Enter.UnsafeEventID, s.CID, phaseCollisionEnter) return nil } return errors.New("This space's entity does not implement collisionPhase") } // CollisionStart/Stop: when a PhaseCollision entity starts/stops touching some label. -// Payload: (Label) the label the entity has started/stopped touching -const ( - Start = "CollisionStart" - Stop = "CollisionStop" +var ( + Start = event.RegisterEvent[Label]() + Stop = event.RegisterEvent[Label]() ) -func phaseCollisionEnter(entities *event.CallerMap) func(id event.CID, nothing interface{}) int { - return func(id event.CID, nothing interface{}) int { - e := entities.GetEntity(id).(collisionPhase) - oc := e.getCollisionPhase() +func phaseCollisionEnter(id event.CallerID, handler event.Handler, _ interface{}) event.Response { + e := handler.GetCallerMap().GetEntity(id).(collisionPhase) + oc := e.getCollisionPhase() - // check hits - hits := oc.tree.Hits(oc.OnCollisionS) - newTouching := map[Label]bool{} + // check hits + hits := oc.tree.Hits(oc.OnCollisionS) + newTouching := map[Label]bool{} - // if any are new, trigger on collision - for _, h := range hits { - l := h.Label - if _, ok := oc.Touching[l]; !ok { - id.TriggerBus(Start, l, oc.bus) - } - newTouching[l] = true + // if any are new, trigger on collision + for _, h := range hits { + l := h.Label + if _, ok := oc.Touching[l]; !ok { + event.TriggerForCallerOn(oc.bus, id, Start, l) } + newTouching[l] = true + } - // if we lost any, trigger off collision - for l := range oc.Touching { - if _, ok := newTouching[l]; !ok { - id.TriggerBus(Stop, l, oc.bus) - } + // if we lost any, trigger off collision + for l := range oc.Touching { + if _, ok := newTouching[l]; !ok { + event.TriggerForCallerOn(handler, id, Stop, l) } + } - oc.Touching = newTouching + oc.Touching = newTouching - return 0 - } + return 0 } diff --git a/collision/onCollision_test.go b/collision/onCollision_test.go index 82777fbc..110036d5 100644 --- a/collision/onCollision_test.go +++ b/collision/onCollision_test.go @@ -12,55 +12,50 @@ type cphase struct { callers *event.CallerMap } -func (cp *cphase) Init() event.CID { - return cp.callers.NextID(cp) -} - func TestCollisionPhase(t *testing.T) { - callers := event.NewCallerMap() - bus := event.NewBus(callers) - go bus.ResolveChanges() + b := event.NewBus(event.NewCallerMap()) go func() { for { <-time.After(5 * time.Millisecond) - <-bus.TriggerBack(event.Enter, nil) + <-event.TriggerOn(b, event.Enter, event.EnterPayload{}) } }() - cp := cphase{ - callers: callers, - } - cid := cp.Init() + cp := &cphase{} + cid := b.GetCallerMap().Register(cp) s := NewSpace(10, 10, 10, 10, cid) tree := NewTree() - err := PhaseCollisionWithBus(s, tree, bus, callers) + err := PhaseCollisionWithBus(s, tree, b) if err != nil { t.Fatalf("phase collision failed: %v", err) } - var active bool - bus.Bind("CollisionStart", cid, func(event.CID, interface{}) int { - active = true + activeCh := make(chan bool, 5) + b1 := event.Bind(b, Start, cp, func(_ *cphase, _ Label) event.Response { + activeCh <- true return 0 }) - bus.Bind("CollisionStop", cid, func(event.CID, interface{}) int { - active = false + b2 := event.Bind(b, Stop, cp, func(_ *cphase, _ Label) event.Response { + activeCh <- false return 0 }) - + <-b1.Bound + <-b2.Bound s2 := NewLabeledSpace(15, 15, 10, 10, 5) tree.Add(s2) - time.Sleep(200 * time.Millisecond) - if !active { + if active := <-activeCh; !active { t.Fatalf("collision should be active") } tree.Remove(s2) time.Sleep(200 * time.Millisecond) - if active { + if active := <-activeCh; active { t.Fatalf("collision should be inactive") } +} +func TestPhaseCollision_Unembedded(t *testing.T) { + t.Parallel() s3 := NewSpace(10, 10, 10, 10, 5) - err = PhaseCollision(s3, nil) + err := PhaseCollision(s3, nil) if err == nil { t.Fatalf("phase collision should have failed") } diff --git a/collision/ray/castFilter.go b/collision/ray/castFilter.go index 06103520..71ba7d7b 100644 --- a/collision/ray/castFilter.go +++ b/collision/ray/castFilter.go @@ -46,7 +46,7 @@ func IgnoreLabels(ls ...collision.Label) CastOption { } // AcceptIDs is equivalent to AcceptLabels, but for CIDs. -func AcceptIDs(ids ...event.CID) CastOption { +func AcceptIDs(ids ...event.CallerID) CastOption { return AddFilter(func(s *collision.Space) bool { for _, id := range ids { if s.CID == id { @@ -58,7 +58,7 @@ func AcceptIDs(ids ...event.CID) CastOption { } // IgnoreIDs is equivalent to IgnoreLabels, but for CIDs. -func IgnoreIDs(ids ...event.CID) CastOption { +func IgnoreIDs(ids ...event.CallerID) CastOption { return AddFilter(func(s *collision.Space) bool { for _, id := range ids { if s.CID == id { diff --git a/collision/ray/castLimit.go b/collision/ray/castLimit.go index 5212958d..46bc245d 100644 --- a/collision/ray/castLimit.go +++ b/collision/ray/castLimit.go @@ -47,7 +47,7 @@ func StopAtLabel(ls ...collision.Label) CastOption { // StopAtID will cause a caster to cease casting as soon as it // hits one of the input CIDs. -func StopAtID(ids ...event.CID) CastOption { +func StopAtID(ids ...event.CallerID) CastOption { return AddLimit(func(ps []collision.Point) bool { z := ps[len(ps)-1].Zone for _, id := range ids { diff --git a/collision/space.go b/collision/space.go index 1d9c07ac..c217ac48 100644 --- a/collision/space.go +++ b/collision/space.go @@ -23,7 +23,7 @@ type Space struct { Label Label // A CID can be used to get the exact // entity which this rectangle belongs to. - CID event.CID + CID event.CallerID // Type represents which ID space the above ID // corresponds to. Type int @@ -197,7 +197,7 @@ func NewUnassignedSpace(x, y, w, h float64) *Space { } // NewSpace returns a space with an associated caller id -func NewSpace(x, y, w, h float64, cID event.CID) *Space { +func NewSpace(x, y, w, h float64, cID event.CallerID) *Space { return NewFullSpace(x, y, w, h, NilLabel, cID) } @@ -212,7 +212,7 @@ func NewLabeledSpace(x, y, w, h float64, l Label) *Space { } // NewFullSpace returns a space with both a label and a caller id -func NewFullSpace(x, y, w, h float64, l Label, cID event.CID) *Space { +func NewFullSpace(x, y, w, h float64, l Label, cID event.CallerID) *Space { rect := NewRect(x, y, w, h) return &Space{ rect, @@ -223,12 +223,12 @@ func NewFullSpace(x, y, w, h float64, l Label, cID event.CID) *Space { } // NewRect2Space returns a space with an associated caller id from a rect2 -func NewRect2Space(rect floatgeom.Rect2, cID event.CID) *Space { +func NewRect2Space(rect floatgeom.Rect2, cID event.CallerID) *Space { return NewSpace(rect.Min.X(), rect.Min.Y(), rect.W(), rect.H(), cID) } // NewRectSpace creates a colliison space with the specified 3D rectangle -func NewRectSpace(rect floatgeom.Rect3, l Label, cID event.CID) *Space { +func NewRectSpace(rect floatgeom.Rect3, l Label, cID event.CallerID) *Space { return &Space{ rect, l, diff --git a/config.go b/config.go index 37f26a13..6e0a14b3 100644 --- a/config.go +++ b/config.go @@ -2,9 +2,7 @@ package oak import ( "encoding/json" - "errors" "io" - "time" "github.com/oakmound/oak/v3/fileutil" "github.com/oakmound/oak/v3/shiny/driver" @@ -22,7 +20,6 @@ type Config struct { IdleDrawFrameRate int `json:"idleDrawFrameRate"` Language string `json:"language"` Title string `json:"title"` - EventRefreshRate Duration `json:"refreshRate"` BatchLoad bool `json:"batchLoad"` GestureSupport bool `json:"gestureSupport"` LoadBuiltinCommands bool `json:"loadBuiltinCommands"` @@ -32,37 +29,7 @@ type Config struct { Borderless bool `json:"borderless"` Fullscreen bool `json:"fullscreen"` SkipRNGSeed bool `json:"skip_rng_seed"` - UnlimitedDrawFrameRate bool `json:"unlimitedDrawFrameRate` -} - -// A Duration is a wrapper around time.Duration that allows for easier json formatting. -type Duration time.Duration - -// MarshalJSON writes a duration as json. -func (d Duration) MarshalJSON() ([]byte, error) { - return json.Marshal(time.Duration(d).String()) -} - -// UnmarshalJSON extracts a duration from a json byte slice. -func (d *Duration) UnmarshalJSON(b []byte) error { - var v interface{} - if err := json.Unmarshal(b, &v); err != nil { - return err - } - switch value := v.(type) { - case float64: - *d = Duration(time.Duration(value)) - return nil - case string: - tmp, err := time.ParseDuration(value) - if err != nil { - return err - } - *d = Duration(tmp) - return nil - default: - return errors.New("invalid duration type") - } + UnlimitedDrawFrameRate bool `json:"unlimitedDrawFrameRate"` } // NewConfig creates a config from a set of transformation options. @@ -98,7 +65,6 @@ func (c Config) setDefaults() Config { c.IdleDrawFrameRate = 60 c.Language = "English" c.Title = "Oak Window" - c.EventRefreshRate = Duration(50 * time.Millisecond) return c } @@ -223,9 +189,6 @@ func (c Config) overwriteFrom(c2 Config) Config { if c2.Title != "" { c.Title = c2.Title } - if c2.EventRefreshRate != 0 { - c.EventRefreshRate = c2.EventRefreshRate - } // Booleans can be directly overwritten-- all booleans in a Config // default to false, if they were unset they will stay false. c.BatchLoad = c2.BatchLoad diff --git a/config_test.go b/config_test.go index 0bbf9a92..bff8501c 100644 --- a/config_test.go +++ b/config_test.go @@ -5,7 +5,6 @@ import ( "os" "path/filepath" "testing" - "time" ) func TestDefaultConfigFileMatchesEmptyConfig(t *testing.T) { @@ -47,7 +46,6 @@ func configEquals(c1, c2 Config) bool { IdleDrawFrameRate int `json:"idleDrawFrameRate"` Language string `json:"language"` Title string `json:"title"` - EventRefreshRate Duration `json:"refreshRate"` BatchLoad bool `json:"batchLoad"` GestureSupport bool `json:"gestureSupport"` LoadBuiltinCommands bool `json:"loadBuiltinCommands"` @@ -57,7 +55,7 @@ func configEquals(c1, c2 Config) bool { Borderless bool `json:"borderless"` Fullscreen bool `json:"fullscreen"` SkipRNGSeed bool `json:"skip_rng_seed"` - UnlimitedDrawFrameRate bool `json:"unlimitedDrawFrameRate` + UnlimitedDrawFrameRate bool `json:"unlimitedDrawFrameRate"` } cc1 := comparableConfig{ Assets: c1.Assets, @@ -69,7 +67,6 @@ func configEquals(c1, c2 Config) bool { IdleDrawFrameRate: c1.IdleDrawFrameRate, Language: c1.Language, Title: c1.Title, - EventRefreshRate: c1.EventRefreshRate, BatchLoad: c1.BatchLoad, GestureSupport: c1.GestureSupport, LoadBuiltinCommands: c1.LoadBuiltinCommands, @@ -91,7 +88,6 @@ func configEquals(c1, c2 Config) bool { IdleDrawFrameRate: c2.IdleDrawFrameRate, Language: c2.Language, Title: c2.Title, - EventRefreshRate: c2.EventRefreshRate, BatchLoad: c2.BatchLoad, GestureSupport: c2.GestureSupport, LoadBuiltinCommands: c2.LoadBuiltinCommands, @@ -144,59 +140,3 @@ func TestReaderConfigBadJSON(t *testing.T) { // This error is an stdlib error, not ours, so we don't care // about its type } - -func TestDuration_HappyPath(t *testing.T) { - d := Duration(time.Second) - marshalled, err := d.MarshalJSON() - if err != nil { - t.Fatalf("marshal duration failed: %v", err) - } - d2 := new(Duration) - err = d2.UnmarshalJSON(marshalled) - if err != nil { - t.Fatalf("unmarshal failed: %v", err) - } - marshalled2, err := d2.MarshalJSON() - if err != nil { - t.Fatalf("marshal duration 2 failed: %v", err) - } - if !bytes.Equal(marshalled, marshalled2) { - t.Fatalf("marshals not equal: %v vs %v", string(marshalled), string(marshalled2)) - } -} - -func TestDuration_UnmarshalJSON_Float(t *testing.T) { - f := []byte("10.0") - d2 := new(Duration) - err := d2.UnmarshalJSON(f) - if err != nil { - t.Fatalf("unmarshal failed: %v", err) - } -} - -func TestDuration_UnmarshalJSON_Boolean(t *testing.T) { - f := []byte("false") - d2 := new(Duration) - err := d2.UnmarshalJSON(f) - if err == nil { - t.Fatalf("expected failure in unmarshal") - } -} - -func TestDuration_UnmarshalJSON_BadString(t *testing.T) { - f := []byte("\"10mmmm\"") - d2 := new(Duration) - err := d2.UnmarshalJSON(f) - if err == nil { - t.Fatalf("expected failure in unmarshal") - } -} - -func TestDuration_UnmarshalJSON_BadJSON(t *testing.T) { - f := []byte("\"1mm") - d2 := new(Duration) - err := d2.UnmarshalJSON(f) - if err == nil { - t.Fatalf("expected failure in unmarshal") - } -} diff --git a/debugstream/scopeHelper.go b/debugstream/scopeHelper.go index b3e4183e..465e3c93 100644 --- a/debugstream/scopeHelper.go +++ b/debugstream/scopeHelper.go @@ -69,14 +69,13 @@ const explainMouseDetails = "the mext mouse click on the given window will print func mouseCommands(w window.Window) func([]string) string { return func(tokenString []string) string { - w.EventHandler().GlobalBind("MouseRelease", mouseDetails(w)) + event.GlobalBind(w.EventHandler(), mouse.Release, mouseDetails(w)) return "" } } -func mouseDetails(w window.Window) func(event.CID, interface{}) int { - return func(nothing event.CID, mevent interface{}) int { - me := mevent.(mouse.Event) +func mouseDetails(w window.Window) func(*mouse.Event) event.Response { + return func(me *mouse.Event) event.Response { viewPos := w.Viewport() x := int(me.X()) + viewPos[0] y := int(me.Y()) + viewPos[1] @@ -86,17 +85,19 @@ func mouseDetails(w window.Window) func(event.CID, interface{}) int { if len(results) == 0 { results = mouse.Hits(loc) } + cm := w.GetCallerMap() + if len(results) > 0 { - i := int(results[0].CID) - if i > 0 && event.HasEntity(event.CID(i)) { - e := event.GetEntity(event.CID(i)) + i := results[0].CID + if i > 0 && cm.HasEntity(i) { + e := cm.HasEntity(i) fmt.Printf("%+v\n", e) } else { fmt.Println("No entity ", i) } } - return event.UnbindSingle + return event.ResponseUnbindThisBinding } } diff --git a/debugtools/inputviz/joystick.go b/debugtools/inputviz/joystick.go index ba61d6a3..a1c0c16f 100644 --- a/debugtools/inputviz/joystick.go +++ b/debugtools/inputviz/joystick.go @@ -47,7 +47,7 @@ type Joystick struct { BaseLayer int ctx *scene.Context - event.CID + event.CallerID joy *joystick.Joystick rs map[string]render.Modifiable lastState *joystick.State @@ -55,11 +55,12 @@ type Joystick struct { lStickCenter floatgeom.Point2 rStickCenter floatgeom.Point2 cancel func() + + bindings []event.Binding } -func (j *Joystick) Init() event.CID { - j.CID = j.ctx.CallerMap.NextID(j) - return j.CID +func (j *Joystick) CID() event.CallerID { + return j.CallerID } func (j *Joystick) RenderAndListen(ctx *scene.Context, joy *joystick.Joystick, layer int) error { @@ -76,7 +77,7 @@ func (j *Joystick) RenderAndListen(ctx *scene.Context, joy *joystick.Joystick, l j.rs = make(map[string]render.Modifiable) j.lastState = &joystick.State{} j.ctx = ctx - j.Init() + j.CallerID = ctx.CallerMap.Register(j) j.rs["Outline"] = outline j.rs["LtStick"] = render.NewCircle(color.RGBA{255, 255, 255, 255}, 15, 12) j.rs["RtStick"] = render.NewCircle(color.RGBA{255, 255, 255, 255}, 15, 12) @@ -180,19 +181,22 @@ func (j *Joystick) RenderAndListen(ctx *scene.Context, joy *joystick.Joystick, l joystick.InputRightShoulder, } - j.CheckedIDBind(joystick.Disconnected, func(rend *Joystick, _ uint32) { + b1 := event.Bind(ctx, joystick.Disconnected, j, func(rend *Joystick, _ uint32) event.Response { j.Destroy() + return 0 }) - j.CheckedBind(key.Down+key.Spacebar, func(rend *Joystick, st *joystick.State) { + // TODO: it is bad that you need to import two 'key' packages + b2 := event.Bind(ctx, key.Down(key.Spacebar), j, func(j *Joystick, _ key.Event) event.Response { j.joy.Vibrate(math.MaxUint16, math.MaxUint16) go func() { time.Sleep(1 * time.Second) j.joy.Vibrate(0, 0) }() + return 0 }) - j.CheckedBind(joystick.Change, func(rend *Joystick, st *joystick.State) { + b3 := event.Bind(ctx, joystick.Change, j, func(j *Joystick, st *joystick.State) event.Response { for _, inputB := range bts { b := string(inputB) r := j.rs[b] @@ -211,60 +215,36 @@ func (j *Joystick) RenderAndListen(ctx *scene.Context, joy *joystick.Joystick, l tgr = "RtTrigger" x = j.rs[tgr].X() j.rs[tgr].SetPos(x, j.triggerY+float64(st.TriggerR/16)) + return 0 }) - j.CheckedBind(joystick.LtStickChange, func(rend *Joystick, st *joystick.State) { + b4 := event.Bind(ctx, joystick.LtStickChange, j, func(j *Joystick, st *joystick.State) event.Response { pos := j.lStickCenter pos = pos.Add(floatgeom.Point2{ float64(st.StickLX / 2048), -float64(st.StickLY / 2048), }) j.rs["LtStick"].SetPos(pos.X(), pos.Y()) + return 0 }) - j.CheckedBind(joystick.RtStickChange, func(rend *Joystick, st *joystick.State) { + b5 := event.Bind(ctx, joystick.RtStickChange, j, func(j *Joystick, st *joystick.State) event.Response { pos := j.rStickCenter pos = pos.Add(floatgeom.Point2{ float64(st.StickRX / 2048), -float64(st.StickRY / 2048), }) j.rs["RtStick"].SetPos(pos.X(), pos.Y()) - }) - return nil -} - -func (j *Joystick) CheckedIDBind(ev string, f func(*Joystick, uint32)) { - j.Bind(ev, func(id event.CID, jid interface{}) int { - joy, ok := event.GetEntity(id).(*Joystick) - if !ok { - return 0 - } - n, ok := jid.(uint32) - if !ok { - return 0 - } - f(joy, n) - return 0 - }) -} - -func (j *Joystick) CheckedBind(ev string, f func(*Joystick, *joystick.State)) { - j.Bind(ev, func(id event.CID, state interface{}) int { - joy, ok := event.GetEntity(id).(*Joystick) - if !ok { - return 0 - } - st, ok := state.(*joystick.State) - if !ok { - return 0 - } - f(joy, st) return 0 }) + j.bindings = []event.Binding{b1, b2, b3, b4, b5} + return nil } func (j *Joystick) Destroy() { - j.UnbindAll() + for _, b := range j.bindings { + b.Unbind() + } for _, r := range j.rs { r.Undraw() } diff --git a/debugtools/inputviz/keyboard.go b/debugtools/inputviz/keyboard.go index 6f21712e..d259dd69 100644 --- a/debugtools/inputviz/keyboard.go +++ b/debugtools/inputviz/keyboard.go @@ -11,7 +11,7 @@ import ( ) type KeyboardLayout interface { - KeyRect(k string) floatgeom.Rect2 + KeyRect(k key.Code) floatgeom.Rect2 } type LayoutKey interface { @@ -19,7 +19,7 @@ type LayoutKey interface { } type LayoutPosition struct { - Key string + Key key.Code Gap bool Width float64 Height float64 @@ -36,37 +36,37 @@ func (g gap) Pos() LayoutPosition { } } -type standardKey string +type standardKey key.Code func (s standardKey) Pos() LayoutPosition { return LayoutPosition{ - Key: string(s), + Key: key.Code(s), Width: 1, Height: 1, } } type wideKey struct { - k string + k key.Code w float64 } func (w wideKey) Pos() LayoutPosition { return LayoutPosition{ - Key: string(w.k), + Key: w.k, Width: w.w, Height: 1, } } type tallKey struct { - k string + k key.Code h float64 } func (h tallKey) Pos() LayoutPosition { return LayoutPosition{ - Key: string(h.k), + Key: h.k, Width: 1, Height: h.h, } @@ -74,7 +74,7 @@ func (h tallKey) Pos() LayoutPosition { type LayoutQWERTY struct { Bounds floatgeom.Rect2 - layoutMap map[string]LayoutPosition + layoutMap map[key.Code]LayoutPosition } func (l *LayoutQWERTY) init() { @@ -83,14 +83,14 @@ func (l *LayoutQWERTY) init() { } type sk = standardKey - l.layoutMap = make(map[string]LayoutPosition) + l.layoutMap = make(map[key.Code]LayoutPosition) qwertyRows := [][]LayoutKey{ {sk(key.Escape), gap(1), sk(key.F1), sk(key.F2), sk(key.F3), sk(key.F4), gap(.5), sk(key.F5), sk(key.F6), sk(key.F7), sk(key.F8), gap(.5), sk(key.F9), sk(key.F10), sk(key.F11), sk(key.F12), gap(2.1), sk(key.Pause)}, - {sk(key.GraveAccent), sk(key.One), sk(key.Two), sk(key.Three), sk(key.Four), sk(key.Five), sk(key.Six), sk(key.Seven), sk(key.Eight), sk(key.Nine), sk(key.Zero), sk(key.HyphenMinus), sk(key.EqualSign), wideKey{key.DeleteBackspace, 2.0}, gap(.1), sk(key.Insert), sk(key.Home), sk(key.PageUp), gap(.1), sk(key.KeypadNumLock), sk(key.KeypadSlash), sk(key.KeypadAsterisk), sk(key.KeypadHyphenMinus)}, + {sk(key.GraveAccent), sk(key.Num1), sk(key.Num2), sk(key.Num3), sk(key.Num4), sk(key.Num5), sk(key.Num6), sk(key.Num7), sk(key.Num8), sk(key.Num9), sk(key.Num0), sk(key.HyphenMinus), sk(key.EqualSign), wideKey{key.DeleteBackspace, 2.0}, gap(.1), sk(key.Insert), sk(key.Home), sk(key.PageUp), gap(.1), sk(key.KeypadNumLock), sk(key.KeypadSlash), sk(key.KeypadAsterisk), sk(key.KeypadHyphenMinus)}, {wideKey{key.Tab, 1.5}, sk(key.Q), sk(key.W), sk(key.E), sk(key.R), sk(key.T), sk(key.Y), sk(key.U), sk(key.I), sk(key.O), sk(key.P), sk(key.LeftSquareBracket), sk(key.RightSquareBracket), wideKey{key.Backslash, 1.5}, gap(.1), sk(key.DeleteForward), sk(key.End), sk(key.PageDown), gap(.1), sk(key.Keypad7), sk(key.Keypad8), sk(key.Keypad9), tallKey{key.KeypadPlusSign, 2}}, {wideKey{key.CapsLock, 1.5}, sk(key.A), sk(key.S), sk(key.D), sk(key.F), sk(key.G), sk(key.H), sk(key.J), sk(key.K), sk(key.L), sk(key.Semicolon), sk(key.Apostrophe), wideKey{key.ReturnEnter, 2.5}, gap(3.2), sk(key.Keypad4), sk(key.Keypad5), sk(key.Keypad6)}, {wideKey{key.LeftShift, 2.0}, sk(key.Z), sk(key.X), sk(key.C), sk(key.V), sk(key.B), sk(key.N), sk(key.M), sk(key.Comma), sk(key.FullStop), sk(key.Slash), wideKey{key.RightShift, 3.0}, gap(1.1), sk(key.UpArrow), gap(1.1), sk(key.Keypad1), sk(key.Keypad2), sk(key.Keypad3), tallKey{key.KeypadEnter, 2.0}}, - {wideKey{key.LeftControl, 1.5}, sk(key.LeftGUI), wideKey{key.LeftAlt, 1.5}, wideKey{key.Spacebar, 7.0}, wideKey{key.RightAlt, 1.5}, sk(key.RightGUI), wideKey{key.RightControl, 1.5}, gap(.1), sk(key.LeftArrow), sk(key.DownArrow), sk(key.RightArrow), gap(.1), wideKey{key.Keypad0, 2.0}, sk(key.KeypadPeriod)}, + {wideKey{key.LeftControl, 1.5}, sk(key.LeftGUI), wideKey{key.LeftAlt, 1.5}, wideKey{key.Spacebar, 7.0}, wideKey{key.RightAlt, 1.5}, sk(key.RightGUI), wideKey{key.RightControl, 1.5}, gap(.1), sk(key.LeftArrow), sk(key.DownArrow), sk(key.RightArrow), gap(.1), wideKey{key.Keypad0, 2.0}, sk(key.KeypadFullStop)}, } rowFloats := []float64{0.0, 1.1, 2.1, 3.1, 4.1, 5.1} for row, cols := range qwertyRows { @@ -98,7 +98,7 @@ func (l *LayoutQWERTY) init() { cf := 0.0 for _, v := range cols { ps := v.Pos() - if ps.Key != "" { + if ps.Key != 0 { l.layoutMap[ps.Key] = LayoutPosition{ Row: rf, Col: cf, @@ -111,7 +111,7 @@ func (l *LayoutQWERTY) init() { } } -func (l *LayoutQWERTY) KeyRect(k string) floatgeom.Rect2 { +func (l *LayoutQWERTY) KeyRect(k key.Code) floatgeom.Rect2 { l.init() pos, ok := l.layoutMap[k] @@ -137,31 +137,32 @@ func (l *LayoutQWERTY) KeyRect(k string) floatgeom.Rect2 { return floatgeom.NewRect2WH(x, y, keyWidth, keyHeight) } -var defaultColors = map[string]color.Color{} +var defaultColors = map[key.Code]color.Color{} type Keyboard struct { Rect floatgeom.Rect2 BaseLayer int - Colors map[string]color.Color + Colors map[key.Code]color.Color KeyboardLayout RenderCharacters bool Font *render.Font - event.CID + event.CallerID ctx *scene.Context - rs map[string]*render.Switch + rs map[key.Code]*render.Switch + + bindings []event.Binding } -func (k *Keyboard) Init() event.CID { - k.CID = k.ctx.CallerMap.NextID(k) - return k.CID +func (k *Keyboard) CID() event.CallerID { + return k.CallerID } func (k *Keyboard) RenderAndListen(ctx *scene.Context, layer int) error { k.ctx = ctx - k.Init() + k.CallerID = k.ctx.CallerMap.Register(k) if k.Rect.W() == 0 || k.Rect.H() == 0 { k.Rect.Max = k.Rect.Min.Add(floatgeom.Point2{320, 180}) @@ -178,9 +179,9 @@ func (k *Keyboard) RenderAndListen(ctx *scene.Context, layer int) error { k.Font = render.DefaultFont() } - k.rs = make(map[string]*render.Switch) + k.rs = make(map[key.Code]*render.Switch) - for kv := range key.AllKeys { + for kv, kstr := range key.AllKeys { rect := k.KeyboardLayout.KeyRect(kv) if rect == (floatgeom.Rect2{}) { continue @@ -198,7 +199,7 @@ func (k *Keyboard) RenderAndListen(ctx *scene.Context, layer int) error { k.rs[kv] = r if k.RenderCharacters { x, y := rect.Min.X(), rect.Min.Y() - txt := k.Font.NewText(kv, x, y) + txt := k.Font.NewText(kstr, x, y) tw, th := txt.GetDims() xBuffer := rect.W() - float64(tw) yBuffer := rect.H() - float64(th) @@ -220,30 +221,28 @@ func (k *Keyboard) RenderAndListen(ctx *scene.Context, layer int) error { } } - k.Bind(key.Down, key.Binding(func(id event.CID, ev key.Event) int { - kb, _ := k.ctx.CallerMap.GetEntity(id).(*Keyboard) - btn := ev.Code.String()[4:] - if kb.rs[btn] == nil { + b1 := event.Bind(ctx, key.AnyDown, k, func(kb *Keyboard, ev key.Event) event.Response { + if kb.rs[ev.Code] == nil { return 0 } - kb.rs[btn].Set("pressed") + kb.rs[ev.Code].Set("pressed") return 0 - })) - k.Bind(key.Up, key.Binding(func(id event.CID, ev key.Event) int { - kb, _ := k.ctx.CallerMap.GetEntity(id).(*Keyboard) - btn := ev.Code.String()[4:] - if kb.rs[btn] == nil { + }) + b2 := event.Bind(ctx, key.AnyUp, k, func(kb *Keyboard, ev key.Event) event.Response { + if kb.rs[ev.Code] == nil { return 0 } - kb.rs[btn].Set("released") + kb.rs[ev.Code].Set("released") return 0 - })) - + }) + k.bindings = []event.Binding{b1, b2} return nil } func (k *Keyboard) Destroy() { - k.UnbindAll() + for _, b := range k.bindings { + b.Unbind() + } for _, r := range k.rs { r.Undraw() } diff --git a/debugtools/inputviz/mouse.go b/debugtools/inputviz/mouse.go index 3356a7a0..fbaceb88 100644 --- a/debugtools/inputviz/mouse.go +++ b/debugtools/inputviz/mouse.go @@ -17,7 +17,7 @@ type Mouse struct { Rect floatgeom.Rect2 BaseLayer int - event.CID + event.CallerID ctx *scene.Context rs map[mouse.Button]*render.Switch @@ -27,16 +27,17 @@ type Mouse struct { stateIncLock sync.RWMutex stateInc map[mouse.Button]int + + bindings []event.Binding } -func (m *Mouse) Init() event.CID { - m.CID = m.ctx.CallerMap.NextID(m) - return m.CID +func (m *Mouse) CID() event.CallerID { + return m.CallerID } func (m *Mouse) RenderAndListen(ctx *scene.Context, layer int) error { m.ctx = ctx - m.Init() + m.CallerID = ctx.Register(m) if m.Rect.W() == 0 || m.Rect.H() == 0 { m.Rect.Max = m.Rect.Min.Add(floatgeom.Point2{60, 100}) @@ -98,24 +99,21 @@ func (m *Mouse) RenderAndListen(ctx *scene.Context, layer int) error { ctx.DrawStack.Draw(m.posText, m.BaseLayer, layer+2) } - m.Bind(mouse.Press, mouse.Binding(func(id event.CID, ev *mouse.Event) int { - m, _ := m.ctx.CallerMap.GetEntity(id).(*Mouse) + b1 := event.Bind(ctx, mouse.Press, m, func(m *Mouse, ev *mouse.Event) event.Response { m.rs[ev.Button].Set("pressed") m.stateIncLock.Lock() m.stateInc[ev.Button]++ m.stateIncLock.Unlock() return 0 - })) - m.Bind(mouse.Release, mouse.Binding(func(id event.CID, ev *mouse.Event) int { - m, _ := m.ctx.CallerMap.GetEntity(id).(*Mouse) + }) + b2 := event.Bind(ctx, mouse.Release, m, func(m *Mouse, ev *mouse.Event) event.Response { m.rs[ev.Button].Set("released") m.stateIncLock.Lock() m.stateInc[ev.Button]++ m.stateIncLock.Unlock() return 0 - })) - m.Bind(mouse.ScrollDown, mouse.Binding(func(id event.CID, e *mouse.Event) int { - m, _ := m.ctx.CallerMap.GetEntity(id).(*Mouse) + }) + b3 := event.Bind(ctx, mouse.ScrollDown, m, func(m *Mouse, ev *mouse.Event) event.Response { m.rs[mouse.ButtonMiddle].Set("scrolldown") m.stateIncLock.Lock() m.stateInc[mouse.ButtonMiddle]++ @@ -129,9 +127,8 @@ func (m *Mouse) RenderAndListen(ctx *scene.Context, layer int) error { m.stateIncLock.Unlock() }) return 0 - })) - m.Bind(mouse.ScrollUp, mouse.Binding(func(id event.CID, e *mouse.Event) int { - m, _ := m.ctx.CallerMap.GetEntity(id).(*Mouse) + }) + b4 := event.Bind(ctx, mouse.ScrollUp, m, func(m *Mouse, ev *mouse.Event) event.Response { m.rs[mouse.ButtonMiddle].Set("scrollup") m.stateIncLock.Lock() m.stateInc[mouse.ButtonMiddle]++ @@ -145,13 +142,12 @@ func (m *Mouse) RenderAndListen(ctx *scene.Context, layer int) error { m.stateIncLock.Unlock() }) return 0 - })) - m.Bind(mouse.Drag, mouse.Binding(func(id event.CID, e *mouse.Event) int { - m, _ := m.ctx.CallerMap.GetEntity(id).(*Mouse) - m.lastMousePos.Point2 = e.Point2 + }) + b5 := event.Bind(ctx, mouse.Drag, m, func(m *Mouse, ev *mouse.Event) event.Response { + m.lastMousePos.Point2 = ev.Point2 return 0 - })) - + }) + m.bindings = []event.Binding{b1, b2, b3, b4, b5} return nil } @@ -164,7 +160,10 @@ func (ps *posStringer) String() string { } func (m *Mouse) Destroy() { - m.UnbindAll() + // TODO: this is a lot of code to write to track and unbind all of an entity's bindings + for _, b := range m.bindings { + b.Unbind() + } for _, r := range m.rs { r.Undraw() } diff --git a/debugtools/mouse.go b/debugtools/mouse.go index 8aebcac2..2c8aae56 100644 --- a/debugtools/mouse.go +++ b/debugtools/mouse.go @@ -3,16 +3,16 @@ package debugtools import ( "github.com/oakmound/oak/v3/dlog" "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v3/key" "github.com/oakmound/oak/v3/mouse" "github.com/oakmound/oak/v3/scene" ) // DebugMouseRelease will print the position and button pressed of the mouse when the mouse is released, if the given -// key is held down at the time. If no key is given, it will always be printed -func DebugMouseRelease(ctx *scene.Context, k string) { - ctx.EventHandler.GlobalBind(mouse.Release, func(_ event.CID, ev interface{}) int { - mev, _ := ev.(*mouse.Event) - if k == "" || ctx.KeyState.IsDown(k) { +// key is held down at the time. If 0 is given, it will always be printed +func DebugMouseRelease(ctx *scene.Context, k key.Code) { + event.GlobalBind(ctx, mouse.Release, func(mev *mouse.Event) event.Response { + if k == 0 || ctx.KeyState.IsDown(k) { dlog.Info(mev) } return 0 diff --git a/default.go b/default.go index 75384bbe..2ac9a638 100644 --- a/default.go +++ b/default.go @@ -7,6 +7,7 @@ import ( "github.com/oakmound/oak/v3/alg/intgeom" "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v3/key" "github.com/oakmound/oak/v3/render" "github.com/oakmound/oak/v3/scene" ) @@ -37,27 +38,15 @@ func AddScene(name string, sc scene.Scene) error { } // IsDown calls IsDown on the default window. -func IsDown(key string) bool { +func IsDown(k key.Code) bool { initDefaultWindow() - return defaultWindow.IsDown(key) + return defaultWindow.IsDown(k) } // IsHeld calls IsHeld on the default window. -func IsHeld(key string) (bool, time.Duration) { +func IsHeld(k key.Code) (bool, time.Duration) { initDefaultWindow() - return defaultWindow.IsHeld(key) -} - -// SetUp calls SetUp on the default window. -func SetUp(key string) { - initDefaultWindow() - defaultWindow.SetUp(key) -} - -// SetDown calls SetDown on the default window. -func SetDown(key string) { - initDefaultWindow() - defaultWindow.SetDown(key) + return defaultWindow.IsHeld(k) } // SetViewportBounds calls SetViewportBounds on the default window. diff --git a/entities/doodad.go b/entities/doodad.go index 47c4beff..807d9685 100644 --- a/entities/doodad.go +++ b/entities/doodad.go @@ -8,7 +8,7 @@ import ( // A Doodad is an entity composed of a position, a renderable, and a CallerID. type Doodad struct { Point - event.CID + event.CallerID R render.Renderable } @@ -17,43 +17,23 @@ type Doodad struct { // any other CID will assume that the struct containing this doodad has // already been initialized to the passed in CID. // This applies to ALL NewX functions in entities which take in a CID. -func NewDoodad(x, y float64, r render.Renderable, cid event.CID) *Doodad { +func NewDoodad(x, y float64, r render.Renderable, cid event.CallerID) *Doodad { if r != nil { r.SetPos(x, y) } - d := Doodad{} + d := &Doodad{} d.Point = *NewPoint(x, y) d.R = r - d.CID = cid.Parse(&d) - return &d -} - -// Init satisfies event.Entity -func (d *Doodad) Init() event.CID { - d.CID = event.NextID(d) - return d.CID + if cid == 0 { + d.CallerID = event.DefaultCallerMap.Register(d) + } else { + d.CallerID = cid + } + return d } -// GetID returns this Doodad's CID -// Consider: are these getters needed? -func (d *Doodad) GetID() event.CID { - return d.CID -} - -// GetRenderable returns this Doodad's Renderable -func (d *Doodad) GetRenderable() render.Renderable { - return d.R -} - -// SetRenderable sets this Doodad's renderable, drawing it. -// Todo:this automatic drawing doesn't really work with our -// two tiers of draw layers -func (d *Doodad) SetRenderable(r render.Renderable) { - if d.R != nil { - d.R.Undraw() - } - d.R = r - render.Draw(d.R, d.R.GetLayer()) +func (d *Doodad) CID() event.CallerID { + return d.CallerID } // Destroy cleans up the events, renderable and @@ -62,18 +42,25 @@ func (d *Doodad) Destroy() { if d.R != nil { d.R.Undraw() } - d.CID.UnbindAll() - event.DestroyEntity(d.CID) + event.DefaultBus.UnbindAllFrom(d.CallerID) + event.DefaultCallerMap.RemoveEntity(d.CallerID) } // Overwrites // SetPos both Sets logical position and renderable position // The need for this sort of function is lessened with the introduction -// of vector attachement. +// of vector attachment. func (d *Doodad) SetPos(x, y float64) { d.SetLogicPos(x, y) if d.R != nil { d.R.SetPos(x, y) } } + +// GetRenmderable retrieves the renderable. +// Mainly used to satisfy upper level interfaces. +// TODO: remove along with entity rework +func (d *Doodad) GetRenderable() render.Renderable { + return d.R +} diff --git a/entities/interactive.go b/entities/interactive.go index ffee9d0b..1a24c77c 100644 --- a/entities/interactive.go +++ b/entities/interactive.go @@ -15,22 +15,23 @@ type Interactive struct { // NewInteractive returns a new Interactive func NewInteractive(x, y, w, h float64, r render.Renderable, tree *collision.Tree, - cid event.CID, friction float64) *Interactive { + cid event.CallerID, friction float64) *Interactive { - i := Interactive{} - cid = cid.Parse(&i) - i.Reactive = *NewReactive(x, y, w, h, r, tree, cid) + i := &Interactive{} + if cid == 0 { + i.CallerID = event.DefaultCallerMap.Register(i) + } else { + i.CallerID = cid + } + i.Reactive = *NewReactive(x, y, w, h, r, tree, i.CallerID) i.vMoving = vMoving{ Delta: physics.NewVector(0, 0), Speed: physics.NewVector(0, 0), Friction: friction, } - return &i + return i } -// Init satisfies event.Entity -func (iv *Interactive) Init() event.CID { - cID := event.NextID(iv) - iv.CID = cID - return cID +func (i *Interactive) CID() event.CallerID { + return i.CallerID } diff --git a/entities/moving.go b/entities/moving.go index da37e3af..62b3a575 100644 --- a/entities/moving.go +++ b/entities/moving.go @@ -14,22 +14,24 @@ type Moving struct { } // NewMoving returns a new Moving -func NewMoving(x, y, w, h float64, r render.Renderable, tree *collision.Tree, cid event.CID, friction float64) *Moving { - m := Moving{} - cid = cid.Parse(&m) - m.Solid = *NewSolid(x, y, w, h, r, tree, cid) +func NewMoving(x, y, w, h float64, r render.Renderable, tree *collision.Tree, cid event.CallerID, friction float64) *Moving { + m := &Moving{} + if cid == 0 { + m.CallerID = event.DefaultCallerMap.Register(m) + } else { + m.CallerID = cid + } + m.Solid = *NewSolid(x, y, w, h, r, tree, m.CallerID) m.vMoving = vMoving{ Delta: physics.NewVector(0, 0), Speed: physics.NewVector(0, 0), Friction: friction, } - return &m + return m } -// Init satisfies event.Entity -func (m *Moving) Init() event.CID { - m.CID = event.NextID(m) - return m.CID +func (m *Moving) CID() event.CallerID { + return m.CallerID } // ShiftVector probably shouldn't be on moving but it lets you diff --git a/entities/reactive.go b/entities/reactive.go index 4acfa644..d6f0a85f 100644 --- a/entities/reactive.go +++ b/entities/reactive.go @@ -17,10 +17,14 @@ type Reactive struct { // NewReactive returns a new Reactive struct. The added space will // be added to the input tree, or DefTree if none is given. -func NewReactive(x, y, w, h float64, r render.Renderable, tree *collision.Tree, cid event.CID) *Reactive { - rct := Reactive{} - cid = cid.Parse(&rct) - rct.Doodad = *NewDoodad(x, y, r, cid) +func NewReactive(x, y, w, h float64, r render.Renderable, tree *collision.Tree, cid event.CallerID) *Reactive { + rct := &Reactive{} + if cid == 0 { + rct.CallerID = event.DefaultCallerMap.Register(rct) + } else { + rct.CallerID = cid + } + rct.Doodad = *NewDoodad(x, y, r, rct.CallerID) rct.W = w rct.H = h rct.RSpace = collision.NewReactiveSpace(collision.NewSpace(x, y, w, h, cid), map[collision.Label]collision.OnHit{}) @@ -30,7 +34,7 @@ func NewReactive(x, y, w, h float64, r render.Renderable, tree *collision.Tree, rct.RSpace.Tree = tree rct.Tree = tree rct.Tree.Add(rct.RSpace.Space) - return &rct + return rct } // SetDim sets the dimensions of this reactive's space and it's logical dimensions @@ -72,10 +76,8 @@ func (r *Reactive) GetReactiveSpace() *collision.ReactiveSpace { // Overwrites -// Init satisfies event.Entity -func (r *Reactive) Init() event.CID { - r.CID = event.NextID(r) - return r.CID +func (r *Reactive) CID() event.CallerID { + return r.CallerID } // ShiftPos acts like SetPos if given r.X()+x, r.Y()+y diff --git a/entities/solid.go b/entities/solid.go index 01b7e0d2..357f374c 100644 --- a/entities/solid.go +++ b/entities/solid.go @@ -17,19 +17,27 @@ type Solid struct { // NewSolid returns an initialized Solid that is not drawn and whose space // belongs to the given collision tree. If nil is given as the tree, it will // belong to collision.DefTree -func NewSolid(x, y, w, h float64, r render.Renderable, tree *collision.Tree, cid event.CID) *Solid { - s := Solid{} - cid = cid.Parse(&s) - s.Doodad = *NewDoodad(x, y, r, cid) +func NewSolid(x, y, w, h float64, r render.Renderable, tree *collision.Tree, cid event.CallerID) *Solid { + s := &Solid{} + if cid == 0 { + s.CallerID = event.DefaultCallerMap.Register(s) + } else { + s.CallerID = cid + } + s.Doodad = *NewDoodad(x, y, r, s.CallerID) s.W = w s.H = h if tree == nil { tree = collision.DefaultTree } s.Tree = tree - s.Space = collision.NewSpace(x, y, w, h, cid) + s.Space = collision.NewSpace(x, y, w, h, s.CallerID) s.Tree.Add(s.Space) - return &s + return s +} + +func (s *Solid) CID() event.CallerID { + return s.CallerID } // SetDim sets the logical dimensions of the solid and the real @@ -98,12 +106,6 @@ func (s *Solid) HitLabel(classtype collision.Label) *collision.Space { // Overwrites -// Init satisfies event.Entity -func (s *Solid) Init() event.CID { - s.CID = event.NextID(s) - return s.CID -} - // SetPos sets the position of the collision space, the logical position, // and the renderable position of the solid. func (s *Solid) SetPos(x float64, y float64) { diff --git a/entities/x/btn/box.go b/entities/x/btn/box.go index 54b3be7b..b5a3606b 100644 --- a/entities/x/btn/box.go +++ b/entities/x/btn/box.go @@ -14,10 +14,17 @@ type Box struct { metadata map[string]string } +func (b Box) CID() event.CallerID { + return b.Solid.CID() +} + // NewBox creates a new Box -func NewBox(cid event.CID, x, y, w, h float64, r render.Renderable, layers ...int) *Box { +func NewBox(cid event.CallerID, x, y, w, h float64, r render.Renderable, layers ...int) *Box { b := Box{} - cid = cid.Parse(&b) + if cid == 0 { + // TODO: not default + cid = event.DefaultCallerMap.Register(b) + } b.Solid = *entities.NewSolid(x, y, w, h, r, mouse.DefaultTree, cid) if b.R != nil && len(layers) > 0 { render.Draw(b.R, layers...) @@ -26,12 +33,6 @@ func NewBox(cid event.CID, x, y, w, h float64, r render.Renderable, layers ...in return &b } -// Init intializes the Box -func (b *Box) Init() event.CID { - b.CID = event.NextID(b) - return b.CID -} - // GetRenderable returns the box's renderable func (b *Box) GetRenderable() render.Renderable { return b.R @@ -54,7 +55,8 @@ func (b *Box) Metadata(k string) (v string, ok bool) { } func (b *Box) Destroy() { - b.UnbindAll() + // TODO: not default + event.DefaultBus.UnbindAllFrom(b.CallerID) b.R.Undraw() mouse.Remove(b.GetSpace()) -} \ No newline at end of file +} diff --git a/entities/x/btn/button.go b/entities/x/btn/button.go index 132de378..00407564 100644 --- a/entities/x/btn/button.go +++ b/entities/x/btn/button.go @@ -4,7 +4,6 @@ import ( "fmt" "image/color" "strconv" - "strings" "github.com/oakmound/oak/v3/collision" "github.com/oakmound/oak/v3/dlog" @@ -28,14 +27,14 @@ type Generator struct { R1 render.Modifiable R2 render.Modifiable RS []render.Modifiable - Cid event.CID + Cid event.CallerID Font *render.Font Layers []int Text string TextPtr *string TextStringer fmt.Stringer Children []Generator - Bindings map[string][]event.Bindable + Bindings []func(caller Btn) event.Binding Trigger string Toggle *bool ListChoice *int @@ -66,7 +65,6 @@ func defGenerator() Generator { Font: nil, Layers: []int{0}, Text: "Button", - Bindings: make(map[string][]event.Bindable), Trigger: "MouseClickOn", Toggle: nil, @@ -98,7 +96,10 @@ func (g Generator) generate(parent *Generator) Btn { "on": g.R1, "off": g.R2, }) - g.Bindings["MouseClickOn"] = append(g.Bindings["MouseClickOn"], toggleFxn(g)) + g.Bindings = append(g.Bindings, func(caller Btn) event.Binding { + // TODO not default + return event.Bind(event.DefaultBus, mouse.ClickOn, caller, toggleFxn(g)) + }) case g.ListChoice != nil: start := "list" + strconv.Itoa(*g.ListChoice) @@ -109,9 +110,13 @@ func (g Generator) generate(parent *Generator) Btn { } mp["list"+strconv.Itoa(i)] = r } + box = render.NewSwitch(start, mp) - g.Bindings["MouseClickOn"] = append(g.Bindings["MouseClickOn"], listFxn(g)) + g.Bindings = append(g.Bindings, func(caller Btn) event.Binding { + // TODO not default + return event.Bind(event.DefaultBus, mouse.ClickOn, caller, listFxn(g)) + }) case g.R != nil: box = g.R case g.ProgressFunc != nil: @@ -159,56 +164,56 @@ func (g Generator) generate(parent *Generator) Btn { btn = bx } + // TODO: this is impossible with how we've done generics + // 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 { + // 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 event.CallerID, button interface{}) int { + // btn := id.E().(Btn) + // 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) + // } + // return 0 + // }, + // } + // } + // } - // 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 event.CID, button interface{}) int { - btn := id.E().(Btn) - 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) - } - return 0 - }, - } - } + for _, binding := range g.Bindings { + binding(btn) } - for k, v := range g.Bindings { - for _, b := range v { - btn.Bind(k, b) - } - } - - err := mouse.PhaseCollision(btn.GetSpace()) + // TODO: not default + err := mouse.PhaseCollision(btn.GetSpace(), event.DefaultBus) dlog.ErrorCheck(err) if g.Group != nil { @@ -239,9 +244,8 @@ type switcher interface { } // toggleFxn sets up the mouseclick binding for toggle buttons created for goreport cyclo decrease -func toggleFxn(g Generator) func(id event.CID, nothing interface{}) int { - return func(id event.CID, nothing interface{}) int { - btn := event.GetEntity(id).(Btn) +func toggleFxn(g Generator) func(btn Btn, payload *mouse.Event) event.Response { + return func(btn Btn, payload *mouse.Event) event.Response { if btn.GetRenderable().(switcher).Get() == "on" { if g.Group != nil && g.Group.active == btn { g.Group.active = nil @@ -253,7 +257,7 @@ func toggleFxn(g Generator) func(id event.CID, nothing interface{}) int { g.Group.active = btn for _, b := range g.Group.members { if b.GetRenderable().(switcher).Get() == "on" { - b.Trigger("MouseClickOn", nil) + event.TriggerForCallerOn(event.DefaultBus, b.CID(), mouse.ClickOn, payload) } } } @@ -267,19 +271,16 @@ func toggleFxn(g Generator) func(id event.CID, nothing interface{}) int { } // listFxn sets up the mouseclick binding for list buttons created for goreport cyclo reduction -func listFxn(g Generator) func(id event.CID, button interface{}) int { - return func(id event.CID, button interface{}) int { - btn := event.GetEntity(id).(Btn) +func listFxn(g Generator) func(btn Btn, payload *mouse.Event) event.Response { + return func(btn Btn, payload *mouse.Event) event.Response { i := *g.ListChoice - mEvent := button.(*mouse.Event) - - if mEvent.Button == mouse.ButtonLeft { + if payload.Button == mouse.ButtonLeft { i++ if i == len(g.RS) { i = 0 } - } else if mEvent.Button == mouse.ButtonRight { + } else if payload.Button == mouse.ButtonRight { i-- if i < 0 { i += len(g.RS) diff --git a/entities/x/btn/option.go b/entities/x/btn/option.go index b5b1b3cd..28e7ca87 100644 --- a/entities/x/btn/option.go +++ b/entities/x/btn/option.go @@ -64,7 +64,7 @@ func Offset(x, y float64) Option { } //CID sets the starting CID of the button to be generated -func CID(c event.CID) Option { +func CID(c event.CallerID) Option { return func(g Generator) Generator { g.Cid = c return g @@ -151,15 +151,18 @@ func ToggleList(chosen *int, rs ...render.Modifiable) Option { // Binding appends a function to be called when a specific event // is triggered. -func Binding(s string, bnd event.Bindable) Option { +func Binding[Payload any](ev event.EventID[Payload], bnd event.Bindable[Btn, Payload]) Option { return func(g Generator) Generator { - g.Bindings[s] = append(g.Bindings[s], bnd) + g.Bindings = append(g.Bindings, func(caller Btn) event.Binding { + // TODO: not default + return event.Bind(event.DefaultBus, ev, caller, bnd) + }) return g } } // Click appends a function to be called when the button is clicked on. -func Click(bnd event.Bindable) Option { +func Click(bnd event.Bindable[Btn, *mouse.Event]) Option { return Binding(mouse.ClickOn, bnd) } diff --git a/entities/x/btn/textBox.go b/entities/x/btn/textBox.go index cbe4f442..68ac4a5e 100644 --- a/entities/x/btn/textBox.go +++ b/entities/x/btn/textBox.go @@ -13,14 +13,8 @@ type TextBox struct { *render.Text } -// Init creates the CID -func (b *TextBox) Init() event.CID { - b.CID = event.NextID(b) - return b.CID -} - // NewTextBox creates a textbox -func NewTextBox(cid event.CID, x, y, w, h, txtX, txtY float64, +func NewTextBox(cid event.CallerID, x, y, w, h, txtX, txtY float64, f *render.Font, r render.Renderable, layers ...int) *TextBox { if f == nil { @@ -28,9 +22,9 @@ func NewTextBox(cid event.CID, x, y, w, h, txtX, txtY float64, } b := new(TextBox) - - cid = cid.Parse(b) - + if cid == 0 { + cid = event.DefaultCallerMap.Register(b) + } b.Box = *NewBox(cid, x, y, w, h, r, layers...) b.Text = f.NewText("Init", 0, 0) b.Text.Attach(b.Box.Vector, txtX, txtY) diff --git a/entities/x/force/directionSpace.go b/entities/x/force/directionSpace.go index 83014cee..345b1d70 100644 --- a/entities/x/force/directionSpace.go +++ b/entities/x/force/directionSpace.go @@ -10,11 +10,11 @@ import ( type DirectionSpace struct { *collision.Space physics.ForceVector + event.CallerID } -// Init initializes the DirectionSpace as an entity -func (ds *DirectionSpace) Init() event.CID { - return event.NextID(ds) +func (ds DirectionSpace) CID() event.CallerID { + return ds.CallerID } // NewDirectionSpace creates a DirectionSpace and initializes it as an entity. @@ -23,6 +23,7 @@ func NewDirectionSpace(s *collision.Space, v physics.ForceVector) *DirectionSpac Space: s, ForceVector: v, } - s.CID = ds.Init() + // TODO: not default + s.CID = event.DefaultCallerMap.Register(ds) return ds } diff --git a/entities/x/move/topdown.go b/entities/x/move/topdown.go index 8d16bbd5..a4a9c6c7 100644 --- a/entities/x/move/topdown.go +++ b/entities/x/move/topdown.go @@ -18,7 +18,7 @@ func Arrows(mvr Mover) { } // TopDown moves the given mover based on its speed as the given keys are pressed -func TopDown(mvr Mover, up, down, left, right string) { +func TopDown(mvr Mover, up, down, left, right key.Code) { delta := mvr.GetDelta() vec := mvr.Vec() spd := mvr.GetSpeed() diff --git a/entities/x/stat/default.go b/entities/x/stat/default.go index cd519ffd..aa6b3aec 100644 --- a/entities/x/stat/default.go +++ b/entities/x/stat/default.go @@ -1,5 +1,7 @@ package stat +import "github.com/oakmound/oak/v3/event" + var ( // DefStatistics is a base set of statistics used by package-level calls // When using multiple statistics, avoid using overlapping event names @@ -7,39 +9,39 @@ var ( ) // Inc triggers an event, incrementing the given statistic by one -func Inc(eventName string) { - DefStatistics.Inc(eventName) +func Inc(ev statEvent) { + DefStatistics.Inc(ev) } // Trigger triggers the given event with a given increment to update a statistic -func Trigger(eventName string, inc int) { - DefStatistics.Trigger(eventName, inc) +func Trigger(ev statEvent, inc int) { + DefStatistics.Trigger(ev, inc) } // TriggerOn triggers the given event, toggling it on -func TriggerOn(eventName string) { - DefStatistics.TriggerOn(eventName) +func TriggerOn(ev timedStatEvent) { + DefStatistics.TriggerOn(ev) } // TriggerOff triggers the given event, toggling it off -func TriggerOff(eventName string) { - DefStatistics.TriggerOff(eventName) +func TriggerOff(ev timedStatEvent) { + DefStatistics.TriggerOff(ev) } // TriggerTimed triggers the given event, toggling it on or off -func TriggerTimed(eventName string, on bool) { - DefStatistics.TriggerTimed(eventName, on) +func TriggerTimed(ev timedStatEvent, on bool) { + DefStatistics.TriggerTimed(ev, on) } // TrackStats records a stat event to the Statistics map and creates the statistic if it does not already exist -func TrackStats(no int, data interface{}) int { +func TrackStats(no int, data interface{}) event.Response { return DefStatistics.TrackStats(no, data) } // TrackTimeStats acts like TrackStats, but tracks durations of events. If the // event has not started, it logs a start time, and then when the event ends // it will log the delta since the start. -func TrackTimeStats(no int, data interface{}) int { +func TrackTimeStats(no int, data interface{}) event.Response { return DefStatistics.TrackTimeStats(no, data) } diff --git a/entities/x/stat/statistic.go b/entities/x/stat/statistic.go index 970fa494..9d1b0b7d 100644 --- a/entities/x/stat/statistic.go +++ b/entities/x/stat/statistic.go @@ -70,11 +70,11 @@ func (st *Statistics) trackStats(name string, val int) { } // TrackStats records a stat event to the Statistics map and creates the statistic if it does not already exist -func (st *Statistics) TrackStats(no int, data interface{}) int { +func (st *Statistics) TrackStats(no int, data interface{}) event.Response { stat, ok := data.(stat) if !ok { dlog.Error("TrackStats called with a non-stat payload") - return event.UnbindEvent + return event.ResponseUnbindThisBinding } st.trackStats(stat.name, stat.inc) return 0 @@ -83,11 +83,11 @@ func (st *Statistics) TrackStats(no int, data interface{}) int { // TrackTimeStats acts like TrackStats, but tracks durations of events. If the // event has not started, it logs a start time, and then when the event ends // it will log the delta since the start. -func (st *Statistics) TrackTimeStats(no int, data interface{}) int { +func (st *Statistics) TrackTimeStats(no int, data interface{}) event.Response { timed, ok := data.(timedStat) if !ok { dlog.Error("TrackTimeStats called with a non-timedStat payload") - return event.UnbindEvent + return event.ResponseUnbindThisBinding } if timed.on { //Turning on a thing to time track st.statTimeLock.Lock() diff --git a/entities/x/stat/stats.go b/entities/x/stat/stats.go index f7fe8358..6710c681 100644 --- a/entities/x/stat/stats.go +++ b/entities/x/stat/stats.go @@ -1,63 +1,81 @@ package stat -import "github.com/oakmound/oak/v3/event" +import ( + "fmt" + + "github.com/oakmound/oak/v3/event" +) + +// TODO: these functions are useless unless the types are exported, and +// if the types are exported the api is bad + +type timedStatEvent struct { + event event.EventID[timedStat] + fmt.Stringer +} type timedStat struct { name string on bool } + +type statEvent struct { + event event.EventID[stat] + fmt.Stringer +} + type stat struct { name string inc int } // TimedOn returns a binding that will trigger toggling on the given event -func TimedOn(eventName string) event.Bindable { - return TimedBind(eventName, true) +func TimedOn(ev timedStatEvent) event.UnsafeBindable { + return TimedBind(ev, true) } // TimedOff returns a binding that will trigger toggling off the given event -func TimedOff(eventName string) event.Bindable { - return TimedBind(eventName, false) +func TimedOff(ev timedStatEvent) event.UnsafeBindable { + return TimedBind(ev, false) } // TimedBind returns a binding that will trigger toggling on or off the given event -func TimedBind(eventName string, on bool) event.Bindable { - return func(event.CID, interface{}) int { - event.Trigger(eventName, timedStat{eventName, on}) +func TimedBind(ev timedStatEvent, on bool) event.UnsafeBindable { + return func(event.CallerID, event.Handler, interface{}) event.Response { + event.TriggerOn(event.DefaultBus, ev.event, timedStat{ev.String(), on}) return 0 } } // Bind returns a binding that will increment the given event by 'inc' -func Bind(eventName string, inc int) event.Bindable { - return func(event.CID, interface{}) int { - event.Trigger(eventName, stat{eventName, inc}) +func Bind(ev statEvent, inc int) event.UnsafeBindable { + return func(event.CallerID, event.Handler, interface{}) event.Response { + event.TriggerOn(event.DefaultBus, ev.event, stat{ev.String(), inc}) return 0 } } // Inc triggers an event, incrementing the given statistic by one -func (st *Statistics) Inc(eventName string) { - st.Trigger(eventName, 1) +func (st *Statistics) Inc(ev statEvent) { + st.Trigger(ev, 1) } // Trigger triggers the given event with a given increment to update a statistic -func (st *Statistics) Trigger(eventName string, inc int) { - event.Trigger(eventName, stat{eventName, inc}) +func (st *Statistics) Trigger(ev statEvent, inc int) { + event.TriggerOn(event.DefaultBus, ev.event, stat{ev.String(), inc}) } // TriggerOn triggers the given event, toggling it on -func (st *Statistics) TriggerOn(eventName string) { - st.TriggerTimed(eventName, true) +func (st *Statistics) TriggerOn(ev timedStatEvent) { + st.TriggerTimed(ev, true) } // TriggerOff triggers the given event, toggling it off -func (st *Statistics) TriggerOff(eventName string) { - st.TriggerTimed(eventName, false) +func (st *Statistics) TriggerOff(ev timedStatEvent) { + st.TriggerTimed(ev, false) } // TriggerTimed triggers the given event, toggling it on or off -func (st *Statistics) TriggerTimed(eventName string, on bool) { - event.Trigger(eventName, timedStat{eventName, on}) +func (st *Statistics) TriggerTimed(ev timedStatEvent, on bool) { + event.TriggerOn(event.DefaultBus, ev.event, timedStat{ev.String(), on}) } diff --git a/event/bind.go b/event/bind.go index 4a0b654f..11fa7d63 100644 --- a/event/bind.go +++ b/event/bind.go @@ -1,71 +1,150 @@ package event -// Bind adds a function to the event bus tied to the given callerID -// to be called when the event name is triggered. It is equivalent to -// calling BindPriority with a zero Priority. -func (eb *Bus) Bind(name string, callerID CID, fn Bindable) { - eb.pendingMutex.Lock() - eb.binds = append(eb.binds, UnbindOption{ - Event: Event{ - Name: name, - CallerID: callerID, - }, Fn: fn}) - eb.pendingMutex.Unlock() -} +import "sync/atomic" -// PersistentBind acts like Bind, but persists the binding such that if the event -// bus is reset, the binding will still trigger. Thes bindings should likely be global -// bindings, using a CID of 0, or be tolerant to the CID bound not being present after -// such a clear. -func (eb *Bus) PersistentBind(name string, callerID CID, fn Bindable) { - eb.pendingMutex.Lock() - opt := UnbindOption{ - Event: Event{ - Name: name, - CallerID: callerID, - }, Fn: fn} - eb.binds = append(eb.binds, opt) - eb.persistentBinds = append(eb.persistentBinds, opt) - eb.pendingMutex.Unlock() -} +// Q: Why do Bind / Unbind / etc not immediately take effect? +// A: For concurrent safety, most operations on a bus lock the bus. Triggers acquire a read lock on the bus, +// as they iterate over internal bus components. Most logic within an event bus will happen from within +// a Trigger call-- when an entity is destroyed by some collision, for example, all of its bindings should +// be unregistered. If one were to call Unbind from within a call to Trigger, the trigger would never release +// its lock-- so the unbind would never be able to take the lock-- so the bus would be unrecoverably stuck. + +// Q: Why not trust users to call Bind / Unbind / etc with `go`, to allow the caller to decide when to use +// concurrency? +// A: It is almost never correct to not call such a function with `go`, and it is a bad user experience for +// the engine to deadlock unexpectedly because you forgot to begin some call with a goroutine. -// ClearPersistentBindings removes all persistent bindings. It will not unbind them -// from the bus, but they will not be bound following the next bus reset. -func (eb *Bus) ClearPersistentBindings() { - eb.pendingMutex.Lock() - eb.persistentBinds = []UnbindOption{} - eb.pendingMutex.Unlock() +// A Binding, returned from calls to Bind, references the details of a binding and where that binding is +// stored within a handler. The common use case for this structure would involve a system that wanted to +// keep track of its bindings for later remote unbinding. This structure can also be used to construct +// and unbind a known reference. +type Binding struct { + Handler Handler + EventID UnsafeEventID + CallerID CallerID + BindID BindID + + busResetCount int64 + + // Bound is closed once the binding has been applied. Wait on this condition carefully; bindings + // will not take effect while an event is being triggered (e.g. in a event callback's returning thread) + Bound <-chan struct{} } -// GlobalBind binds on the bus to the cid 0, a non entity. -func (eb *Bus) GlobalBind(name string, fn Bindable) { - eb.Bind(name, 0, fn) +// Unbind unbinds the callback associated with this binding from it's own event handler. If this binding +// does not belong to its handler or has already been unbound, this will do nothing. +func (b Binding) Unbind() chan struct{} { + return b.Handler.Unbind(b) } -// Empty is a helper to convert a func() into a Bindable function signature. -func Empty(f func()) Bindable { - return func(CID, interface{}) int { - f() - return 0 +// A BindID is a unique identifier for a binding within a bus. +type BindID int64 + +// UnsafeBind registers a callback function to be called whenever the provided event is triggered +// against this bus. The binding is concurrently bound, and therefore may not be immediately +// available to be triggered. When Reset is called on a Bus, all prior bindings are unbound. This +// call is 'unsafe' because UnsafeBindables use bare interface{} types. +func (bus *Bus) UnsafeBind(eventID UnsafeEventID, callerID CallerID, fn UnsafeBindable) Binding { + expectedResetCount := bus.resetCount + bindID := BindID(atomic.AddInt64(bus.nextBindID, 1)) + ch := make(chan struct{}) + go func() { + defer close(ch) + bus.mutex.Lock() + defer bus.mutex.Unlock() + if bus.resetCount != expectedResetCount { + // The event bus has reset while we we were waiting to bind this + return + } + bl := bus.getBindableList(eventID, callerID) + bl[bindID] = fn + }() + return Binding{ + Handler: bus, + EventID: eventID, + CallerID: callerID, + BindID: bindID, + Bound: ch, + busResetCount: bus.resetCount, } } -// WaitForEvent will return a single payload from the given event. This -// makes an internal binding, but that binding will clean itself up -// regardless of how this is used. This should be used in a select clause -// to ensure the signal is captured, if the signal comes and the output -// channel is not being waited on, the channel will be closed. -func (eb *Bus) WaitForEvent(name string) <-chan interface{} { - ch := make(chan interface{}) +// PersistentBind acts like UnsafeBind, but cause Bind to be called with these inputs after a Bus is Reset, i.e. +// persisting the binding through bus resets. Unbinding this will not stop it from being rebound on the next +// Bus Reset-- ClearPersistentBindings will. If called concurrently during a bus Reset, the request may not be +// bound until the next bus Reset. +func (bus *Bus) PersistentBind(eventID UnsafeEventID, callerID CallerID, fn UnsafeBindable) Binding { + binding := bus.UnsafeBind(eventID, callerID, fn) go func() { - eb.GlobalBind(name, func(c CID, i interface{}) int { - select { - case ch <- i: - default: - } - close(ch) - return UnbindSingle + bus.mutex.Lock() + bus.persistentBindings = append(bus.persistentBindings, persistentBinding{ + eventID: eventID, + callerID: callerID, + fn: fn, }) + bus.mutex.Unlock() + }() + return binding +} + +// Unbind unregisters a binding from a bus concurrently. Once complete, triggers that would +// have previously caused the Bindable callback to execute will no longer do so. +func (bus *Bus) Unbind(loc Binding) chan struct{} { + ch := make(chan struct{}) + go func() { + bus.mutex.Lock() + defer bus.mutex.Unlock() + if bus.resetCount != loc.busResetCount { + // This binding is not valid for this bus (in this state) + return + } + l := bus.getBindableList(loc.EventID, loc.CallerID) + delete(l, loc.BindID) + close(ch) + }() + return ch +} + +// A Bindable is a strongly typed callback function to be executed on Trigger. It must be paired +// with an event registered via RegisterEvent. +type Bindable[C any, Payload any] func(C, Payload) Response + +// Bind will cause the function fn to be called whenever the event ev is triggered on the given event handler. The function +// will be called with the provided caller as its first argument, and will also be called when the provided event is specifically +// triggered on the caller's ID. +func Bind[C Caller, Payload any](h Handler, ev EventID[Payload], caller C, fn Bindable[C, Payload]) Binding { + return h.UnsafeBind(ev.UnsafeEventID, caller.CID(), func(cid CallerID, h Handler, payload interface{}) Response { + typedPayload := payload.(Payload) + ent := h.GetCallerMap().GetEntity(cid) + typedEntity := ent.(C) + return fn(typedEntity, typedPayload) + }) +} + +// A GlobalBindable is a bindable that is not bound to a specific caller. +type GlobalBindable[Payload any] func(Payload) Response + +// GlobalBind will cause the function fn to be called whenever the event ev is triggered on the given event handler. +func GlobalBind[Payload any](h Handler, ev EventID[Payload], fn GlobalBindable[Payload]) Binding { + return h.UnsafeBind(ev.UnsafeEventID, Global, func(cid CallerID, h Handler, payload interface{}) Response { + typedPayload := payload.(Payload) + return fn(typedPayload) + }) +} + +// UnsafeBindable defines the underlying signature of all bindings. +type UnsafeBindable func(CallerID, Handler, interface{}) Response + +// UnbindAllFrom unbinds all bindings currently bound to the provided caller via ID. +func (bus *Bus) UnbindAllFrom(c CallerID) chan struct{} { + ch := make(chan struct{}) + go func() { + bus.mutex.Lock() + for _, callerMap := range bus.bindingMap { + delete(callerMap, c) + } + bus.mutex.Unlock() + close(ch) }() return ch } diff --git a/event/bind_test.go b/event/bind_test.go new file mode 100644 index 00000000..4fe1d942 --- /dev/null +++ b/event/bind_test.go @@ -0,0 +1,119 @@ +package event_test + +import ( + "sync/atomic" + "testing" + + "github.com/oakmound/oak/v3/event" +) + +func TestBus_UnsafeBind(t *testing.T) { + t.Run("ConcurrentReset", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + + var calls int32 + for i := 0; i < 1000; i++ { + b.UnsafeBind(1, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + atomic.AddInt32(&calls, 1) + return 0 + }) + b.Reset() + // No matter what happens with thread scheduling above, this trigger should never increment calls + <-b.Trigger(1, nil) + } + if calls != 0 { + t.Fatal("a pre-reset binding was triggered after a bus reset") + } + }) +} + +func TestBus_Unbind(t *testing.T) { + t.Run("ConcurrentReset", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + + var goodCalls int32 + for i := 0; i < 1000; i++ { + b1 := b.UnsafeBind(1, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + return 0 + }) + b.Unbind(b1) + b.Reset() + // b1 and b2 will share a bindID + b2 := b.UnsafeBind(1, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + atomic.AddInt32(&goodCalls, 1) + return 0 + }) + <-b2.Bound + <-b.Trigger(1, nil) + <-b2.Unbind() + } + if goodCalls != 1000 { + t.Fatal("a pre-reset unbind unbound a post-reset binding", goodCalls) + } + }) +} + +func TestBind(t *testing.T) { + t.Run("SuperficialCoverage", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + var cid event.CallerID + id := b.GetCallerMap().Register(cid) + var calls int32 + b1 := event.Bind(b, event.Enter, id, func(event.CallerID, event.EnterPayload) event.Response { + atomic.AddInt32(&calls, 1) + return 0 + }) + <-b1.Bound + <-event.TriggerOn(b, event.Enter, event.EnterPayload{}) + if calls != 1 { + t.Fatal(expectedError("calls", 1, calls)) + } + }) +} + +func TestGlobalBind(t *testing.T) { + t.Run("SuperficialCoverage", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + var calls int32 + b1 := event.GlobalBind(b, event.Enter, func(event.EnterPayload) event.Response { + atomic.AddInt32(&calls, 1) + return 0 + }) + <-b1.Bound + <-event.TriggerOn(b, event.Enter, event.EnterPayload{}) + if calls != 1 { + t.Fatal(expectedError("calls", 1, calls)) + } + }) +} + +func TestBus_UnbindAllFrom(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + var cid event.CallerID + id := b.GetCallerMap().Register(cid) + var calls int32 + for i := 0; i < 5; i++ { + b1 := event.Bind(b, event.Enter, id, func(event.CallerID, event.EnterPayload) event.Response { + atomic.AddInt32(&calls, 1) + return 0 + }) + <-b1.Bound + } + id2 := b.GetCallerMap().Register(cid) + b1 := event.Bind(b, event.Enter, id2, func(event.CallerID, event.EnterPayload) event.Response { + atomic.AddInt32(&calls, 1) + return 0 + }) + <-b1.Bound + <-event.TriggerOn(b, event.Enter, event.EnterPayload{}) + if calls != 6 { + t.Fatal(expectedError("calls", 1, calls)) + } + <-b.UnbindAllFrom(id) + <-event.TriggerOn(b, event.Enter, event.EnterPayload{}) + if calls != 7 { + t.Fatal(expectedError("calls", 1, calls)) + } + }) +} diff --git a/event/bindingSet.go b/event/bindingSet.go deleted file mode 100644 index 23dc6a5c..00000000 --- a/event/bindingSet.go +++ /dev/null @@ -1,42 +0,0 @@ -package event - -// A Mapping stores a slice of event names and bindings -type Mapping struct { - eventNames []string - binds []Bindable -} - -// A BindingSet stores sets of event mappings bound to string names. -// The use case for a BindingSet is for a character that can exist in multiple states, -// so that they can swiftly switch between the event bindings that define those -// states. -type BindingSet map[string]Mapping - -// Set makes a new EventMapping for BindingSet -func (b BindingSet) Set(setName string, mappingSets ...map[string]Bindable) BindingSet { - - numMappings := 0 - for _, m := range mappingSets { - numMappings += len(m) - - } - bindings := make([]Bindable, numMappings) - events := make([]string, numMappings) - i := 0 - for _, m := range mappingSets { - for k, v := range m { - bindings[i] = v - events[i] = k - i++ - } - } - - b[setName] = Mapping{eventNames: events, binds: bindings} - return b -} - -// RebindMapping resets the entity controlling this cid to only have the bindings -// in the passed in event mapping -func (cid CID) RebindMapping(mapping Mapping) { - cid.UnbindAllAndRebind(mapping.binds, mapping.eventNames) -} diff --git a/event/bus.go b/event/bus.go index c95f1655..e47f7502 100644 --- a/event/bus.go +++ b/event/bus.go @@ -1,51 +1,33 @@ package event import ( - "reflect" "sync" "time" ) -// Bindable is a way of saying "Any function -// that takes a generic struct of data -// and returns an error can be bound". -type Bindable func(CID, interface{}) int +// A Bus stores bindables to be triggered by events. +type Bus struct { + // nextBindID is an atomically incrementing value to track bindings within this structure + nextBindID *int64 -// BindableList just stores other relevant data -// that a list of bindables needs to -// operate efficiently -type bindableList struct { - sl []Bindable - // We keep track of where the next nil - // element in our list is, so we - // can let bindings know where they - // are by index, (we don't shift to - // fill empty spaces) and so we can - // fill that slot next when a - // new binding comes in. - nextEmpty int -} + // resetCount increments every time the bus is reset. bindings and unbindings make sure that + // they are called on a bus with an unchanged reset count, and become NOPs if performed on + // a bus with a different reset count to ensure they do not interfere with a bus using different + // bind IDs. + resetCount int64 + bindingMap map[UnsafeEventID]map[CallerID]bindableList + persistentBindings []persistentBinding -// A Bus stores bindables to be triggered by events -type Bus struct { - bindingMap map[string]map[CID]*bindableList - doneCh chan struct{} - framesElapsed int - Ticker *time.Ticker - binds []UnbindOption - partUnbinds []Event - fullUnbinds []UnbindOption - unbinds []binding - unbindAllAndRebinds []UnbindAllOption - persistentBinds []UnbindOption - framerate int - refreshRate time.Duration - callerMap *CallerMap + callerMap *CallerMap - mutex sync.RWMutex - pendingMutex sync.Mutex + mutex sync.RWMutex +} - init sync.Once +// a persistentBinding is rebound every time the bus is reset. +type persistentBinding struct { + eventID UnsafeEventID + callerID CallerID + fn UnsafeBindable } // NewBus returns an empty event bus with an assigned caller map. If nil @@ -55,125 +37,78 @@ func NewBus(callerMap *CallerMap) *Bus { callerMap = DefaultCallerMap } return &Bus{ - bindingMap: make(map[string]map[CID]*bindableList), - doneCh: make(chan struct{}), + nextBindID: new(int64), + bindingMap: make(map[UnsafeEventID]map[CallerID]bindableList), callerMap: callerMap, } } // SetCallerMap updates a bus to use a specific set of callers. -func (b *Bus) SetCallerMap(cm *CallerMap) { - b.callerMap = cm +func (bus *Bus) SetCallerMap(cm *CallerMap) { + bus.callerMap = cm } -// An Event is an event name and an associated caller id -type Event struct { - Name string - CallerID CID +// GetCallerMap returns this bus's caller map. +func (b *Bus) GetCallerMap() *CallerMap { + return b.callerMap } -// UnbindOption stores information necessary -// to unbind a bindable -type UnbindOption struct { - Event - Fn Bindable -} - -// binding stores data necessary -// to trace back to a bindable function -// and remove it from a Bus. -type binding struct { - Event - index int -} - -// Reset empties out all transient portions of the bus. It will not stop -// an ongoing loop. -func (eb *Bus) Reset() { +// ClearPersistentBindings removes all persistent bindings. It will not unbind them +// from the bus, but they will not be bound following the next bus reset. +func (eb *Bus) ClearPersistentBindings() { eb.mutex.Lock() - eb.pendingMutex.Lock() - eb.bindingMap = make(map[string]map[CID]*bindableList) - eb.binds = []UnbindOption{} - eb.partUnbinds = []Event{} - eb.fullUnbinds = []UnbindOption{} - eb.unbinds = []binding{} - eb.unbindAllAndRebinds = []UnbindAllOption{} - for _, bindSet := range eb.persistentBinds { - list := eb.getBindableList(bindSet.Event) - list.storeBindable(bindSet.Fn) - } - eb.pendingMutex.Unlock() + eb.persistentBindings = eb.persistentBindings[:0] eb.mutex.Unlock() } -// UnbindAllOption stores information needed to unbind and rebind -type UnbindAllOption struct { - ub Event - bs []Event - bnds []Bindable -} - -// Store a bindable into a BindableList. -func (bl *bindableList) storeBindable(fn Bindable) int { - - i := bl.nextEmpty - if len(bl.sl) == i { - bl.sl = append(bl.sl, fn) - } else { - bl.sl[i] = fn +// Reset unbinds all present, non-persistent bindings on the bus. It will block until +// persistent bindings are in place. +func (bus *Bus) Reset() { + bus.mutex.Lock() + bus.resetCount++ + bus.bindingMap = make(map[UnsafeEventID]map[CallerID]bindableList) + repersist := make([]Binding, len(bus.persistentBindings)) + for i, pb := range bus.persistentBindings { + repersist[i] = bus.UnsafeBind(pb.eventID, pb.callerID, pb.fn) } - - // Find the next empty space - for len(bl.sl) != bl.nextEmpty && bl.sl[bl.nextEmpty] != nil { - bl.nextEmpty++ + bus.mutex.Unlock() + for _, bnd := range repersist { + <-bnd.Bound } - - return i } -// This scans linearly for the bindable -// This will cause an issue with closures! -// You can't unbind closures that don't have the -// same variable reference because this compares -// pointers! -// -// At all costs, this should be avoided, and -// returning UnbindSingle from the function -// itself is much safer! -func (bl *bindableList) removeBindable(fn Bindable) { - v := reflect.ValueOf(fn) - for i := 0; i < len(bl.sl); i++ { - v2 := reflect.ValueOf(bl.sl[i]) - if v2 == v { - bl.removeIndex(i) - return +// EnterLoop triggers Enter events at the specified rate until the returned cancel is called. +func EnterLoop(bus Handler, frameDelay time.Duration) (cancel func()) { + ch := make(chan struct{}) + go func() { + ticker := time.NewTicker(frameDelay) + frameDelayF64 := float64(frameDelay) + lastTick := time.Now() + framesElapsed := 0 + for { + select { + case now := <-ticker.C: + deltaTime := now.Sub(lastTick) + lastTick = now + <-bus.Trigger(Enter.UnsafeEventID, EnterPayload{ + FramesElapsed: framesElapsed, + SinceLastFrame: deltaTime, + TickPercent: float64(deltaTime) / frameDelayF64, + }) + framesElapsed++ + case <-ch: + ticker.Stop() + return + } } + }() + return func() { + // Q: why send here as well as close + // A: to ensure that no more ticks are sent, the above goroutine has to + // acknowledge that it should stop and return-- just closing would + // enable code following this cancel function to assume no enters were + // being triggered when they still are. + ch <- struct{}{} + close(ch) } } - -// Remove a bindable from a BindableList -func (bl *bindableList) removeBinding(b binding) { - bl.removeIndex(b.index) -} - -func (bl *bindableList) removeIndex(i int) { - if len(bl.sl) <= i { - return - } - - bl.sl[i] = nil - - if i < bl.nextEmpty { - bl.nextEmpty = i - } -} - -func (eb *Bus) getBindableList(opt Event) *bindableList { - if m := eb.bindingMap[opt.Name]; m == nil { - eb.bindingMap[opt.Name] = make(map[CID]*bindableList) - } - if m := eb.bindingMap[opt.Name][opt.CallerID]; m == nil { - eb.bindingMap[opt.Name][opt.CallerID] = new(bindableList) - } - return eb.bindingMap[opt.Name][opt.CallerID] -} diff --git a/event/bus_test.go b/event/bus_test.go index bde6a6d9..8b227ddb 100644 --- a/event/bus_test.go +++ b/event/bus_test.go @@ -1,52 +1,99 @@ -package event +package event_test import ( - "fmt" + "math/rand" + "sync/atomic" "testing" "time" + + "github.com/oakmound/oak/v3/event" ) -func TestBusStop(t *testing.T) { - b := NewBus(nil) - b.Ticker = time.NewTicker(10000 * time.Second) - phase := 0 - wait := make(chan struct{}) - var topErr error - go func() { - if err := b.Stop(); err != nil { - topErr = fmt.Errorf("stop errored: %v", err) - } - if phase != 1 { - topErr = fmt.Errorf("expected phase %v, got %v", 1, phase) - } - wait <- struct{}{} - }() - phase = 1 +func TestNewBus(t *testing.T) { + t.Run("DefaultCallerMap", func(t *testing.T) { + b := event.NewBus(nil) + if b.GetCallerMap() != event.DefaultCallerMap { + t.Fatal("nil caller map not turned into default caller map") + } + }) + t.Run("Basic", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + if b == nil { + t.Fatal("NewBus created nil bus") + } + }) +} + +func TestBus_SetCallerMap(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + cm1 := event.NewCallerMap() + b := event.NewBus(cm1) + c1 := event.CallerID(rand.Intn(10000)) + b.GetCallerMap().Register(c1) + cm2 := event.NewCallerMap() + b.SetCallerMap(cm2) + if b.GetCallerMap().HasEntity(c1) { + t.Fatal("event had old entity after changed caller map") + } + }) +} - <-b.doneCh - <-wait - if topErr != nil { - t.Fatal(topErr) - } +func TestBus_ClearPersistentBindings(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + var impersistentCalls int32 + var persistentCalls int32 + b1 := b.UnsafeBind(1, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + atomic.AddInt32(&impersistentCalls, 1) + return 0 + }) + b2 := b.PersistentBind(1, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + atomic.AddInt32(&persistentCalls, 1) + return 0 + }) + <-b1.Bound + <-b2.Bound + <-b.Trigger(1, nil) + if impersistentCalls != 1 { + t.Fatal(expectedError("impersistent calls", 1, impersistentCalls)) + } + if persistentCalls != 1 { + t.Fatal(expectedError("persistent calls", 1, persistentCalls)) + } + b.Reset() + <-b.Trigger(1, nil) + if impersistentCalls != 1 { + t.Fatal(expectedError("impersistent calls", 1, impersistentCalls)) + } + if persistentCalls != 2 { + t.Fatal(expectedError("persistent calls", 2, persistentCalls)) + } + b.ClearPersistentBindings() + b.Reset() + <-b.Trigger(1, nil) + if impersistentCalls != 1 { + t.Fatal(expectedError("impersistent calls", 1, impersistentCalls)) + } + if persistentCalls != 2 { + t.Fatal(expectedError("persistent calls", 2, persistentCalls)) + } + }) } -func TestBusPersistentBind(t *testing.T) { - t.Parallel() - b := NewBus(nil) - ev := "eventName" - calls := 0 - b.PersistentBind(ev, 0, func(c CID, i interface{}) int { - calls++ - return 0 +func TestBus_EnterLoop(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + var calls int32 + b1 := b.UnsafeBind(event.Enter.UnsafeEventID, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + atomic.AddInt32(&calls, 1) + return 0 + }) + <-b1.Bound + cancel := event.EnterLoop(b, 50*time.Millisecond) + time.Sleep(1 * time.Second) + cancel() + if calls != 20 { + t.Fatal(expectedError("calls", 20, calls)) + } }) - b.Flush() - <-b.TriggerBack(ev, nil) - if calls != 1 { - t.Fatalf("expected binding to be called once, was called %d time(s)", calls) - } - b.Reset() - <-b.TriggerBack(ev, nil) - if calls != 2 { - t.Fatalf("expected binding to be called twice, was called %d time(s)", calls) - } } diff --git a/event/caller.go b/event/caller.go index b7515c40..880fd1a7 100644 --- a/event/caller.go +++ b/event/caller.go @@ -1,11 +1,94 @@ package event -// A Caller can bind, unbind and trigger events. +import ( + "sync" +) + +// A CallerID is a caller ID that Callers use to bind themselves to receive callback +// signals when given events are triggered +type CallerID int64 + +func (c CallerID) CID() CallerID { + return c +} + +// Global is the CallerID associated with global bindings. A caller must not be assigned +// this ID. Global may be used to manually create bindings scoped to no callers, but the GlobalBind function +// should be preferred when possible for type safety. +const Global CallerID = 0 + type Caller interface { - Trigger(string, interface{}) - Bind(string, Bindable) - UnbindAll() - UnbindAllAndRebind([]Bindable, []string) - E() interface{} - Parse(Entity) CID + CID() CallerID +} + +// A CallerMap tracks CallerID mappings to Entities. +// This is an alternative to passing in the entity via closure scoping, +// and allows for more general bindings as simple top level functions. +type CallerMap struct { + highestID CallerID + callersLock sync.RWMutex + callers map[CallerID]Caller +} + +// NewCallerMap creates a caller map. A CallerMap +// is not valid for use if not created via this function. +func NewCallerMap() *CallerMap { + return &CallerMap{ + callers: map[CallerID]Caller{}, + } +} + +// NextID finds the next available caller id +// and returns it, after adding the given entity to +// the caller map. +func (cm *CallerMap) Register(e Caller) CallerID { + cm.callersLock.Lock() + defer cm.callersLock.Unlock() + // Q: Why not use atomic? + // A: We're in a mutex and therefore it is not needed. + // A2: We need the mutex to safely assign to the map. + // A3: We cannot atomically increment outside of the map, consider: + // - GR1 calls Clear, waits on Lock + // - GR2 calls Register, gets id 100, waits on lock + // - GR1 claims lock, resets highestID to 0, exits + // - GR2 claims lock, inserts id 100 in the map + // - ... later, register silently overwrites entity 100, its + // bindings will now panic on a bad type assertion + // + // Increment before assigning to preserve Global == caller 0 + cm.highestID++ + cm.callers[cm.highestID] = e + return cm.highestID +} + +// Get returns the entity corresponding to the given ID within +// the caller map. If no entity is found, it returns nil. +func (cm *CallerMap) GetEntity(id CallerID) Caller { + cm.callersLock.RLock() + defer cm.callersLock.RUnlock() + return cm.callers[id] +} + +// Has returns whether the given caller id is an initialized entity +// within the caller map. +func (cm *CallerMap) HasEntity(id CallerID) bool { + cm.callersLock.RLock() + defer cm.callersLock.RUnlock() + _, ok := cm.callers[id] + return ok +} + +// Remove removes an entity from the caller map. +func (cm *CallerMap) RemoveEntity(id CallerID) { + cm.callersLock.Lock() + delete(cm.callers, id) + cm.callersLock.Unlock() +} + +// Clear clears the caller map to forget all registered callers. +func (cm *CallerMap) Clear() { + cm.callersLock.Lock() + cm.highestID = 0 + cm.callers = map[CallerID]Caller{} + cm.callersLock.Unlock() } diff --git a/event/callerMap.go b/event/callerMap.go deleted file mode 100644 index 49512f7a..00000000 --- a/event/callerMap.go +++ /dev/null @@ -1,104 +0,0 @@ -package event - -import ( - "sync" - "sync/atomic" -) - -// A CallerMap tracks CID mappings to Entities. Its intended use is -// to be a source of truth within event bindings for what entity the -// binding is triggering on: -// var cm *event.CallerMap -// func(cid event.CID, payload interface{}) int { -// ent := cm.GetEntity(cid) -// f, ok := ent.(*Foo) -// if !ok { -// // bound to an unexpected entity type! -// return event.UnbindSingle -// } -// // ... -// } -// This is an alternative to passing in the entity via closure scoping, -// and allows for more general bindings as simple top level functions. -type CallerMap struct { - highestID *int64 - callersLock sync.RWMutex - callers map[CID]Entity -} - -// NewCallerMap creates a caller map. A CallerMap -// is not valid for use if not created via this function. -func NewCallerMap() *CallerMap { - return &CallerMap{ - highestID: new(int64), - callers: map[CID]Entity{}, - } -} - -// DefaultCallerMap is the caller map used by all event package caller -// functions. -var DefaultCallerMap = NewCallerMap() - -// NextID finds the next available caller id -// and returns it, after adding the given entity to -// the caller map. -func (cm *CallerMap) NextID(e Entity) CID { - nextID := atomic.AddInt64(cm.highestID, 1) - cm.callersLock.Lock() - cm.callers[CID(nextID)] = e - cm.callersLock.Unlock() - return CID(nextID) -} - -// GetEntity returns the entity corresponding to the given ID within -// the caller map. If no entity is found, it returns nil. -func (cm *CallerMap) GetEntity(id CID) Entity { - cm.callersLock.RLock() - defer cm.callersLock.RUnlock() - return cm.callers[id] -} - -// HasEntity returns whether the given caller id is an initialized entity -// within the caller map. -func (cm *CallerMap) HasEntity(id CID) bool { - cm.callersLock.RLock() - defer cm.callersLock.RUnlock() - _, ok := cm.callers[id] - return ok -} - -// DestroyEntity removes an entity from the caller map. -func (cm *CallerMap) DestroyEntity(id CID) { - cm.callersLock.Lock() - delete(cm.callers, id) - cm.callersLock.Unlock() -} - -// NextID finds the next available caller id -// and returns it, after adding the given entity to -// the default caller map. -func NextID(e Entity) CID { - return DefaultCallerMap.NextID(e) -} - -// GetEntity returns the entity corresponding to the given ID within -// the default caller map. If no entity is found, it returns nil. -func GetEntity(id CID) Entity { - return DefaultCallerMap.GetEntity(id) -} - -// HasEntity returns whether the given caller id is an initialized entity -// within the default caller map. -func HasEntity(id CID) bool { - return DefaultCallerMap.HasEntity(id) -} - -// DestroyEntity removes an entity from the default caller map. -func DestroyEntity(id CID) { - DefaultCallerMap.DestroyEntity(id) -} - -// ResetCallerMap resets the DefaultCallerMap to be empty. -func ResetCallerMap() { - *DefaultCallerMap = *NewCallerMap() -} diff --git a/event/caller_test.go b/event/caller_test.go new file mode 100644 index 00000000..10707660 --- /dev/null +++ b/event/caller_test.go @@ -0,0 +1,67 @@ +package event_test + +import ( + "math/rand" + "testing" + + "github.com/oakmound/oak/v3/event" +) + +func TestCallerID_CID(t *testing.T) { + t.Run("Identity", func(t *testing.T) { + c := event.CallerID(rand.Intn(100000)) + if c != c.CID() { + t.Fatalf("callerID did not match itself: was %v, got %v", c, c.CID()) + } + }) +} + +func TestNewCallerMap(t *testing.T) { + t.Run("NotNil", func(t *testing.T) { + m := event.NewCallerMap() + if m == nil { + t.Fatalf("created caller map was nil") + } + }) +} + +func TestCallerMap_Register(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + m := event.NewCallerMap() + c1 := event.CallerID(rand.Intn(10000)) + id := m.Register(c1) + c2 := m.GetEntity(id) + if c2 != c1 { + t.Fatalf("unable to retrieve registered caller") + } + if !m.HasEntity(id) { + t.Fatalf("caller map does not have registered caller") + } + }) + t.Run("Remove", func(t *testing.T) { + m := event.NewCallerMap() + c1 := event.CallerID(rand.Intn(10000)) + id := m.Register(c1) + m.RemoveEntity(id) + c3 := m.GetEntity(id) + if c3 != nil { + t.Fatalf("get entity had registered caller after remove") + } + if m.HasEntity(id) { + t.Fatalf("caller map has registered caller after remove") + } + }) + t.Run("Clear", func(t *testing.T) { + m := event.NewCallerMap() + c1 := event.CallerID(rand.Intn(10000)) + id := m.Register(c1) + m.Clear() + c3 := m.GetEntity(id) + if c3 != nil { + t.Fatalf("get entity had registered caller after clear") + } + if m.HasEntity(id) { + t.Fatalf("caller map has registered caller after clear") + } + }) +} diff --git a/event/cid.go b/event/cid.go deleted file mode 100644 index 14a2946b..00000000 --- a/event/cid.go +++ /dev/null @@ -1,21 +0,0 @@ -package event - -// A CID is a caller ID that entities use to trigger and bind functionality -type CID int - -// E is shorthand for GetEntity(int(cid)) -func (cid CID) E() interface{} { - return GetEntity(cid) -} - -// Parse returns the given cid, or the entity's cid -// if the given cid is 0. This way, multiple entities can be -// composed together by passing 0 down to lower tiered constructors, so that -// the topmost entity is stored once and bind functions will -// bind to the topmost entity. -func (cid CID) Parse(e Entity) CID { - if cid == 0 { - return e.Init() - } - return cid -} diff --git a/event/default.go b/event/default.go index c9c7a277..9f82cd56 100644 --- a/event/default.go +++ b/event/default.go @@ -1,124 +1,15 @@ package event -// As in collision and mouse, default.go lists functions that -// only operate on DefaultBus, a package global bus. +// DefaultBus is a global Bus. It uses the DefaultCallerMap internally. It should not be used unless your program is only +// using a single Bus. Preferably multi-bus programs would create their own buses and caller maps specific to each bus's +// use. +var DefaultBus *Bus -var ( - // DefaultBus is a bus that has additional operations for CIDs, and can - // be called via event.Call as opposed to bus.Call - DefaultBus = NewBus(DefaultCallerMap) -) - -// Trigger an event, but only for one ID, on the default bus -func (cid CID) Trigger(eventName string, data interface{}) { - go func(eventName string, data interface{}) { - DefaultBus.mutex.RLock() - if idMap, ok := DefaultBus.bindingMap[eventName]; ok { - if bs, ok := idMap[cid]; ok { - DefaultBus.triggerDefault(bs.sl, cid, eventName, data) - } - } - DefaultBus.mutex.RUnlock() - }(eventName, data) -} - -// TriggerBus triggers an event with some payload for this cid against the provided bus. -func (cid CID) TriggerBus(eventName string, data interface{}, bus Handler) chan struct{} { - return bus.TriggerCIDBack(cid, eventName, data) -} - -// Bind on a CID is shorthand for bus.Bind(name, cid, fn), on the default bus. -func (cid CID) Bind(name string, fn Bindable) { - DefaultBus.Bind(name, cid, fn) -} - -// UnbindAll removes all events with the given cid from the event bus -func (cid CID) UnbindAll() { - DefaultBus.UnbindAll(Event{ - Name: "", - CallerID: cid, - }) -} - -// UnbindAllAndRebind on a CID is equivalent to bus.UnbindAllAndRebind(..., cid) -func (cid CID) UnbindAllAndRebind(binds []Bindable, events []string) { - DefaultBus.UnbindAllAndRebind(Event{ - Name: "", - CallerID: cid, - }, binds, cid, events) -} - -// Trigger calls Trigger on the DefaultBus -func Trigger(eventName string, data interface{}) { - DefaultBus.Trigger(eventName, data) -} - -// TriggerBack calls TriggerBack on the DefaultBus -func TriggerBack(eventName string, data interface{}) chan struct{} { - return DefaultBus.TriggerBack(eventName, data) -} - -// GlobalBind calls GlobalBind on the DefaultBus -func GlobalBind(name string, fn Bindable) { - DefaultBus.GlobalBind(name, fn) -} - -// UnbindAll calls UnbindAll on the DefaultBus -func UnbindAll(opt Event) { - DefaultBus.UnbindAll(opt) -} - -// UnbindAllAndRebind calls UnbindAllAndRebind on the DefaultBus -func UnbindAllAndRebind(bo Event, binds []Bindable, cid CID, events []string) { - DefaultBus.UnbindAllAndRebind(bo, binds, cid, events) -} - -// UnbindBindable calls UnbindBindable on the DefaultBus -func UnbindBindable(opt UnbindOption) { - DefaultBus.UnbindBindable(opt) -} - -// Bind calls Bind on the DefaultBus -func Bind(name string, callerID CID, fn Bindable) { - DefaultBus.Bind(name, callerID, fn) -} - -// Flush calls Flush on the DefaultBus -func Flush() error { - return DefaultBus.Flush() -} - -// FramesElapsed calls FramesElapsed on the DefaultBus -func FramesElapsed() int { - return DefaultBus.FramesElapsed() -} - -// Reset calls Reset on the DefaultBus -func Reset() { - DefaultBus.Reset() -} - -// ResolveChanges calls ResolveChanges on the DefaultBus -func ResolveChanges() { - DefaultBus.ResolveChanges() -} - -// SetTick calls SetTick on the DefaultBus -func SetTick(framerate int) error { - return DefaultBus.SetTick(framerate) -} +// DefaultCallerMap is a global CallerMap. It should not be used unless your program is only using a single CallerMap, +// or in other words definitely only has one event bus running at a time. +var DefaultCallerMap *CallerMap -// Stop calls Stop on the DefaultBus -func Stop() error { - return DefaultBus.Stop() -} - -// Update calls Update on the DefaultBus -func Update() error { - return DefaultBus.Update() -} - -// UpdateLoop calls UpdateLoop on the DefaultBus -func UpdateLoop(framerate int, updateCh chan struct{}) error { - return DefaultBus.UpdateLoop(framerate, updateCh) +func init() { + DefaultCallerMap = NewCallerMap() + DefaultBus = NewBus(DefaultCallerMap) } diff --git a/event/doc.go b/event/doc.go index af897110..33069b21 100644 --- a/event/doc.go +++ b/event/doc.go @@ -1,2 +1,2 @@ -// Package event provides structures to propagate event occurences to subscribed system entities. +// Package event provides structures to propagate event occurrences to subscribed system entities. package event diff --git a/event/entity.go b/event/entity.go deleted file mode 100644 index 50345953..00000000 --- a/event/entity.go +++ /dev/null @@ -1,53 +0,0 @@ -package event - -// An Entity is an element which can be bound to, -// in that it has a CID. All Entities need to implement -// is an Init function which should call NextID(e) and -// return that id: -// func (f *Foo) Init() event.CID { -// f.CID = event.NextID(f) -// return f.CID -// } -// In a multi-window setup each window may have its own -// callerMap, in which case event.NextID should be replaced -// with a NextID call on the appropriate callerMap. -type Entity interface { - Init() CID -} - -// Q: Why does every entity need its own implementation -// of Init()? Why can't it get that method definition -// from struct embedding? -// -// A: Because the CallerMap will store whatever struct is -// passed in to NextID. In a naive implementation: -// type A struct { -// DefaultEntity -// } -// -// type DefaultEntity struct { -// event.CID -// } -// -// func (de *DefaultEntity) Init() event.CID { -// de.CID = event.NextID(de) -// return de.CID -// } -// -// func main() { -// ... -// a := &A{} -// cid := a.Init() -// ent := event.GetEntity(cid) -// _, ok := ent.(*A) -// // ok is false, ent is type *DefaultEntity -// } -// -// So to effectively do this you would need something like: -// func DefaultEntity(parent interface{}) *DefaultEntity {} -// ... where the structure would store and pass down the parent. -// This introduces empty interfaces, would make initialization -// more difficult, and would use slightly more memory. -// -// Feel free to use this idea in your own implementations, but -// this package will not provide this structure at this time. diff --git a/event/entity_test.go b/event/entity_test.go deleted file mode 100644 index 212a2aff..00000000 --- a/event/entity_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package event - -import ( - "testing" -) - -func TestGetEntityFails(t *testing.T) { - entity := GetEntity(100) - if entity != nil { - t.Fatalf("expected nil entity, got %v", entity) - } -} diff --git a/event/event_test.go b/event/event_test.go deleted file mode 100644 index 1f689c98..00000000 --- a/event/event_test.go +++ /dev/null @@ -1,249 +0,0 @@ -package event - -import ( - "testing" - "time" -) - -func sleep() { - // this is effectively "sync", or wait for the previous - // goroutine job to get its job done (Trigger - // use channels for 'done' signals because we don't want - // to enable users to wait on triggers that won't actually - // happen -because they are waiting- within a call that is - // holding a lock) - time.Sleep(200 * time.Millisecond) -} - -func TestBus(t *testing.T) { - triggers := 0 - go ResolveChanges() - GlobalBind("T", Empty(func() { - triggers++ - })) - sleep() - <-TriggerBack("T", nil) - if triggers != 1 { - t.Fatalf("first trigger did not happen") - } - Trigger("T", nil) - sleep() - if triggers != 2 { - t.Fatalf("second trigger did not happen") - } -} - -func TestUnbind(t *testing.T) { - triggers := 0 - go ResolveChanges() - GlobalBind("T", func(CID, interface{}) int { - triggers++ - return UnbindSingle - }) - sleep() - <-TriggerBack("T", nil) - if triggers != 1 { - t.Fatalf("first trigger did not happen") - } - sleep() - Trigger("T", nil) - sleep() - if triggers != 1 { - t.Fatalf("second trigger after unbind happened") - } - GlobalBind("T", func(CID, interface{}) int { - triggers++ - return 0 - }) - GlobalBind("T", func(CID, interface{}) int { - triggers++ - return UnbindEvent - }) - sleep() - Trigger("T", nil) - sleep() - if triggers != 3 { - t.Fatalf("global triggers did not happen") - } - - Trigger("T", nil) - sleep() - if triggers != 3 { - t.Fatalf("global triggers happened after unbind") - } - - GlobalBind("T", func(CID, interface{}) int { - triggers++ - return 0 - }) - sleep() - Trigger("T", nil) - sleep() - if triggers != 4 { - t.Fatalf("global triggers did not happen") - } - - Reset() - - Trigger("T", nil) - sleep() - if triggers != 4 { - t.Fatalf("global triggers did not unbind after reset") - } -} - -type ent struct{} - -func (e ent) Init() CID { - return NextID(e) -} - -func TestCID(t *testing.T) { - triggers := 0 - go ResolveChanges() - cid := CID(0).Parse(ent{}) - cid.Bind("T", func(CID, interface{}) int { - triggers++ - return 0 - }) - sleep() - cid.Trigger("T", nil) - sleep() - if triggers != 1 { - t.Fatalf("first trigger did not happen") - } - - // UnbindAllAndRebind - cid.UnbindAllAndRebind([]Bindable{ - func(CID, interface{}) int { - triggers-- - return 0 - }, - }, []string{ - "T", - }) - - sleep() - cid.Trigger("T", nil) - sleep() - if triggers != 0 { - t.Fatalf("second trigger did not happen") - } - - // UnbindAll - cid.UnbindAll() - - sleep() - cid.Trigger("T", nil) - sleep() - if triggers != 0 { - t.Fatalf("second trigger did not unbind") - } - - cid.Bind("T", func(CID, interface{}) int { - panic("Should not have been triggered") - }) - - // ResetEntities, etc - ResetCallerMap() - - cid.Trigger("T", nil) - sleep() -} - -func TestEntity(t *testing.T) { - go ResolveChanges() - e := ent{} - cid := e.Init() - cid2 := cid.Parse(e) - if cid != cid2 { - t.Fatalf("expected id %v got %v", cid, cid2) - } - if _, ok := cid.E().(ent); !ok { - t.Fatalf("cid entity was not present") - } - DestroyEntity(cid) - if cid.E() != nil { - t.Fatalf("cid entity was not deleted") - } -} - -var ( - ubTriggers int -) - -func TestUnbindBindable(t *testing.T) { - go ResolveChanges() - GlobalBind("T", tBinding) - sleep() - Trigger("T", nil) - sleep() - if ubTriggers != 1 { - t.Fatalf("first trigger did not happen") - } - // Fix this syntax - UnbindBindable( - UnbindOption{ - Event: Event{ - Name: "T", - CallerID: 0, - }, - Fn: tBinding, - }, - ) - sleep() - Trigger("T", nil) - sleep() - if ubTriggers != 1 { - t.Fatalf("unbind call did not unbind trigger") - } -} - -func tBinding(CID, interface{}) int { - ubTriggers++ - return 0 -} - -func TestBindableList(t *testing.T) { - bl := new(bindableList) - bl.sl = make([]Bindable, 10) - bl.removeIndex(11) - bl.sl[2] = tBinding - bl.removeBindable(tBinding) - // Assert nothing panicked -} - -func TestUnbindAllAndRebind(t *testing.T) { - go ResolveChanges() - UnbindAllAndRebind( - Event{ - Name: "T", - CallerID: 0, - }, []Bindable{}, 0, []string{}) -} - -func TestBindingSet(t *testing.T) { - triggers := 0 - bs := BindingSet{} - bs.Set("one", map[string]Bindable{ - "T": func(CID, interface{}) int { - triggers++ - return 0 - }, - "P": func(CID, interface{}) int { - triggers *= 2 - return 0 - }, - }) - e := ent{} - cid := e.Init() - cid.RebindMapping(bs["one"]) - sleep() - cid.Trigger("T", nil) - sleep() - cid.Trigger("P", nil) - sleep() - if triggers != 2 { - t.Fatalf("triggers did not happen") - } -} diff --git a/event/events.go b/event/events.go new file mode 100644 index 00000000..4a90f2a2 --- /dev/null +++ b/event/events.go @@ -0,0 +1,39 @@ +package event + +import ( + "sync/atomic" + "time" +) + +// An UnsafeEventID is a non-typed eventID. EventIDs are just these, with type information attached. +type UnsafeEventID int64 + +// A EventID represents an event associated with a given payload type. +type EventID[T any] struct { + UnsafeEventID +} + +var ( + nextEventID int64 +) + +// RegisterEvent returns a unique ID to associate an event with. EventIDs not created through RegisterEvent are +// not valid for use in type-safe bindings. +func RegisterEvent[T any]() EventID[T] { + id := atomic.AddInt64(&nextEventID, 1) + return EventID[T]{ + UnsafeEventID: UnsafeEventID(id), + } +} + +// EnterPayload is the payload sent down to Enter bindings +type EnterPayload struct { + FramesElapsed int + SinceLastFrame time.Duration + TickPercent float64 +} + +var ( + // Enter: the beginning of every logical frame. + Enter = RegisterEvent[EnterPayload]() +) diff --git a/event/handler.go b/event/handler.go index 6088763a..af59e3ea 100644 --- a/event/handler.go +++ b/event/handler.go @@ -1,12 +1,5 @@ package event -import ( - "math" - "time" - - "github.com/oakmound/oak/v3/timing" -) - var ( _ Handler = &Bus{} ) @@ -15,162 +8,12 @@ var ( // for use in oak internally, and thus the functions that need to be replaced // by alternative event handlers. type Handler interface { - WaitForEvent(name string) <-chan interface{} - // - UpdateLoop(framerate int, updateCh chan struct{}) error - FramesElapsed() int - SetTick(framerate int) error - Update() error - Flush() error - Stop() error Reset() - SetRefreshRate(time.Duration) - // - Trigger(event string, data interface{}) - TriggerBack(event string, data interface{}) chan struct{} - TriggerCIDBack(cid CID, eventName string, data interface{}) chan struct{} - // - Pause() - Resume() - // - Bind(string, CID, Bindable) - GlobalBind(string, Bindable) - UnbindAll(Event) - UnbindAllAndRebind(Event, []Bindable, CID, []string) - UnbindBindable(UnbindOption) -} - -// A CallerMapper has an internal caller map that can be set. -type CallerMapper interface { + TriggerForCaller(cid CallerID, event UnsafeEventID, data interface{}) chan struct{} + Trigger(event UnsafeEventID, data interface{}) chan struct{} + UnsafeBind(UnsafeEventID, CallerID, UnsafeBindable) Binding + Unbind(Binding) chan struct{} + UnbindAllFrom(CallerID) chan struct{} SetCallerMap(*CallerMap) -} - -// A PersistentBinder can persist bindings through bus resets -type PersistentBinder interface { - PersistentBind(string, CID, Bindable) - ClearPersistentBindings() -} - -// UpdateLoop is expected to internally call Update() -// or do something equivalent at the given frameRate, -// sending signals to the sceneCh after each Update(). -// Any flushing should be done as needed. This should -// not be called with `go`, if this requires goroutines -// it should create them itself. -// UpdateLoop is expected separately from Update() and -// Flush() because it will be more efficient for a Logical -// System to perform its own Updates outside of it’s exposed -// interface. -func (eb *Bus) UpdateLoop(framerate int, updateCh chan struct{}) error { - // The logical loop. - // In order, it waits on receiving a signal to begin a logical frame. - // It then runs any functions bound to when a frame begins. - // It then allows a scene to perform it's loop operation. - eb.framesElapsed = 0 - eb.framerate = framerate - frameDelay := timing.FPSToFrameDelay(framerate) - if eb.Ticker == nil { - eb.Ticker = time.NewTicker(frameDelay) - } - go eb.ResolveChanges() - go func() { - eb.Ticker.Reset(frameDelay) - frameDelayF64 := float64(frameDelay) - lastTick := time.Now() - for { - select { - case now := <-eb.Ticker.C: - deltaTime := now.Sub(lastTick) - lastTick = now - <-eb.TriggerBack(Enter, EnterPayload{ - FramesElapsed: eb.framesElapsed, - SinceLastFrame: deltaTime, - TickPercent: float64(deltaTime) / frameDelayF64, - }) - eb.framesElapsed++ - select { - case updateCh <- struct{}{}: - case <-eb.doneCh: - return - } - case <-eb.doneCh: - return - } - } - }() - return nil -} - -// EnterPayload is the payload sent down to Enter bindings -type EnterPayload struct { - FramesElapsed int - SinceLastFrame time.Duration - TickPercent float64 -} - -// Update updates all entities bound to this handler -func (eb *Bus) Update() error { - <-eb.TriggerBack(Enter, EnterPayload{ - FramesElapsed: eb.framesElapsed, - }) - return nil -} - -// Flush refreshes any changes to the Handler’s bindings. -func (eb *Bus) Flush() error { - if len(eb.unbindAllAndRebinds) > 0 { - eb.resolveUnbindAllAndRebinds() - } - // Specific unbinds - if len(eb.unbinds) > 0 { - eb.resolveUnbinds() - } - - // A full set of unbind settings - if len(eb.fullUnbinds) > 0 { - eb.resolveFullUnbinds() - } - - // A partial set of unbind settings - if len(eb.partUnbinds) > 0 { - eb.resolvePartialUnbinds() - } - - // Bindings - if len(eb.binds) > 0 { - eb.resolveBindings() - } - return nil -} - -// Stop ceases anything spawned by an ongoing UpdateLoop -func (eb *Bus) Stop() error { - if eb.Ticker != nil { - eb.Ticker.Stop() - } - eb.doneCh <- struct{}{} - return nil -} - -// Pause stops the event bus from running any further enter events -func (eb *Bus) Pause() { - eb.Ticker.Reset(math.MaxInt32 * time.Second) -} - -// Resume will resume emitting enter events -func (eb *Bus) Resume() { - eb.Ticker.Reset(timing.FPSToFrameDelay(eb.framerate)) -} - -// FramesElapsed returns how many frames have elapsed since UpdateLoop was last called. -func (eb *Bus) FramesElapsed() int { - return eb.framesElapsed -} - -// SetTick optionally updates the Logical System’s tick rate -// (while it is looping) to be frameRate. If this operation is not -// supported, it should return an error. -func (eb *Bus) SetTick(framerate int) error { - eb.Ticker.Reset(timing.FPSToFrameDelay(framerate)) - return nil + GetCallerMap() *CallerMap } diff --git a/event/handler_test.go b/event/handler_test.go deleted file mode 100644 index 9c5e0d1b..00000000 --- a/event/handler_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package event - -import ( - "testing" - "time" -) - -func TestHandler(t *testing.T) { - updateCh := make(chan struct{}) - if UpdateLoop(60, updateCh) != nil { - t.Fatalf("UpdateLoop failed") - } - triggers := 0 - Bind(Enter, 0, func(CID, interface{}) int { - triggers++ - return 0 - }) - sleep() - if triggers != 1 { - t.Fatalf("expected update loop to increment triggers") - } - <-updateCh - sleep() - if triggers != 2 { - t.Fatalf("expected update loop to increment triggers") - } - if FramesElapsed() != 2 { - t.Fatalf("expected 2 update frames to have elapsed") - } - if SetTick(1) != nil { - t.Fatalf("SetTick failed") - } - <-updateCh - if Stop() != nil { - t.Fatalf("Stop failed") - } - sleep() - sleep() - select { - case <-updateCh: - t.Fatal("Handler should be closed") - default: - } - expectedTriggers := triggers + 1 - if Update() != nil { - t.Fatalf("Update failed") - } - sleep() - - if triggers != expectedTriggers { - t.Fatalf("expected update to increment triggers") - } - if Flush() != nil { - t.Fatalf("Flush failed") - } - - Flush() - sleep() - if Update() != nil { - t.Fatalf("final Update failed") - } - sleep() - sleep() - Reset() -} - -func BenchmarkHandler(b *testing.B) { - triggers := 0 - entities := 10 - go DefaultBus.ResolveChanges() - for i := 0; i < entities; i++ { - DefaultBus.GlobalBind(Enter, func(CID, interface{}) int { - triggers++ - return 0 - }) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - <-DefaultBus.TriggerBack(Enter, DefaultBus.framesElapsed) - } -} - -func TestPauseAndResume(t *testing.T) { - b := NewBus(nil) - b.ResolveChanges() - triggerCt := 0 - b.Bind("EnterFrame", 0, func(CID, interface{}) int { - triggerCt++ - return 0 - }) - ch := make(chan struct{}, 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/internal.go b/event/internal.go new file mode 100644 index 00000000..ea692834 --- /dev/null +++ b/event/internal.go @@ -0,0 +1,48 @@ +package event + +import ( + "sync" +) + +type bindableList map[BindID]UnsafeBindable + +func (eb *Bus) getBindableList(eventID UnsafeEventID, callerID CallerID) bindableList { + if m := eb.bindingMap[eventID]; m == nil { + eb.bindingMap[eventID] = make(map[CallerID]bindableList) + bl := make(bindableList) + eb.bindingMap[eventID][callerID] = bl + return bl + } + bl := eb.bindingMap[eventID][callerID] + if bl == nil { + bl = make(bindableList) + eb.bindingMap[eventID][callerID] = bl + } + return bl +} + +func (bus *Bus) trigger(binds bindableList, eventID UnsafeEventID, callerID CallerID, data interface{}) { + wg := &sync.WaitGroup{} + wg.Add(len(binds)) + for bindID, bnd := range binds { + bindID := bindID + bnd := bnd + go func() { + if callerID == Global || bus.callerMap.HasEntity(callerID) { + response := bnd(callerID, bus, data) + switch response { + case ResponseUnbindThisBinding: + // Q: Why does this call bus.Unbind when it already has the event index to delete? + // A: This goroutine does not own a write lock on the bus, and should therefore + // not modify its contents. We do not have a simple way of promoting our read lock + // to a write lock. + bus.Unbind(Binding{EventID: eventID, CallerID: callerID, BindID: bindID}) + case ResponseUnbindThisCaller: + bus.UnbindAllFrom(callerID) + } + } + wg.Done() + }() + } + wg.Wait() +} diff --git a/event/resolve.go b/event/resolve.go deleted file mode 100644 index 847d6d82..00000000 --- a/event/resolve.go +++ /dev/null @@ -1,135 +0,0 @@ -package event - -import "time" - -// ResolveChanges 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. -// -// 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) ResolveChanges() { - eb.init.Do(func() { - go func() { - for { - time.Sleep(eb.refreshRate) - eb.Flush() - } - }() - }) -} - -// SetRefreshRate on the event bus detailing the time to wait per attempt to ResolveChanges. -func (eb *Bus) SetRefreshRate(refreshRate time.Duration) { - eb.refreshRate = refreshRate -} - -func (eb *Bus) resolveUnbindAllAndRebinds() { - eb.mutex.Lock() - eb.pendingMutex.Lock() - for _, ubaarb := range eb.unbindAllAndRebinds { - unbind := ubaarb.ub - orderedBindables := ubaarb.bnds - orderedBindOptions := ubaarb.bs - - var namekeys []string - // If we were given a name, - // we'll just iterate with that name. - if unbind.Name != "" { - namekeys = append(namekeys, unbind.Name) - // Otherwise, iterate through all events. - } else { - for k := range eb.bindingMap { - namekeys = append(namekeys, k) - } - } - - if unbind.CallerID != 0 { - for _, k := range namekeys { - delete(eb.bindingMap[k], unbind.CallerID) - } - } else { - for _, k := range namekeys { - delete(eb.bindingMap, k) - } - } - - // Bindings - for i := 0; i < len(orderedBindables); i++ { - fn := orderedBindables[i] - opt := orderedBindOptions[i] - list := eb.getBindableList(opt) - list.storeBindable(fn) - } - } - eb.unbindAllAndRebinds = []UnbindAllOption{} - eb.pendingMutex.Unlock() - eb.mutex.Unlock() -} - -func (eb *Bus) resolveUnbinds() { - eb.mutex.Lock() - eb.pendingMutex.Lock() - for _, bnd := range eb.unbinds { - eb.getBindableList(bnd.Event).removeBinding(bnd) - } - eb.unbinds = []binding{} - eb.pendingMutex.Unlock() - eb.mutex.Unlock() -} - -func (eb *Bus) resolveFullUnbinds() { - eb.mutex.Lock() - eb.pendingMutex.Lock() - for _, opt := range eb.fullUnbinds { - eb.getBindableList(opt.Event).removeBindable(opt.Fn) - } - eb.fullUnbinds = []UnbindOption{} - eb.pendingMutex.Unlock() - eb.mutex.Unlock() -} - -func (eb *Bus) resolvePartialUnbinds() { - eb.mutex.Lock() - eb.pendingMutex.Lock() - for _, opt := range eb.partUnbinds { - var namekeys []string - - // If we were given a name, - // we'll just iterate with that name. - if opt.Name != "" { - namekeys = append(namekeys, opt.Name) - - // Otherwise, iterate through all events. - } else { - for k := range eb.bindingMap { - namekeys = append(namekeys, k) - } - } - - if opt.CallerID != 0 { - for _, k := range namekeys { - delete(eb.bindingMap[k], opt.CallerID) - } - } else { - for _, k := range namekeys { - delete(eb.bindingMap, k) - } - } - } - eb.partUnbinds = []Event{} - eb.pendingMutex.Unlock() - eb.mutex.Unlock() -} - -func (eb *Bus) resolveBindings() { - eb.mutex.Lock() - eb.pendingMutex.Lock() - for _, bindSet := range eb.binds { - list := eb.getBindableList(bindSet.Event) - list.storeBindable(bindSet.Fn) - } - eb.binds = []UnbindOption{} - eb.pendingMutex.Unlock() - eb.mutex.Unlock() -} diff --git a/event/resolve_test.go b/event/resolve_test.go deleted file mode 100644 index 58cbc97c..00000000 --- a/event/resolve_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package event - -import ( - "testing" - "time" -) - -func TestResolveChangesWithRefreshRate(t *testing.T) { - b := NewBus(nil) - b.SetRefreshRate(6 * time.Second) - b.ResolveChanges() - failed := false - b.Bind("EnterFrame", 0, func(CID, interface{}) int { - failed = true - return 0 - }) - ch := make(chan struct{}, 1000) - b.UpdateLoop(60, ch) - time.Sleep(3 * time.Second) - if failed { - t.Fatal("binding was called before refresh rate should have added binding") - } -} diff --git a/event/response.go b/event/response.go index 336cd389..a4391b4a 100644 --- a/event/response.go +++ b/event/response.go @@ -1,20 +1,16 @@ package event -// Response types from bindables -// reponses are not their own type because func(event.CID, interface{}) int -// is easier to write than func(event.CID, interface{}) event.Response. This may -// yet change. +type Response uint8 + +// Response types for bindables const ( - // NoResponse or 0, is returned by events that + // ResponseNone or 0, is returned by events that // don't want the event bus to do anything with // the event after they have been evaluated. This // is the usual behavior. - NoResponse = iota - // UnbindEvent unbinds everything for a specific - // event name from an entity at the bindable's - // priority. - UnbindEvent - // UnbindSingle just unbinds the one binding that - // it is returned from - UnbindSingle + ResponseNone Response = iota + // ResponseUnbindThisBinding unbinds the one binding that returns it. + ResponseUnbindThisBinding + // ResponseUnbindThisCaller unbinds all of a caller's bindings when returned from any binding. + ResponseUnbindThisCaller ) diff --git a/event/response_test.go b/event/response_test.go new file mode 100644 index 00000000..7064e353 --- /dev/null +++ b/event/response_test.go @@ -0,0 +1,57 @@ +package event_test + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/oakmound/oak/v3/event" +) + +func TestBindingResponses(t *testing.T) { + t.Run("UnbindThisBinding", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + + var calls int32 + b1 := b.UnsafeBind(1, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + atomic.AddInt32(&calls, 1) + return event.ResponseUnbindThisBinding + }) + <-b1.Bound + <-b.Trigger(1, nil) + if calls != 1 { + t.Fatal(expectedError("calls", 1, calls)) + } + // we do not get a signal for when this unbinding is finished + time.Sleep(1 * time.Second) + <-b.Trigger(1, nil) + if calls != 1 { + t.Fatal(expectedError("calls", 1, calls)) + } + }) + t.Run("UNbindThisCaller", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + + var calls int32 + b1 := b.UnsafeBind(1, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + atomic.AddInt32(&calls, 1) + return event.ResponseUnbindThisCaller + }) + <-b1.Bound + b2 := b.UnsafeBind(1, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + atomic.AddInt32(&calls, 1) + return 0 + }) + <-b2.Bound + <-b.Trigger(1, nil) + if calls != 2 { + t.Fatal(expectedError("calls", 1, calls)) + } + // we do not get a signal for when this unbinding is finished + time.Sleep(1 * time.Second) + <-b.Trigger(1, nil) + if calls != 2 { + t.Fatal(expectedError("calls", 2, calls)) + } + }) +} diff --git a/event/strings.go b/event/strings.go deleted file mode 100644 index 0c4885fa..00000000 --- a/event/strings.go +++ /dev/null @@ -1,50 +0,0 @@ -package event - -// Oak uses the following built in events: -// -// - CollisionStart/Stop: when a PhaseCollision entity starts/stops touching some label. -// Payload: (collision.Label) the label the entity has started/stopped touching -// -// - MouseCollisionStart/Stop: as above, for mouse collision -// Payload: (*mouse.Event) -// -// - Mouse events: MousePress, MouseRelease, MouseScrollDown, MouseScrollUp, MouseDrag -// Payload: (*mouse.Event) details on the mouse event -// -// - KeyDown, KeyDown$a: when any key is pressed down, when key $a is pressed down. -// Payload: (key.Event) the key pressed -// -// - KeyUp, KeyUp$a: when any key is released, when key $a is released. -// Payload: (key.Event) the key released -// -// And the following: -const ( - // Enter : the beginning of every logical frame. - // Payload: (EnterPayload) details on the frame and time since last tick - Enter = "EnterFrame" - // AnimationEnd: Triggered on animations CIDs when they loop from the last to the first frame - // Payload: nil - AnimationEnd = "AnimationEnd" - // ViewportUpdate: Triggered when the position of of the viewport changes - // Payload: intgeom.Point2 - ViewportUpdate = "ViewportUpdate" - // OnStop: Triggered when the engine is stopped. - // Payload: nil - OnStop = "OnStop" - // FocusGain: Triggered when the window gains focus - // Payload: nil - FocusGain = "FocusGain" - // FocusLoss: Triggered when the window loses focus - // Payload: nil - FocusLoss = "FocusLoss" - // InputChange: triggered when the most recent input device changes (e.g. keyboard to joystick or vice versa) - // Payload: oak.InputType - InputChange = "InputChange" -) - -// -// Note all events built in to oak are CapitalizedCamelCase. Although our adding of new -// built in events is rare, we don't consider the addition of these events breaking -// changes for versioning. If a game has many events with generalized names, making -// them uncapitalizedCamelCase is perhaps the best approach to guarantee that builtin -// event names will never conflict with custom events. diff --git a/event/trigger.go b/event/trigger.go index b80184e3..c65c966a 100644 --- a/event/trigger.go +++ b/event/trigger.go @@ -1,99 +1,45 @@ package event -import ( - "sync" -) - -// TriggerBack is a version of Trigger which returns a channel that -// informs on when all bindables have been called and returned from -// the input event. It is dangerous to use this unless you have a -// very good idea how things will synchronize, as if a triggered -// bindable itself makes a TriggerBack call, this will cause the engine to freeze, -// as the function will never end because the first TriggerBack has control of -// the lock for the event bus, and the first TriggerBack won't give up that lock -// until the function ends. -// -// This inherently means that when you call Trigger, the event will almost -// almost never be immediately triggered but rather will be triggered sometime -// soon in the future. -// -// TriggerBack is right now used by the primary logic loop to dictate logical -// framerate, so EnterFrame events are called through TriggerBack. -func (eb *Bus) TriggerBack(eventName string, data interface{}) chan struct{} { +// TriggerForCaller acts like Trigger, but will only trigger for the given caller. +func (bus *Bus) TriggerForCaller(callerID CallerID, eventID UnsafeEventID, data interface{}) chan struct{} { + if callerID == Global { + return bus.Trigger(eventID, data) + } ch := make(chan struct{}) - go func(ch chan struct{}, eb *Bus, eventName string, data interface{}) { - eb.trigger(eventName, data) + go func() { + bus.mutex.RLock() + if idMap, ok := bus.bindingMap[eventID]; ok { + if bs, ok := idMap[callerID]; ok { + bus.trigger(bs, eventID, callerID, data) + } + } + bus.mutex.RUnlock() close(ch) - }(ch, eb, eventName, data) + }() return ch } // Trigger will scan through the event bus and call all bindables found attached // to the given event, with the passed in data. -func (eb *Bus) Trigger(eventName string, data interface{}) { - go func(eb *Bus, eventName string, data interface{}) { - eb.trigger(eventName, data) - }(eb, eventName, data) -} - -// TriggerCIDBack acts like trigger back, but triggers for a specific cid only. -func (eb *Bus) TriggerCIDBack(cid CID, eventName string, data interface{}) chan struct{} { +func (bus *Bus) Trigger(eventID UnsafeEventID, data interface{}) chan struct{} { ch := make(chan struct{}) go func() { - eb.mutex.RLock() - if idMap, ok := eb.bindingMap[eventName]; ok { - if bs, ok := idMap[cid]; ok { - eb.triggerDefault(bs.sl, cid, eventName, data) - } + bus.mutex.RLock() + for callerID, bs := range bus.bindingMap[eventID] { + bus.trigger(bs, eventID, callerID, data) } - eb.mutex.RUnlock() + bus.mutex.RUnlock() close(ch) }() return ch } -func (eb *Bus) trigger(eventName string, data interface{}) { - eb.mutex.RLock() - for id, bs := range eb.bindingMap[eventName] { - eb.triggerDefault(bs.sl, id, eventName, data) - } - eb.mutex.RUnlock() -} - -func (eb *Bus) triggerDefault(sl []Bindable, id CID, eventName string, data interface{}) { - prog := &sync.WaitGroup{} - prog.Add(len(sl)) - for i, bnd := range sl { - if bnd == nil { - prog.Done() - continue - } - go func(bnd Bindable, id CID, eventName string, data interface{}, prog *sync.WaitGroup, index int) { - eb.handleBindable(bnd, id, data, index, eventName) - prog.Done() - }(bnd, id, eventName, data, prog, i) - } - prog.Wait() +// TriggerOn calls Trigger with a strongly typed event. +func TriggerOn[T any](b Handler, ev EventID[T], data T) chan struct{} { + return b.Trigger(ev.UnsafeEventID, data) } -func (eb *Bus) handleBindable(bnd Bindable, id CID, data interface{}, index int, eventName string) { - if id == 0 || eb.callerMap.HasEntity(id) { - response := bnd(id, data) - switch response { - case UnbindEvent: - UnbindAll(Event{ - Name: eventName, - CallerID: id, - }) - case UnbindSingle: - bnd := binding{ - Event: Event{ - Name: eventName, - CallerID: id, - }, - index: index, - } - bnd.unbind(eb) - } - } +// TriggerForCallerOn calls TriggerForCaller with a strongly typed event. +func TriggerForCallerOn[T any](b Handler, cid CallerID, ev EventID[T], data T) chan struct{} { + return b.TriggerForCaller(cid, ev.UnsafeEventID, data) } diff --git a/event/trigger_test.go b/event/trigger_test.go new file mode 100644 index 00000000..2dae1215 --- /dev/null +++ b/event/trigger_test.go @@ -0,0 +1,254 @@ +package event_test + +import ( + "fmt" + "math/rand" + "os" + "testing" + "time" + + "github.com/oakmound/oak/v3/event" +) + +func TestMain(m *testing.M) { + rand.Seed(time.Now().UnixNano()) + os.Exit(m.Run()) +} + +func TestBus_TriggerForCaller(t *testing.T) { + t.Run("NoBinding", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + id := event.UnsafeEventID(rand.Intn(100000)) + ch := b.TriggerForCaller(0, id, nil) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for trigger to close channel") + case <-ch: + } + }) + t.Run("GlobalWithBinding", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + id := event.UnsafeEventID(rand.Intn(100000)) + errs := make(chan error) + binding := b.UnsafeBind(id, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + defer close(errs) + if ci != 0 { + errs <- expectedError("callerID", 0, ci) + } + if h != b { + errs <- expectedError("bus", b, h) + } + if i != nil { + errs <- expectedError("payload", nil, i) + } + return 0 + }) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for bind to close channel") + case <-binding.Bound: + } + ch := b.TriggerForCaller(0, id, nil) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for trigger to close channel") + case <-ch: + } + for err := range errs { + t.Error(err) + } + }) + t.Run("WithMissingCallerID", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + id := event.UnsafeEventID(rand.Intn(100000)) + callerID := event.CallerID(rand.Intn(100000)) + errs := make(chan error) + binding := b.UnsafeBind(id, callerID, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + errs <- fmt.Errorf("binding should not be triggered") + return 0 + }) + _ = binding + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for bind to close channel") + case <-binding.Bound: + } + ch := b.TriggerForCaller(callerID, id, nil) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for trigger to close channel") + case <-ch: + } + select { + case err := <-errs: + t.Error(err) + default: + } + }) + t.Run("WithValidCallerID", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + var cid event.CallerID + callerID := b.GetCallerMap().Register(cid) + id := event.UnsafeEventID(rand.Intn(100000)) + errs := make(chan error) + binding := b.UnsafeBind(id, callerID, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + defer close(errs) + if ci != callerID { + errs <- expectedError("callerID", callerID, ci) + } + if h != b { + errs <- expectedError("bus", b, h) + } + if i != nil { + errs <- expectedError("payload", nil, i) + } + return 0 + }) + _ = binding + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for bind to close channel") + case <-binding.Bound: + } + ch := b.TriggerForCaller(callerID, id, nil) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for trigger to close channel") + case <-ch: + } + for err := range errs { + t.Error(err) + } + }) +} + +func TestBus_Trigger(t *testing.T) { + t.Run("NoBinding", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + id := event.UnsafeEventID(rand.Intn(100000)) + ch := b.Trigger(id, nil) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for trigger to close channel") + case <-ch: + } + }) + t.Run("GlobalWithBinding", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + id := event.UnsafeEventID(rand.Intn(100000)) + errs := make(chan error) + binding := b.UnsafeBind(id, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + defer close(errs) + if ci != 0 { + errs <- expectedError("callerID", 0, ci) + } + if h != b { + errs <- expectedError("bus", b, h) + } + if i != nil { + errs <- expectedError("payload", nil, i) + } + return 0 + }) + _ = binding + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for bind to close channel") + case <-binding.Bound: + } + ch := b.Trigger(id, nil) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for trigger to close channel") + case <-ch: + } + for err := range errs { + t.Error(err) + } + }) + t.Run("WithMissingCallerID", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + id := event.UnsafeEventID(rand.Intn(100000)) + callerID := rand.Intn(100000) + errs := make(chan error) + binding := b.UnsafeBind(id, event.CallerID(callerID), func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + errs <- fmt.Errorf("binding should not be triggered") + return 0 + }) + _ = binding + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for bind to close channel") + case <-binding.Bound: + } + ch := b.Trigger(id, nil) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for trigger to close channel") + case <-ch: + } + select { + case err := <-errs: + t.Error(err) + default: + } + }) + t.Run("WithValidCallerID", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + var cid event.CallerID + callerID := b.GetCallerMap().Register(cid) + id := event.UnsafeEventID(rand.Intn(100000)) + errs := make(chan error) + binding := b.UnsafeBind(id, event.CallerID(callerID), func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + defer close(errs) + if ci != callerID { + errs <- expectedError("callerID", callerID, ci) + } + if h != b { + errs <- expectedError("bus", b, h) + } + if i != nil { + errs <- expectedError("payload", nil, i) + } + return 0 + }) + _ = binding + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for bind to close channel") + case <-binding.Bound: + } + ch := b.Trigger(id, nil) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for trigger to close channel") + case <-ch: + } + for err := range errs { + t.Error(err) + } + }) +} + +// TriggerOn and TriggerForCallerOn are simple wrappers of the tested methods above, so +// they are not tested thoroughly. + +func TestTriggerOn(t *testing.T) { + t.Run("SuperficialCoverage", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + eventID := event.RegisterEvent[struct{}]() + event.TriggerOn(b, eventID, struct{}{}) + }) +} + +func TestTriggerForCallerOn(t *testing.T) { + t.Run("SuperficialCoverage", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + eventID := event.RegisterEvent[struct{}]() + event.TriggerForCallerOn(b, 0, eventID, struct{}{}) + }) +} + +func expectedError(name string, expected, got interface{}) error { + return fmt.Errorf("expected %s to be %v, got %v", name, expected, got) +} diff --git a/event/unbind.go b/event/unbind.go deleted file mode 100644 index 636d465a..00000000 --- a/event/unbind.go +++ /dev/null @@ -1,50 +0,0 @@ -package event - -// Unbind on a binding is a rewriting of bus.Unbind(b) -func (b binding) unbind(eb *Bus) { - eb.unbind(b) -} - -func (eb *Bus) unbind(b binding) { - eb.pendingMutex.Lock() - eb.unbinds = append(eb.unbinds, b) - eb.pendingMutex.Unlock() -} - -// UnbindAllAndRebind is a way to reset the bindings on a CID efficiently, -// given a new set of equal length binding and event slices. This is equivalent -// to calling UnbindAll and then looping over Bind calls for the pairs of -// bindables and event names, but uses less mutex time. -func (eb *Bus) UnbindAllAndRebind(bo Event, binds []Bindable, cid CID, events []string) { - opts := make([]Event, len(events)) - for k, v := range events { - opts[k] = Event{ - Name: v, - CallerID: cid, - } - } - - eb.pendingMutex.Lock() - eb.unbindAllAndRebinds = append(eb.unbindAllAndRebinds, UnbindAllOption{ - ub: bo, - bs: opts, - bnds: binds, - }) - eb.pendingMutex.Unlock() -} - -// UnbindAll removes all events that match the given bindingOption from the -// default event bus -func (eb *Bus) UnbindAll(opt Event) { - eb.pendingMutex.Lock() - eb.partUnbinds = append(eb.partUnbinds, opt) - eb.pendingMutex.Unlock() -} - -// UnbindBindable is a manual way to unbind a function Bindable. Use of -// this with closures will result in undefined behavior. -func (eb *Bus) UnbindBindable(opt UnbindOption) { - eb.pendingMutex.Lock() - eb.fullUnbinds = append(eb.fullUnbinds, opt) - eb.pendingMutex.Unlock() -} diff --git a/examples/bezier/main.go b/examples/bezier/main.go index f7b3b18a..eca12490 100644 --- a/examples/bezier/main.go +++ b/examples/bezier/main.go @@ -53,23 +53,23 @@ func main() { return "" }}) - oak.AddScene("bezier", scene.Scene{Start: func(*scene.Context) { + oak.AddScene("bezier", scene.Scene{Start: func(ctx *scene.Context) { mouseFloats := []float64{} - event.GlobalBind(mouse.Press, func(_ event.CID, mouseEvent interface{}) int { - me := mouseEvent.(*mouse.Event) - // Left click to add a point to the curve - if me.Button == mouse.ButtonLeft { - mouseFloats = append(mouseFloats, float64(me.X()), float64(me.Y())) - renderCurve(mouseFloats) - // Perform any other click to reset the drawn curve - } else { - mouseFloats = []float64{} - if cmp != nil { - cmp.Undraw() + event.GlobalBind(ctx, + mouse.Press, func(me *mouse.Event) event.Response { + // Left click to add a point to the curve + if me.Button == mouse.ButtonLeft { + mouseFloats = append(mouseFloats, float64(me.X()), float64(me.Y())) + renderCurve(mouseFloats) + // Perform any other click to reset the drawn curve + } else { + mouseFloats = []float64{} + if cmp != nil { + cmp.Undraw() + } } - } - return 0 - }) + return 0 + }) }}) oak.Init("bezier", func(c oak.Config) (oak.Config, error) { c.EnableDebugConsole = true diff --git a/examples/click-propagation/main.go b/examples/click-propagation/main.go index c56e6fc0..7ada4e5a 100644 --- a/examples/click-propagation/main.go +++ b/examples/click-propagation/main.go @@ -26,7 +26,7 @@ func main() { for x := 20.0; x < 400; x += 20 { z++ y -= 20 - newHoverButton(x, y, 35, 35, color.RGBA{200, 200, 200, 200}, z) + newHoverButton(ctx, x, y, 35, 35, color.RGBA{200, 200, 200, 200}, z) } }, }) @@ -34,50 +34,44 @@ func main() { } type hoverButton struct { - id event.CID + id event.CallerID mouse.CollisionPhase *changingColorBox } -func (hb *hoverButton) Init() event.CID { - hb.id = event.NextID(hb) +func (hb *hoverButton) CID() event.CallerID { return hb.id } -func newHoverButton(x, y, w, h float64, clr color.RGBA, layer int) { +func newHoverButton(ctx *scene.Context, x, y, w, h float64, clr color.RGBA, layer int) { hb := &hoverButton{} - hb.Init() + hb.id = ctx.Register(hb) hb.changingColorBox = newChangingColorBox(x, y, int(w), int(h), clr) sp := collision.NewSpace(x, y, w, h, hb.id) sp.SetZLayer(float64(layer)) mouse.Add(sp) - mouse.PhaseCollision(sp) + mouse.PhaseCollision(sp, ctx.Handler) render.Draw(hb.changingColorBox, 0, layer) - hb.id.Bind(mouse.ClickOn, func(c event.CID, i interface{}) int { - hb := event.GetEntity(c).(*hoverButton) - me := i.(*mouse.Event) - fmt.Println(c, me.Point2) - hb.changingColorBox.c = color.RGBA{128, 128, 128, 128} + + event.Bind(ctx, mouse.Click, hb, func(box *hoverButton, me *mouse.Event) event.Response { + fmt.Println(box, me.Point2) + box.changingColorBox.c = color.RGBA{128, 128, 128, 128} me.StopPropagation = true return 0 }) - hb.id.Bind(mouse.Start, func(c event.CID, i interface{}) int { + event.Bind(ctx, mouse.Start, hb, func(box *hoverButton, me *mouse.Event) event.Response { fmt.Println("start") - hb := event.GetEntity(c).(*hoverButton) - me := i.(*mouse.Event) - hb.changingColorBox.c = color.RGBA{50, 50, 50, 50} + box.changingColorBox.c = color.RGBA{50, 50, 50, 50} me.StopPropagation = true return 0 }) - hb.id.Bind(mouse.Stop, func(c event.CID, i interface{}) int { + event.Bind(ctx, mouse.Stop, hb, func(box *hoverButton, me *mouse.Event) event.Response { fmt.Println("stop") - hb := event.GetEntity(c).(*hoverButton) - me := i.(*mouse.Event) - hb.changingColorBox.c = clr + box.changingColorBox.c = clr me.StopPropagation = true return 0 }) diff --git a/examples/clipboard/go.mod b/examples/clipboard/go.mod index f55b9172..15d5fc95 100644 --- a/examples/clipboard/go.mod +++ b/examples/clipboard/go.mod @@ -1,6 +1,6 @@ module github.com/oakmound/oak/examples/clipboard -go 1.16 +go 1.18 require ( github.com/atotto/clipboard v0.1.4 diff --git a/examples/clipboard/main.go b/examples/clipboard/main.go index 76fc8e5c..af38afb5 100644 --- a/examples/clipboard/main.go +++ b/examples/clipboard/main.go @@ -3,16 +3,15 @@ package main import ( "fmt" - gokey "golang.org/x/mobile/event/key" - + "github.com/atotto/clipboard" "github.com/oakmound/oak/v3" "github.com/oakmound/oak/v3/entities/x/btn" "github.com/oakmound/oak/v3/event" "github.com/oakmound/oak/v3/key" + "github.com/oakmound/oak/v3/mouse" "github.com/oakmound/oak/v3/render" "github.com/oakmound/oak/v3/scene" - - "github.com/atotto/clipboard" + gokey "golang.org/x/mobile/event/key" ) func main() { @@ -33,7 +32,7 @@ func newClipboardCopyText(text string, x, y float64) { btn.Pos(x, y), btn.Height(20), btn.FitText(20), - btn.Click(func(event.CID, interface{}) int { + btn.Click(func(b btn.Btn, me *mouse.Event) event.Response { err := clipboard.WriteAll(text) if err != nil { fmt.Println(err) @@ -46,14 +45,15 @@ func newClipboardCopyText(text string, x, y float64) { func newClipboardPaster(placeholder string, x, y float64) { textPtr := new(string) *textPtr = placeholder + btn.New( btn.Font(render.DefaultFont()), btn.TextPtr(textPtr), btn.Pos(x, y), btn.Height(20), btn.FitText(20), - btn.Binding(key.Down+key.V, func(_ event.CID, payload interface{}) int { - kv := payload.(key.Event) + btn.Binding(key.Down(key.V), func(b btn.Btn, kv key.Event) event.Response { + if kv.Modifiers&gokey.ModControl == gokey.ModControl { got, err := clipboard.ReadAll() if err != nil { @@ -64,7 +64,7 @@ func newClipboardPaster(placeholder string, x, y float64) { } return 0 }), - btn.Click(func(event.CID, interface{}) int { + btn.Click(func(b btn.Btn, me *mouse.Event) event.Response { got, err := clipboard.ReadAll() if err != nil { fmt.Println(err) diff --git a/examples/collision-demo/main.go b/examples/collision-demo/main.go index 41210b75..d5374005 100644 --- a/examples/collision-demo/main.go +++ b/examples/collision-demo/main.go @@ -7,6 +7,7 @@ import ( "github.com/oakmound/oak/v3/collision" "github.com/oakmound/oak/v3/entities" "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v3/key" "github.com/oakmound/oak/v3/render" "github.com/oakmound/oak/v3/scene" ) @@ -20,33 +21,33 @@ const ( ) func main() { - oak.AddScene("demo", scene.Scene{Start: func(*scene.Context) { + oak.AddScene("demo", scene.Scene{Start: func(ctx *scene.Context) { act := &AttachCollisionTest{} - act.Solid = entities.NewSolid(50, 50, 50, 50, render.NewColorBox(50, 50, color.RGBA{0, 0, 0, 255}), nil, act.Init()) + act.Solid = entities.NewSolid(50, 50, 50, 50, render.NewColorBox(50, 50, color.RGBA{0, 0, 0, 255}), nil, ctx.CallerMap.Register(act)) collision.Attach(act.Vector, act.Space, nil, 0, 0) - act.Bind(event.Enter, func(event.CID, interface{}) int { + event.Bind(ctx, event.Enter, act, func(act *AttachCollisionTest, ev event.EnterPayload) event.Response { if act.ShouldUpdate { act.ShouldUpdate = false act.R.Undraw() act.R = act.nextR render.Draw(act.R, 0, 1) } - if oak.IsDown("A") { + if oak.IsDown(key.A) { // We could use attachment here to not have to shift both // R and act but that is made more difficult by constantly // changing the act's R act.ShiftX(-3) act.R.ShiftX(-3) - } else if oak.IsDown("D") { + } else if oak.IsDown(key.D) { act.ShiftX(3) act.R.ShiftX(3) } - if oak.IsDown("W") { + if oak.IsDown(key.W) { act.ShiftY(-3) act.R.ShiftY(-3) - } else if oak.IsDown("S") { + } else if oak.IsDown(key.S) { act.ShiftY(3) act.R.ShiftY(3) } @@ -56,8 +57,8 @@ func main() { render.Draw(act.R, 0, 1) collision.PhaseCollision(act.Space, nil) - act.Bind(collision.Start, func(id event.CID, label interface{}) int { - l := label.(collision.Label) + + event.Bind(ctx, collision.Start, act, func(act *AttachCollisionTest, l collision.Label) event.Response { switch l { case RED: act.r += 125 @@ -75,8 +76,7 @@ func main() { } return 0 }) - act.Bind(collision.Stop, func(id event.CID, label interface{}) int { - l := label.(collision.Label) + event.Bind(ctx, collision.Stop, act, func(act *AttachCollisionTest, l collision.Label) event.Response { switch l { case RED: act.r -= 125 @@ -136,10 +136,12 @@ type AttachCollisionTest struct { nextR render.Renderable } -func (act *AttachCollisionTest) Init() event.CID { - return event.NextID(act) +// CID returns the event.CallerID so that this can be bound to. +func (act *AttachCollisionTest) CID() event.CallerID { + return act.CallerID } +// UpdateR with the rgb set on the act. func (act *AttachCollisionTest) UpdateR() { act.nextR = render.NewColorBox(50, 50, color.RGBA{uint8(act.r), uint8(act.g), uint8(act.b), 255}) act.nextR.SetPos(act.X(), act.Y()) diff --git a/examples/custom-cursor/main.go b/examples/custom-cursor/main.go index 863e0ae7..a7d391a9 100644 --- a/examples/custom-cursor/main.go +++ b/examples/custom-cursor/main.go @@ -37,11 +37,11 @@ func main() { ) ctx.DrawStack.Draw(box) - ctx.EventHandler.GlobalBind(mouse.Drag, func(_ event.CID, me interface{}) int { - mouseEvent := me.(*mouse.Event) - box.SetPos(mouseEvent.X(), mouseEvent.Y()) - return 0 - }) + event.GlobalBind(ctx, + mouse.Drag, func(mouseEvent *mouse.Event) event.Response { + box.SetPos(mouseEvent.X(), mouseEvent.Y()) + return 0 + }) }, }) oak.Init("customcursor") diff --git a/examples/fallback-font/go.mod b/examples/fallback-font/go.mod index 6ae55a2d..bc069ee2 100644 --- a/examples/fallback-font/go.mod +++ b/examples/fallback-font/go.mod @@ -1,6 +1,6 @@ module github.com/oakmound/oak/examples/fallback-font -go 1.16 +go 1.18 require ( github.com/flopp/go-findfont v0.0.0-20201114153133-e7393a00c15b diff --git a/examples/flappy-bird/main.go b/examples/flappy-bird/main.go index 6f6adbb3..f8516b85 100644 --- a/examples/flappy-bird/main.go +++ b/examples/flappy-bird/main.go @@ -37,11 +37,11 @@ func main() { score = 0 // 1. Make Player - newFlappy(90, 140) + newFlappy(ctx, 90, 140) // 2. Make scrolling repeating pillars var pillarLoop func() pillarLoop = func() { - newPillarPair() + newPillarPair(ctx) ctx.DoAfter(time.Duration(pillarFreq.Poll()*float64(time.Second)), pillarLoop) } go ctx.DoAfter(time.Duration(pillarFreq.Poll()*float64(time.Second)), pillarLoop) @@ -49,12 +49,6 @@ func main() { // 3. Make Score t := render.DefaultFont().NewIntText(&score, 200, 30) render.Draw(t, 0) - }, Loop: func() bool { - if playerHitPillar { - playerHitPillar = false - return false - } - return true }, End: func() (string, *scene.Result) { return "bounce", nil }}) @@ -69,25 +63,24 @@ type Flappy struct { *entities.Interactive } -// Init satisfies the event.Entity interface -func (f *Flappy) Init() event.CID { - return event.NextID(f) +// CID returns the event.CallerID so that this can be bound to. +func (flap *Flappy) CID() event.CallerID { + return flap.CallerID } -func newFlappy(x, y float64) *Flappy { +func newFlappy(ctx *scene.Context, x, y float64) *Flappy { f := new(Flappy) - f.Interactive = entities.NewInteractive(x, y, 32, 32, render.NewColorBox(32, 32, color.RGBA{0, 255, 255, 255}), nil, f.Init(), 1) + f.Interactive = entities.NewInteractive(x, y, 32, 32, render.NewColorBox(32, 32, color.RGBA{0, 255, 255, 255}), nil, ctx.Register(f), 1) f.RSpace.Add(pillar, func(s1, s2 *collision.Space) { - playerHitPillar = true + ctx.Window.NextScene() }) f.RSpace.Space.Label = player collision.Add(f.RSpace.Space) f.R.SetLayer(1) render.Draw(f.R, 0) - - f.Bind(event.Enter, func(event.CID, interface{}) int { + event.Bind(ctx, event.Enter, f, func(f *Flappy, ev event.EnterPayload) event.Response { f.ShiftPos(f.Delta.X(), f.Delta.Y()) f.Add(f.Delta) if f.Delta.Y() > 10 { @@ -101,7 +94,7 @@ func newFlappy(x, y float64) *Flappy { <-f.RSpace.CallOnHits() if f.Y()+f.H > 480 { - playerHitPillar = true + ctx.Window.NextScene() } if f.Y() < 0 { f.SetY(0) @@ -109,11 +102,12 @@ func newFlappy(x, y float64) *Flappy { } return 0 }) - f.Bind(mouse.Press, func(event.CID, interface{}) int { + + event.Bind(ctx, mouse.Press, f, func(f *Flappy, me *mouse.Event) event.Response { f.Delta.ShiftY(-4) return 0 }) - f.Bind(key.Down+key.W, func(event.CID, interface{}) int { + event.Bind(ctx, key.Down(key.W), f, func(f *Flappy, k key.Event) event.Response { f.Delta.ShiftY(-4) return 0 }) @@ -126,17 +120,18 @@ type Pillar struct { hasScored bool } -// Init satisfies the event.Entity interface -func (p *Pillar) Init() event.CID { - return event.NextID(p) +// CID returns the event.CallerID so that this can be bound to. +func (p *Pillar) CID() event.CallerID { + return p.CallerID } -func newPillar(x, y, h float64, isAbove bool) { +func newPillar(ctx *scene.Context, x, y, h float64, isAbove bool) { p := new(Pillar) - p.Solid = entities.NewSolid(x, y, 64, h, render.NewColorBox(64, int(h), color.RGBA{0, 255, 0, 255}), nil, p.Init()) + p.Solid = entities.NewSolid(x, y, 64, h, render.NewColorBox(64, int(h), color.RGBA{0, 255, 0, 255}), nil, ctx.Register(p)) p.Space.Label = pillar collision.Add(p.Space) - p.Bind(event.Enter, enterPillar) + event.Bind(ctx, event.Enter, p, enterPillar) + p.R.SetLayer(1) render.Draw(p.R, 0) // Don't score one out of each two pillars @@ -145,7 +140,7 @@ func newPillar(x, y, h float64, isAbove bool) { } } -func newPillarPair() { +func newPillarPair(ctx *scene.Context) { pos := gapPosition.Poll() span := gapSpan.Poll() if (pos + span) > 470 { @@ -155,12 +150,11 @@ func newPillarPair() { pos = 370 span = 100 } - newPillar(641, 0, pos, true) - newPillar(641, pos+span, 480-(pos+span), false) + newPillar(ctx, 641, 0, pos, true) + newPillar(ctx, 641, pos+span, 480-(pos+span), false) } -func enterPillar(id event.CID, nothing interface{}) int { - p := event.GetEntity(id).(*Pillar) +func enterPillar(p *Pillar, ev event.EnterPayload) event.Response { p.ShiftX(-2) if p.X()+p.W < 0 { p.Destroy() diff --git a/examples/joystick-viz/main.go b/examples/joystick-viz/main.go index 946da118..6061b7e0 100644 --- a/examples/joystick-viz/main.go +++ b/examples/joystick-viz/main.go @@ -22,13 +22,16 @@ func main() { *latestInput = "Latest Input: Keyboard+Mouse" ctx.DrawStack.Draw(render.NewStrPtrText(latestInput, 10, 460), 4) ctx.DrawStack.Draw(render.NewText("Space to Vibrate", 10, 440), 4) - ctx.EventHandler.GlobalBind(event.InputChange, func(_ event.CID, payload interface{}) int { - input := payload.(oak.InputType) + + event.GlobalBind(ctx, oak.InputChange, func(input oak.InputType) event.Response { + switch input { case oak.InputJoystick: *latestInput = "Latest Input: Joystick" - case oak.InputKeyboardMouse: - *latestInput = "Latest Input: Keyboard+Mouse" + case oak.InputKeyboard: + *latestInput = "Latest Input: Keyboard" + case oak.InputMouse: + *latestInput = "Latest Input: Mouse" } return 0 }) diff --git a/examples/keyboard-viz/main.go b/examples/keyboard-viz/main.go index ce10b8cc..4ba204f7 100644 --- a/examples/keyboard-viz/main.go +++ b/examples/keyboard-viz/main.go @@ -8,6 +8,7 @@ import ( "github.com/oakmound/oak/v3" "github.com/oakmound/oak/v3/alg/floatgeom" "github.com/oakmound/oak/v3/debugtools/inputviz" + "github.com/oakmound/oak/v3/dlog" "github.com/oakmound/oak/v3/render" "github.com/oakmound/oak/v3/scene" ) @@ -15,6 +16,7 @@ import ( func main() { oak.AddScene("keyviz", scene.Scene{ Start: func(ctx *scene.Context) { + fmt.Println("start") fnt, _ := render.DefFontGenerator.RegenerateWith(func(fg render.FontGenerator) render.FontGenerator { fg.Color = image.NewUniform(color.RGBA{0, 0, 0, 255}) fg.Size = 13 @@ -30,6 +32,7 @@ func main() { }, }) err := oak.Init("keyviz", func(c oak.Config) (oak.Config, error) { + c.Debug.Level = dlog.VERBOSE.String() c.Screen.Width = 800 c.Screen.Height = 300 return c, nil diff --git a/examples/multi-window/main.go b/examples/multi-window/main.go index e07cfa15..120e0840 100644 --- a/examples/multi-window/main.go +++ b/examples/multi-window/main.go @@ -26,10 +26,10 @@ func main() { ctx.DrawStack.Draw(cb, 0) dFPS := render.NewDrawFPS(0.1, nil, 600, 10) ctx.DrawStack.Draw(dFPS, 1) - ctx.EventHandler.GlobalBind(mouse.Press, mouse.Binding(func(_ event.CID, me *mouse.Event) int { + event.GlobalBind(ctx, mouse.Press, func(me *mouse.Event) event.Response { cb.SetPos(me.X(), me.Y()) return 0 - })) + }) }, }) go func() { @@ -55,10 +55,10 @@ func main() { ctx.DrawStack.Draw(cb, 0) dFPS := render.NewDrawFPS(0.1, nil, 600, 10) ctx.DrawStack.Draw(dFPS, 1) - ctx.EventHandler.GlobalBind(mouse.Press, mouse.Binding(func(_ event.CID, me *mouse.Event) int { + event.GlobalBind(ctx, mouse.Press, func(me *mouse.Event) event.Response { cb.SetPos(me.X(), me.Y()) return 0 - })) + }) }, }) c2.Init("scene2", func(c oak.Config) (oak.Config, error) { diff --git a/examples/particle-demo/main.go b/examples/particle-demo/main.go index 7112d4b1..238f9d87 100644 --- a/examples/particle-demo/main.go +++ b/examples/particle-demo/main.go @@ -56,7 +56,7 @@ func parseShape(args []string) shape.Shape { func main() { debugstream.AddCommand(debugstream.Command{Name: "followMouse", Operation: func(args []string) string { - event.GlobalBind(event.Enter, func(event.CID, interface{}) int { + event.GlobalBind(event.DefaultBus, event.Enter, func(ev event.EnterPayload) event.Response { // It'd be interesting to attach to the mouse position src.SetPos(float64(mouse.LastEvent.X()), float64(mouse.LastEvent.Y())) return 0 @@ -284,6 +284,7 @@ func main() { endColor = color.RGBA{255, 255, 255, 255} endColorRand = color.RGBA{0, 0, 0, 0} shape := shape.Square + src = pt.NewColorGenerator( pt.Pos(x, y), pt.Duration(pt.Inf), diff --git a/examples/piano/main.go b/examples/piano/main.go index f2bbae6f..ef045b8e 100644 --- a/examples/piano/main.go +++ b/examples/piano/main.go @@ -60,7 +60,7 @@ func (kc keyColor) Color() color.RGBA { return color.RGBA{255, 255, 255, 255} } -func newKey(note synth.Pitch, c keyColor, k string) *entities.Solid { +func newKey(ctx *scene.Context, note synth.Pitch, c keyColor, k key.Code) *entities.Solid { w := c.Width() h := c.Height() clr := c.Color() @@ -93,30 +93,30 @@ func newKey(note synth.Pitch, c keyColor, k string) *entities.Solid { s.Space.Label = labelWhiteKey } mouse.UpdateSpace(s.X(), s.Y(), s.W, s.H, s.Space) - s.Bind(key.Down+k, func(c event.CID, i interface{}) int { - if oak.IsDown(key.LeftShift) || oak.IsDown(key.RightShift) { + event.GlobalBind(ctx, key.Down(k), func(ev key.Event) event.Response { + // TODO: add helper function for this? + if ev.Modifiers&key.ModShift == key.ModShift { return 0 } - playPitch(note) + playPitch(ctx, note) sw.Set("down") return 0 }) - s.Bind(key.Up+k, func(c event.CID, i interface{}) int { - if oak.IsDown(key.LeftShift) || oak.IsDown(key.RightShift) { + event.GlobalBind(ctx, key.Up(k), func(ev key.Event) event.Response { + if ev.Modifiers&key.ModShift == key.ModShift { return 0 } releasePitch(note) sw.Set("up") return 0 }) - s.Bind(mouse.PressOn, func(c event.CID, i interface{}) int { - playPitch(note) - me := i.(*mouse.Event) + event.Bind(ctx, mouse.PressOn, s, func(_ *entities.Solid, me *mouse.Event) event.Response { + playPitch(ctx, note) me.StopPropagation = true sw.Set("down") return 0 }) - s.Bind(mouse.Release, func(c event.CID, i interface{}) int { + event.Bind(ctx, mouse.Release, s, func(_ *entities.Solid, me *mouse.Event) event.Response { releasePitch(note) sw.Set("up") return 0 @@ -130,13 +130,13 @@ type keyDef struct { x float64 } -var keycharOrder = []string{ - "Z", "S", "X", "D", "C", - "V", "G", "B", "H", "N", "J", "M", - key.Comma, "L", key.Period, key.Semicolon, key.Slash, - "Q", "2", "W", "3", "E", "4", "R", - "T", "6", "Y", "7", "U", - "I", "9", "O", "0", "P", key.HyphenMinus, key.LeftSquareBracket, +var keycharOrder = []key.Code{ + key.Z, key.S, key.X, key.D, key.C, + key.V, key.G, key.B, key.H, key.N, key.J, key.M, + key.Comma, key.L, key.FullStop, key.Semicolon, key.Slash, + key.Q, key.Num2, key.W, key.Num3, key.E, key.Num4, key.R, + key.T, key.Num6, key.Y, key.Num7, key.U, + key.I, key.Num9, key.O, key.Num0, key.P, key.HyphenMinus, key.LeftSquareBracket, } var playLock sync.Mutex @@ -144,7 +144,7 @@ var cancelFuncs = map[synth.Pitch]func(){} var synthKind func(...synth.Option) (pcm.Reader, error) -func playPitch(pitch synth.Pitch) { +func playPitch(ctx *scene.Context, pitch synth.Pitch) { playLock.Lock() defer playLock.Unlock() if cancel, ok := cancelFuncs[pitch]; ok { @@ -158,12 +158,12 @@ func playPitch(pitch synth.Pitch) { fmt.Println("new writer failed:", err) return } - monitor := newPCMMonitor(speaker) + monitor := newPCMMonitor(ctx, speaker) monitor.SetPos(0, 0) render.Draw(monitor) - ctx, cancel := context.WithCancel(context.Background()) + gctx, cancel := context.WithCancel(ctx) go func() { - err = pcm.Play(ctx, monitor, toPlay) + err = pcm.Play(gctx, monitor, toPlay) if err != nil { fmt.Println("play error:", err) } @@ -204,7 +204,7 @@ func main() { y := 200.0 i := 0 for i < len(keycharOrder) && x+kc.Width() < float64(ctx.Window.Width()-10) { - ky := newKey(pitch, kc, keycharOrder[i]) + ky := newKey(ctx, pitch, kc, keycharOrder[i]) ky.SetPos(x, y) layer := 0 if kc == keyColorBlack { @@ -223,36 +223,27 @@ func main() { i++ } // Consider: Adding volume control - event.GlobalBind(key.Down+key.S, func(c event.CID, i interface{}) int { - if oak.IsDown(key.LeftShift) || oak.IsDown(key.RightShift) { - synthKind = src.SinPCM - } - return 0 - }) - event.GlobalBind(key.Down+key.W, func(c event.CID, i interface{}) int { - if oak.IsDown(key.LeftShift) || oak.IsDown(key.RightShift) { - synthKind = src.SawPCM - } - return 0 - }) - event.GlobalBind(key.Down+key.T, func(c event.CID, i interface{}) int { - if oak.IsDown(key.LeftShift) || oak.IsDown(key.RightShift) { - synthKind = src.TrianglePCM - } - return 0 - }) - event.GlobalBind(key.Down+key.P, func(c event.CID, i interface{}) int { - if oak.IsDown(key.LeftShift) || oak.IsDown(key.RightShift) { - synthKind = src.PulsePCM(2) - } - return 0 - }) + codeKinds := map[key.Code]func(...synth.Option) (pcm.Reader, error){ + key.S: src.SinPCM, + key.W: src.SawPCM, + key.T: src.TrianglePCM, + key.P: src.PulsePCM(2), + } + for kc, synfn := range codeKinds { + event.GlobalBind(ctx, key.Down(kc), func(ev key.Event) event.Response { + if ev.Modifiers&key.ModShift == key.ModShift { + synthKind = synfn + } + return 0 + }) + } + help1 := render.NewText("Shift+([S]in/[T]ri/[P]ulse/sa[W]) to change wave style", 10, 500) help2 := render.NewText("Keyboard / mouse to play", 10, 520) render.Draw(help1) render.Draw(help2) - event.GlobalBind(mouse.ScrollDown, func(c event.CID, i interface{}) int { + event.GlobalBind(ctx, mouse.ScrollDown, func(_ *mouse.Event) event.Response { mag := globalMagnification - 0.05 if mag < 1 { mag = 1 @@ -260,7 +251,7 @@ func main() { globalMagnification = mag return 0 }) - event.GlobalBind(mouse.ScrollUp, func(c event.CID, i interface{}) int { + event.GlobalBind(ctx, mouse.ScrollUp, func(_ *mouse.Event) event.Response { globalMagnification += 0.05 return 0 }) @@ -275,7 +266,7 @@ func main() { } type pcmMonitor struct { - event.CID + event.CallerID render.LayeredPoint pcm.Writer pcm.Format @@ -285,7 +276,7 @@ type pcmMonitor struct { var globalMagnification float64 = 1 -func newPCMMonitor(w pcm.Writer) *pcmMonitor { +func newPCMMonitor(ctx *scene.Context, w pcm.Writer) *pcmMonitor { fmt := w.PCMFormat() pm := &pcmMonitor{ Writer: w, @@ -293,13 +284,11 @@ func newPCMMonitor(w pcm.Writer) *pcmMonitor { LayeredPoint: render.NewLayeredPoint(0, 0, 0), written: make([]byte, fmt.BytesPerSecond()*pcm.WriterBufferLengthInSeconds), } - pm.Init() return pm } -func (pm *pcmMonitor) Init() event.CID { - pm.CID = event.NextID(pm) - return pm.CID +func (pm *pcmMonitor) CID() event.CallerID { + return pm.CallerID } func (pm *pcmMonitor) PCMFormat() pcm.Format { diff --git a/examples/platformer-tutorial/2-moving/moving.go b/examples/platformer-tutorial/2-moving/moving.go index f50cfd82..d025bf3a 100644 --- a/examples/platformer-tutorial/2-moving/moving.go +++ b/examples/platformer-tutorial/2-moving/moving.go @@ -15,7 +15,7 @@ import ( ) func main() { - oak.AddScene("platformer", scene.Scene{Start: func(*scene.Context) { + oak.AddScene("platformer", scene.Scene{Start: func(ctx *scene.Context) { char := entities.NewMoving(100, 100, 16, 32, render.NewColorBox(16, 32, color.RGBA{255, 0, 0, 255}), @@ -24,15 +24,13 @@ func main() { render.Draw(char.R) char.Speed = physics.NewVector(3, 3) - - char.Bind(event.Enter, func(id event.CID, nothing interface{}) int { - char := event.GetEntity(id).(*entities.Moving) + event.Bind(ctx, event.Enter, char, func(c *entities.Moving, ev event.EnterPayload) event.Response { // Move left and right with A and D if oak.IsDown(key.A) { - char.ShiftX(-char.Speed.X()) + c.ShiftX(-c.Speed.X()) } if oak.IsDown(key.D) { - char.ShiftX(char.Speed.X()) + c.ShiftX(c.Speed.X()) } return 0 }) diff --git a/examples/platformer-tutorial/3-falling/falling.go b/examples/platformer-tutorial/3-falling/falling.go index cad71106..a6968536 100644 --- a/examples/platformer-tutorial/3-falling/falling.go +++ b/examples/platformer-tutorial/3-falling/falling.go @@ -24,7 +24,7 @@ const ( ) func main() { - oak.AddScene("platformer", scene.Scene{Start: func(*scene.Context) { + oak.AddScene("platformer", scene.Scene{Start: func(ctx *scene.Context) { char := entities.NewMoving(100, 100, 16, 32, render.NewColorBox(16, 32, color.RGBA{255, 0, 0, 255}), @@ -36,23 +36,23 @@ func main() { fallSpeed := .1 - char.Bind(event.Enter, func(id event.CID, nothing interface{}) int { - char := event.GetEntity(id).(*entities.Moving) + event.Bind(ctx, event.Enter, char, func(c *entities.Moving, ev event.EnterPayload) event.Response { + // Move left and right with A and D if oak.IsDown(key.A) { - char.ShiftX(-char.Speed.X()) + c.ShiftX(-c.Speed.X()) } if oak.IsDown(key.D) { - char.ShiftX(char.Speed.X()) + c.ShiftX(c.Speed.X()) } - hit := char.HitLabel(Ground) + hit := c.HitLabel(Ground) if hit == nil { // Fall if there's no ground - char.Delta.ShiftY(fallSpeed) + c.Delta.ShiftY(fallSpeed) } else { - char.Delta.SetY(0) + c.Delta.SetY(0) } - char.ShiftY(char.Delta.Y()) + c.ShiftY(c.Delta.Y()) return 0 }) diff --git a/examples/platformer-tutorial/4-jumping/jumping.go b/examples/platformer-tutorial/4-jumping/jumping.go index ea83252a..c9049469 100644 --- a/examples/platformer-tutorial/4-jumping/jumping.go +++ b/examples/platformer-tutorial/4-jumping/jumping.go @@ -24,7 +24,7 @@ const ( ) func main() { - oak.AddScene("platformer", scene.Scene{Start: func(*scene.Context) { + oak.AddScene("platformer", scene.Scene{Start: func(ctx *scene.Context) { char := entities.NewMoving(100, 100, 16, 32, render.NewColorBox(16, 32, color.RGBA{255, 0, 0, 255}), @@ -36,27 +36,26 @@ func main() { fallSpeed := .1 - char.Bind(event.Enter, func(id event.CID, nothing interface{}) int { - char := event.GetEntity(id).(*entities.Moving) + event.Bind(ctx, event.Enter, char, func(c *entities.Moving, ev event.EnterPayload) event.Response { // Move left and right with A and D if oak.IsDown(key.A) { - char.ShiftX(-char.Speed.X()) + c.ShiftX(-c.Speed.X()) } if oak.IsDown(key.D) { - char.ShiftX(char.Speed.X()) + c.ShiftX(c.Speed.X()) } - hit := collision.HitLabel(char.Space, Ground) + hit := collision.HitLabel(c.Space, Ground) if hit == nil { // Fall if there's no ground - char.Delta.ShiftY(fallSpeed) + c.Delta.ShiftY(fallSpeed) } else { - char.Delta.SetY(0) + c.Delta.SetY(0) // Jump with Space if oak.IsDown(key.Spacebar) { - char.Delta.ShiftY(-char.Speed.Y()) + c.Delta.ShiftY(-c.Speed.Y()) } } - char.ShiftY(char.Delta.Y()) + c.ShiftY(c.Delta.Y()) return 0 }) diff --git a/examples/platformer-tutorial/5-correct-jumping/correct-jumping.go b/examples/platformer-tutorial/5-correct-jumping/correct-jumping.go index 6691f3db..92040f79 100644 --- a/examples/platformer-tutorial/5-correct-jumping/correct-jumping.go +++ b/examples/platformer-tutorial/5-correct-jumping/correct-jumping.go @@ -24,7 +24,7 @@ const ( ) func main() { - oak.AddScene("platformer", scene.Scene{Start: func(*scene.Context) { + oak.AddScene("platformer", scene.Scene{Start: func(ctx *scene.Context) { char := entities.NewMoving(100, 100, 16, 32, render.NewColorBox(16, 32, color.RGBA{255, 0, 0, 255}), @@ -36,8 +36,7 @@ func main() { fallSpeed := .1 - char.Bind(event.Enter, func(id event.CID, nothing interface{}) int { - char := event.GetEntity(id).(*entities.Moving) + event.Bind(ctx, event.Enter, char, func(c *entities.Moving, ev event.EnterPayload) event.Response { // Move left and right with A and D if oak.IsDown(key.A) { char.ShiftX(-char.Speed.X()) diff --git a/examples/platformer-tutorial/6-complete/complete.go b/examples/platformer-tutorial/6-complete/complete.go index 5decabf7..42f17f7a 100644 --- a/examples/platformer-tutorial/6-complete/complete.go +++ b/examples/platformer-tutorial/6-complete/complete.go @@ -26,7 +26,7 @@ const ( ) func main() { - oak.AddScene("platformer", scene.Scene{Start: func(*scene.Context) { + oak.AddScene("platformer", scene.Scene{Start: func(ctx *scene.Context) { char := entities.NewMoving(100, 100, 16, 32, render.NewColorBox(16, 32, color.RGBA{255, 0, 0, 255}), @@ -38,8 +38,7 @@ func main() { fallSpeed := .2 - char.Bind(event.Enter, func(id event.CID, nothing interface{}) int { - char := event.GetEntity(id).(*entities.Moving) + event.Bind(ctx, event.Enter, char, func(c *entities.Moving, ev event.EnterPayload) event.Response { // Move left and right with A and D if oak.IsDown(key.A) { diff --git a/examples/pong/main.go b/examples/pong/main.go index ec8efbeb..5ae43bcc 100644 --- a/examples/pong/main.go +++ b/examples/pong/main.go @@ -25,10 +25,10 @@ const ( func main() { oak.AddScene("pong", - scene.Scene{Start: func(*scene.Context) { - newPaddle(20, 200, 1) - newPaddle(600, 200, 2) - newBall(320, 240) + scene.Scene{Start: func(ctx *scene.Context) { + newPaddle(ctx, 20, 200, 1) + newPaddle(ctx, 600, 200, 2) + newBall(ctx, 320, 240) render.Draw(render.DefaultFont().NewIntText(&score2, 200, 20), 3) render.Draw(render.DefaultFont().NewIntText(&score1, 400, 20), 3) }}) @@ -38,10 +38,10 @@ func main() { }) } -func newBall(x, y float64) { +func newBall(ctx *scene.Context, x, y float64) { b := entities.NewMoving(x, y, 10, 10, render.NewColorBoxR(10, 10, color.RGBA{255, 255, 255, 255}), nil, 0, 0) render.Draw(b.R, 2) - b.Bind(event.Enter, func(id event.CID, nothing interface{}) int { + event.GlobalBind(ctx, event.Enter, func(_ event.EnterPayload) event.Response { if b.Delta.X() == 0 && b.Delta.Y() == 0 { b.Delta.SetY((rand.Float64() - 0.5) * 4) b.Delta.SetX((rand.Float64() - 0.5) * 16) @@ -70,21 +70,20 @@ func newBall(x, y float64) { }) } -func newPaddle(x, y float64, player int) { +func newPaddle(ctx *scene.Context, x, y float64, player int) { p := entities.NewMoving(x, y, 20, 100, render.NewColorBoxR(20, 100, color.RGBA{255, 255, 255, 255}), nil, 0, 0) p.Speed.SetY(8) render.Draw(p.R, 1) p.Space.UpdateLabel(hitPaddle) if player == 1 { - p.Bind(event.Enter, enterPaddle(key.UpArrow, key.DownArrow)) + event.Bind(ctx, event.Enter, p, enterPaddle(key.UpArrow, key.DownArrow)) } else { - p.Bind(event.Enter, enterPaddle(key.W, key.S)) + event.Bind(ctx, event.Enter, p, enterPaddle(key.W, key.S)) } } -func enterPaddle(up, down string) func(event.CID, interface{}) int { - return func(id event.CID, nothing interface{}) int { - p := id.E().(*entities.Moving) +func enterPaddle(up, down key.Code) func(*entities.Moving, event.EnterPayload) event.Response { + return func(p *entities.Moving, _ event.EnterPayload) event.Response { p.Delta.SetY(0) if oak.IsDown(up) { p.Delta.SetY(-p.Speed.Y()) diff --git a/examples/radar-demo/main.go b/examples/radar-demo/main.go index 235fa48b..981ceea5 100644 --- a/examples/radar-demo/main.go +++ b/examples/radar-demo/main.go @@ -26,6 +26,8 @@ const ( // This example demonstrates making a basic radar or other custom renderable // type. The radar here acts as a UI element, staying on screen, and follows // around a player character. +//TODO: Remove and or link to grove radar as it is cleaner +// https://github.com/oakmound/grove/tree/master/components/radar func main() { oak.AddScene("demo", scene.Scene{Start: func(ctx *scene.Context) { @@ -36,8 +38,7 @@ func main() { oak.SetViewportBounds(intgeom.NewRect2(0, 0, xLimit, yLimit)) moveRect := floatgeom.NewRect2(0, 0, xLimit, yLimit) - - char.Bind(event.Enter, func(event.CID, interface{}) int { + event.Bind(ctx, event.Enter, char, func(char *entities.Moving, ev event.EnterPayload) event.Response { move.WASD(char) move.Limit(char, moveRect) move.CenterScreenOn(char) @@ -55,8 +56,8 @@ func main() { for i := 0; i < 5; i++ { x, y := rand.Float64()*400, rand.Float64()*400 - enemy := newEnemyOnRadar(x, y) - enemy.CID.Bind(event.Enter, standardEnemyMove) + enemy := newEnemyOnRadar(ctx, x, y) + event.Bind(ctx, event.Enter, enemy, standardEnemyMove) render.Draw(enemy.R, 1, 1) r.AddPoint(radar.Point{X: enemy.Xp(), Y: enemy.Yp()}, color.RGBA{255, 255, 0, 255}) } @@ -87,19 +88,15 @@ type enemyOnRadar struct { *entities.Moving } -func (eor *enemyOnRadar) Init() event.CID { - return event.NextID(eor) -} -func newEnemyOnRadar(x, y float64) *enemyOnRadar { +func newEnemyOnRadar(ctx *scene.Context, x, y float64) *enemyOnRadar { eor := new(enemyOnRadar) - eor.Moving = entities.NewMoving(50, y, 50, 50, render.NewColorBox(25, 25, color.RGBA{0, 200, 0, 0}), nil, eor.Init(), 0) + eor.Moving = entities.NewMoving(50, y, 50, 50, render.NewColorBox(25, 25, color.RGBA{0, 200, 0, 0}), nil, ctx.Register(eor), 0) eor.Speed = physics.NewVector(-1*(rand.Float64()*2+1), rand.Float64()*2-1) eor.Delta = eor.Speed return eor } -func standardEnemyMove(id event.CID, nothing interface{}) int { - eor := event.GetEntity(id).(*enemyOnRadar) +func standardEnemyMove(eor *enemyOnRadar, ev event.EnterPayload) event.Response { if eor.X() < 0 { eor.Delta.SetPos(math.Abs(eor.Speed.X()), (eor.Speed.Y())) } diff --git a/examples/rooms/main.go b/examples/rooms/main.go index 181e3c56..b9d3ad74 100644 --- a/examples/rooms/main.go +++ b/examples/rooms/main.go @@ -49,7 +49,7 @@ func main() { var transitioning bool var totalTransitionDelta intgeom.Point2 var transitionDelta intgeom.Point2 - char.Bind(event.Enter, func(event.CID, interface{}) int { + event.Bind(ctx, event.Enter, char, func(c *entities.Moving, ev event.EnterPayload) event.Response { dir, ok := isOffScreen(ctx, char) if !transitioning && ok { transitioning = true diff --git a/examples/screenopts/main.go b/examples/screenopts/main.go index d1b0b9bd..d0dd3bde 100644 --- a/examples/screenopts/main.go +++ b/examples/screenopts/main.go @@ -16,14 +16,13 @@ const ( ) func main() { - oak.AddScene("demo", scene.Scene{Start: func(*scene.Context) { + oak.AddScene("demo", scene.Scene{Start: func(ctx *scene.Context) { txt := render.NewText("Press F to toggle fullscreen. Press B to toggle borderless.", 50, 50) render.Draw(txt) borderless := borderlessAtStart fullscreen := fullscreenAtStart - - event.GlobalBind(key.Down+key.F, func(event.CID, interface{}) int { + event.GlobalBind(ctx, key.Down(key.W), func(k key.Event) event.Response { fullscreen = !fullscreen err := oak.SetFullScreen(fullscreen) if err != nil { @@ -32,7 +31,7 @@ func main() { } return 0 }) - event.GlobalBind(key.Down+key.B, func(event.CID, interface{}) int { + event.GlobalBind(ctx, key.Down(key.B), func(k key.Event) event.Response { borderless = !borderless err := oak.SetBorderless(borderless) if err != nil { diff --git a/examples/slide/show/slide.go b/examples/slide/show/slide.go index acf4fa7c..2fdade72 100644 --- a/examples/slide/show/slide.go +++ b/examples/slide/show/slide.go @@ -9,12 +9,13 @@ import ( oak "github.com/oakmound/oak/v3" "github.com/oakmound/oak/v3/debugstream" "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v3/key" "github.com/oakmound/oak/v3/render" "github.com/oakmound/oak/v3/scene" ) type Slide interface { - Init() + Init(*scene.Context) Continue() bool Prev() bool Transition() scene.Transition @@ -59,14 +60,19 @@ func Start(width, height int, slides ...Slide) { i := i sl := sl oak.AddScene("slide"+strconv.Itoa(i), scene.Scene{ - Start: func(*scene.Context) { sl.Init() }, - Loop: func() bool { - cont := sl.Continue() && !skip - // This should be disable-able - if !cont { + Start: func(ctx *scene.Context) { + + sl.Init(ctx) + event.GlobalBind(ctx, event.Enter, func(event.EnterPayload) event.Response { + cont := sl.Continue() && !skip oak.SetLoadingRenderable(render.NewSprite(0, 0, oak.ScreenShot())) - } - return cont + if !cont { + ctx.Window.NextScene() + return event.ResponseUnbindThisBinding + } + return 0 + }) + }, End: func() (string, *scene.Result) { fmt.Println("ending") @@ -75,6 +81,7 @@ func Start(width, height int, slides ...Slide) { return "slide" + skipTo, slideResult(sl) } if sl.Prev() { + fmt.Println("Prev slide requested from", i) if i > 0 { return "slide" + strconv.Itoa(i-1), slideResult(sl) } @@ -102,13 +109,18 @@ func Start(width, height int, slides ...Slide) { float64(ctx.Window.Height()-50), ), ) - event.GlobalBind("KeyDownSpacebar", func(event.CID, interface{}) int { + event.GlobalBind(ctx, key.Down(key.Spacebar), func(key.Event) event.Response { reset = true return 0 }) - }, - Loop: func() bool { - return !reset + + event.GlobalBind(ctx, event.Enter, func(event.EnterPayload) event.Response { + if !reset { + ctx.Window.NextScene() + return event.ResponseUnbindThisBinding + } + return 0 + }) }, End: func() (string, *scene.Result) { oak.SetColorBackground(oldBackground) diff --git a/examples/slide/show/static/basicSlide.go b/examples/slide/show/static/basicSlide.go index b0a4e803..3b2d01ce 100644 --- a/examples/slide/show/static/basicSlide.go +++ b/examples/slide/show/static/basicSlide.go @@ -6,39 +6,45 @@ import ( oak "github.com/oakmound/oak/v3" "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v3/key" + "github.com/oakmound/oak/v3/mouse" "github.com/oakmound/oak/v3/render" "github.com/oakmound/oak/v3/scene" ) type Slide struct { Rs *render.CompositeR - ContinueKey string - PrevKey string + ContinueKey key.Code + PrevKey key.Code transition scene.Transition cont bool prev bool OnClick func() } -func (ss *Slide) Init() { +func (ss *Slide) Init(ctx *scene.Context) { oak.SetFullScreen(true) render.Draw(ss.Rs, 0) - event.GlobalBind("KeyUp"+ss.ContinueKey, func(event.CID, interface{}) int { + + event.GlobalBind(ctx, key.Up(ss.ContinueKey), func(key.Event) event.Response { + fmt.Println("continue key pressed") ss.cont = true return 0 }) - event.GlobalBind("KeyUp"+ss.PrevKey, func(event.CID, interface{}) int { + + event.GlobalBind(ctx, key.Up(ss.PrevKey), func(key.Event) event.Response { fmt.Println("prev key pressed") ss.prev = true return 0 }) - event.GlobalBind("KeyUpEscape", func(event.CID, interface{}) int { + + event.GlobalBind(ctx, key.Up(key.Escape), func(key.Event) event.Response { os.Exit(0) return 0 }) if ss.OnClick != nil { - event.GlobalBind("MousePress", func(event.CID, interface{}) int { + event.GlobalBind(ctx, mouse.Press, func(*mouse.Event) event.Response { ss.OnClick() return 0 }) @@ -69,8 +75,8 @@ func (ss *Slide) Transition() scene.Transition { func NewSlide(rs ...render.Renderable) *Slide { return &Slide{ Rs: render.NewCompositeR(rs...), - ContinueKey: "RightArrow", - PrevKey: "LeftArrow", + ContinueKey: key.RightArrow, + PrevKey: key.LeftArrow, } } @@ -88,7 +94,7 @@ func Background(r render.Modifiable) SlideOption { } } -func ControlKeys(cont, prev string) SlideOption { +func ControlKeys(cont, prev key.Code) SlideOption { return func(s *Slide) *Slide { s.ContinueKey = cont s.PrevKey = prev diff --git a/examples/sprite-demo/main.go b/examples/sprite-demo/main.go index a427072e..872a3e53 100644 --- a/examples/sprite-demo/main.go +++ b/examples/sprite-demo/main.go @@ -10,6 +10,7 @@ import ( oak "github.com/oakmound/oak/v3" "github.com/oakmound/oak/v3/entities" "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v3/key" "github.com/oakmound/oak/v3/render" "github.com/oakmound/oak/v3/render/mod" "github.com/oakmound/oak/v3/scene" @@ -27,7 +28,7 @@ var cache = [360]*image.RGBA{} func main() { oak.AddScene( "demo", - scene.Scene{Start: func(*scene.Context) { + scene.Scene{Start: func(ctx *scene.Context) { render.Draw(render.NewDrawFPS(0.03, nil, 10, 10)) render.Draw(render.NewLogicFPS(0.03, nil, 10, 20)) @@ -35,11 +36,11 @@ func main() { layerTxt := render.DefaultFont().NewIntText(&layer, 30, 20) layerTxt.SetLayer(100000000) render.Draw(layerTxt, 0) - NewGopher(layer) + NewGopher(ctx, layer) layer++ - event.GlobalBind(event.Enter, func(event.CID, interface{}) int { - if oak.IsDown("K") { - NewGopher(layer) + event.GlobalBind(ctx, event.Enter, func(ev event.EnterPayload) event.Response { + if oak.IsDown(key.K) { + NewGopher(ctx, layer) layer++ } return 0 @@ -76,31 +77,23 @@ type Gopher struct { rotation int } -// Init sets up a gophers CID -func (g *Gopher) Init() event.CID { - return event.NextID(g) -} - // NewGopher creates a gopher sprite to bounce around -func NewGopher(layer int) { - goph := Gopher{} +func NewGopher(ctx *scene.Context, layer int) { + goph := new(Gopher) goph.Doodad = entities.NewDoodad( rand.Float64()*576, rand.Float64()*416, render.NewSwitch("goph", map[string]render.Modifiable{"goph": render.EmptyRenderable()}), - //render.NewReverting(render.LoadSprite(filepath.Join("raw", "gopher11.png"))), - goph.Init()) + ctx.Register(goph)) goph.R.SetLayer(layer) - goph.Bind("EnterFrame", gophEnter) + event.Bind(ctx, event.Enter, goph, gophEnter) goph.deltaX = 4 * float64(rand.Intn(2)*2-1) goph.deltaY = 4 * float64(rand.Intn(2)*2-1) goph.rotation = rand.Intn(360) render.Draw(goph.R, 0) } -func gophEnter(cid event.CID, nothing interface{}) int { - goph := event.GetEntity(cid).(*Gopher) - +func gophEnter(goph *Gopher, ev event.EnterPayload) event.Response { // Compare against this version of rotation // (also swap the comments on lines in goph.Doodad's renderable) //goph.R.(*render.Reverting).RevertAndModify(1, render.Rotate(goph.rotation)) diff --git a/examples/svg/go.mod b/examples/svg/go.mod index c85ce234..0f091b98 100644 --- a/examples/svg/go.mod +++ b/examples/svg/go.mod @@ -1,12 +1,32 @@ module github.com/oakmound/oak/examples/svg -go 1.16 +go 1.18 require ( github.com/oakmound/oak/v3 v3.0.0-alpha.1 github.com/srwiley/oksvg v0.0.0-20210320200257-875f767ac39a github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 +) + +require ( + dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037 // indirect + github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect + github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect + github.com/disintegration/gift v1.2.0 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/hajimehoshi/go-mp3 v0.3.1 // indirect + github.com/oakmound/alsa v0.0.2 // indirect + github.com/oakmound/libudev v0.2.1 // indirect + github.com/oakmound/w32 v2.1.0+incompatible // indirect + github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf // indirect + golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect golang.org/x/image v0.0.0-20210504121937-7319ad40d33e // indirect + golang.org/x/mobile v0.0.0-20220112015953-858099ff7816 // indirect + golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect + golang.org/x/sys v0.0.0-20220111092808-5a964db01320 // indirect + golang.org/x/text v0.3.6 // indirect ) replace github.com/oakmound/oak/v3 => ../.. diff --git a/examples/svg/go.sum b/examples/svg/go.sum index e94799e5..8ec8315c 100644 --- a/examples/svg/go.sum +++ b/examples/svg/go.sum @@ -7,8 +7,6 @@ github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= 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/go.mod h1:CHkHWWZ4kbGY6jEy1+qlitDaCtRgNvCOQdakj/1Yl/Q= -github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1/go.mod h1:frG94byMNy+1CgGrQ25dZ+17tf98EN+OYBQL4Zh612M= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb h1:T6gaWBvRzJjuOrdCtg8fXXjKai2xSDqWTcKFUPuw8Tw= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= @@ -17,7 +15,6 @@ github.com/hajimehoshi/go-mp3 v0.3.1 h1:pn/SKU1+/rfK8KaZXdGEC2G/KCB2aLRjbTCrwKco github.com/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= github.com/jfreymuth/pulse v0.1.0 h1:KN38/9hoF9PJvP5DpEVhMRKNuwnJUonc8c9ARorRXUA= -github.com/jfreymuth/pulse v0.1.0/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no= github.com/oakmound/alsa v0.0.2 h1:JbOUckkJqVvhABth7qy2JgAjqsWuBPggyoYOk1L6eK0= github.com/oakmound/alsa v0.0.2/go.mod h1:wx+ehwqFnNL7foTwxxu2bKQlaUmD2oXd4ka1UBSgWAo= github.com/oakmound/libudev v0.2.1 h1:gaXuw7Pbt3RSRxbUakAjl0dSW6Wo3TZWpwS5aMq8+EA= @@ -39,7 +36,6 @@ golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMD golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210504121937-7319ad40d33e h1:PzJMNfFQx+QO9hrC1GwZ4BoPGeNGhfeQEgcQFArEjPk= golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= diff --git a/examples/text-demo-1/main.go b/examples/text-demo-1/main.go index 0b6c7a30..5aa60be5 100644 --- a/examples/text-demo-1/main.go +++ b/examples/text-demo-1/main.go @@ -64,21 +64,23 @@ func main() { render.Draw(font2.NewText("r", 160, 260), 0) render.Draw(font2.NewText("g", 280, 260), 0) render.Draw(font2.NewText("b", 400, 260), 0) + + go func() { + for { + r = limit.EnforceRange(r + diff.Poll()) + g = limit.EnforceRange(g + diff.Poll()) + b = limit.EnforceRange(b + diff.Poll()) + font.Drawer.Src = image.NewUniform( + color.RGBA{ + uint8(r), + uint8(g), + uint8(b), + 255, + }, + ) + } + }() }, - Loop: func() bool { - r = limit.EnforceRange(r + diff.Poll()) - g = limit.EnforceRange(g + diff.Poll()) - b = limit.EnforceRange(b + diff.Poll()) - font.Drawer.Src = image.NewUniform( - color.RGBA{ - uint8(r), - uint8(g), - uint8(b), - 255, - }, - ) - return true - }, }) oak.SetFS(assets) oak.Init("demo") diff --git a/examples/text-demo-2/main.go b/examples/text-demo-2/main.go index 235f5c10..afe8cf38 100644 --- a/examples/text-demo-2/main.go +++ b/examples/text-demo-2/main.go @@ -60,26 +60,28 @@ func main() { strs = append(strs, font.NewText(str, 0, y)) render.Draw(strs[len(strs)-1], 0) } - }, - Loop: func() bool { - r = limit.EnforceRange(r + diff.Poll()) - g = limit.EnforceRange(g + diff.Poll()) - b = limit.EnforceRange(b + diff.Poll()) - // This should be a function in oak to just set color source - // (or texture source) - font.Drawer.Src = image.NewUniform( - color.RGBA{ - uint8(r), - uint8(g), - uint8(b), - 255, - }, - ) - for _, st := range strs { - st.SetString(randomStr(strlen)) + + go func() { + for { + r = limit.EnforceRange(r + diff.Poll()) + g = limit.EnforceRange(g + diff.Poll()) + b = limit.EnforceRange(b + diff.Poll()) + // This should be a function in oak to just set color source + // (or texture source) + font.Drawer.Src = image.NewUniform( + color.RGBA{ + uint8(r), + uint8(g), + uint8(b), + 255, + }, + ) + for _, st := range strs { + st.SetString(randomStr(strlen)) + } } - return true - }, + }() + }, }) render.SetDrawStack( render.NewDynamicHeap(), diff --git a/examples/titlescreen-demo/main.go b/examples/titlescreen-demo/main.go index 9cda7718..246140b4 100644 --- a/examples/titlescreen-demo/main.go +++ b/examples/titlescreen-demo/main.go @@ -6,6 +6,7 @@ import ( oak "github.com/oakmound/oak/v3" "github.com/oakmound/oak/v3/entities" + "github.com/oakmound/oak/v3/event" "github.com/oakmound/oak/v3/key" "github.com/oakmound/oak/v3/render" "github.com/oakmound/oak/v3/scene" @@ -48,69 +49,71 @@ func main() { //tell the draw loop to draw titleText render.Draw(titleText) - //do the same for the text with button instuctions, but this time Y position is not a placeholder (X still is) + //do the same for the text with button instructions, but this time Y position is not a placeholder (X still is) instructionText := render.NewText("press Enter to start, or press Q to quit", 0, float64(ctx.Window.Height()*3/4)) //this time we only center the X axis, otherwise it would overlap titleText center(ctx, instructionText, X) render.Draw(instructionText) - }, Loop: func() bool { - //if the enter key is pressed, go to the next scene - if oak.IsDown(key.Enter) { - return false - } - //exit the program if the q key is pressed - if oak.IsDown(key.Q) { + event.GlobalBind(ctx, key.Down(key.ReturnEnter), func(key.Event) event.Response { + // Go to the next scene if enter is pressed. Next scene is the game + ctx.Window.NextScene() + return 0 + }) + event.GlobalBind(ctx, key.Down(key.Q), func(key.Event) event.Response { + // exit the game if q is pressed os.Exit(0) - } - return true + return 0 + }) + }, End: func() (string, *scene.Result) { return "game", nil //set the next scene to "game" }}) - //we declare this here so it can be accesed by the scene start and scene loop + //we declare this here so it can be accessed by the scene start and scene loop var player *entities.Moving //define the "game" (it's just a square that can be moved with WASD) - oak.AddScene("game", scene.Scene{Start: func(*scene.Context) { + oak.AddScene("game", scene.Scene{Start: func(ctx *scene.Context) { //create the player, a blue 32x32 square at 100,100 player = entities.NewMoving(100, 100, 32, 32, render.NewColorBox(32, 32, color.RGBA{0, 0, 255, 255}), nil, 0, 0) //because the player is more than visuals (it has a hitbox, even though we don't use it), - //we have to get the visual part specificaly, and not the whole thing. + //we have to get the visual part specifically, and not the whole thing. render.Draw(player.R) controlsText := render.NewText("WASD to move, ESC to return to titlescreen", 5, 20) //we draw the text on layer 1 (instead of the default layer 0) //because we want it to show up above the player render.Draw(controlsText, 1) - }, Loop: func() bool { - //if escape is pressed, go to the next scene (titlescreen) - if oak.IsDown(key.Escape) { - return false - } - //controls - if oak.IsDown(key.S) { - //if S is pressed, set the player's vertical speed to 2 (positive == down) - player.Delta.SetY(2) - } else if oak.IsDown(key.W) { - player.Delta.SetY(-2) - } else { - //if the now buttons are pressed for vertical movement, don't move verticaly - player.Delta.SetY(0) - } - - //do the same thing as before, but horizontaly - if oak.IsDown(key.D) { - player.Delta.SetX(2) - } else if oak.IsDown(key.A) { - player.Delta.SetX(-2) - } else { - player.Delta.SetX(0) - } - //apply the player's speed to their position - player.ShiftPos(player.Delta.X(), player.Delta.Y()) - return true + event.GlobalBind(ctx, key.Down(key.Escape), func(key.Event) event.Response { + // Go to the next scene if escape is pressed. Next scene is titlescreen + ctx.Window.NextScene() + return 0 + }) + event.GlobalBind(ctx, event.Enter, func(event.EnterPayload) event.Response { + if oak.IsDown(key.S) { + //if S is pressed, set the player's vertical speed to 2 (positive == down) + player.Delta.SetY(2) + } else if oak.IsDown(key.W) { + player.Delta.SetY(-2) + } else { + //if the now buttons are pressed for vertical movement, don't move vertically + player.Delta.SetY(0) + } + + //do the same thing as before, but horizontally + if oak.IsDown(key.D) { + player.Delta.SetX(2) + } else if oak.IsDown(key.A) { + player.Delta.SetX(-2) + } else { + player.Delta.SetX(0) + } + //apply the player's speed to their position + player.ShiftPos(player.Delta.X(), player.Delta.Y()) + return 0 + }) }, End: func() (string, *scene.Result) { return "titlescreen", nil //set the next scene to be titlescreen }}) diff --git a/examples/top-down-shooter-tutorial/1-start/start.go b/examples/top-down-shooter-tutorial/1-start/start.go index 8a1d6671..126cf2e4 100644 --- a/examples/top-down-shooter-tutorial/1-start/start.go +++ b/examples/top-down-shooter-tutorial/1-start/start.go @@ -24,7 +24,7 @@ var ( ) func main() { - oak.AddScene("tds", scene.Scene{Start: func(*scene.Context) { + oak.AddScene("tds", scene.Scene{Start: func(ctx *scene.Context) { playerAlive = true char := entities.NewMoving(100, 100, 32, 32, render.NewColorBox(32, 32, color.RGBA{0, 255, 0, 255}), @@ -33,8 +33,8 @@ func main() { char.Speed = physics.NewVector(5, 5) render.Draw(char.R) - char.Bind(event.Enter, func(id event.CID, _ interface{}) int { - char := event.GetEntity(id).(*entities.Moving) + event.Bind(ctx, event.Enter, char, func(c *entities.Moving, ev event.EnterPayload) event.Response { + char.Delta.Zero() if oak.IsDown(key.W) { char.Delta.ShiftY(-char.Speed.Y()) diff --git a/examples/top-down-shooter-tutorial/2-shooting/shooting.go b/examples/top-down-shooter-tutorial/2-shooting/shooting.go index 4d1e4493..89844bb0 100644 --- a/examples/top-down-shooter-tutorial/2-shooting/shooting.go +++ b/examples/top-down-shooter-tutorial/2-shooting/shooting.go @@ -21,13 +21,9 @@ const ( Player collision.Label = 2 ) -var ( - playerAlive = true -) - func main() { oak.AddScene("tds", scene.Scene{Start: func(ctx *scene.Context) { - playerAlive = true + char := entities.NewMoving(100, 100, 32, 32, render.NewColorBox(32, 32, color.RGBA{0, 255, 0, 255}), nil, 0, 0) @@ -35,8 +31,8 @@ func main() { char.Speed = physics.NewVector(5, 5) render.Draw(char.R) - char.Bind(event.Enter, func(id event.CID, _ interface{}) int { - char := event.GetEntity(id).(*entities.Moving) + event.Bind(ctx, event.Enter, char, func(char *entities.Moving, ev event.EnterPayload) event.Response { + char.Delta.Zero() if oak.IsDown(key.W) { char.Delta.ShiftY(-char.Speed.Y()) @@ -53,24 +49,20 @@ func main() { char.ShiftPos(char.Delta.X(), char.Delta.Y()) hit := char.HitLabel(Enemy) if hit != nil { - playerAlive = false + ctx.Window.NextScene() } return 0 }) - - char.Bind(mouse.Press, func(id event.CID, me interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - mevent := me.(*mouse.Event) + event.Bind(ctx, mouse.Press, char, func(char *entities.Moving, mevent *mouse.Event) event.Response { ctx.DrawForTime( render.NewLine(char.X()+char.W/2, char.Y()+char.H/2, mevent.X(), mevent.Y(), color.RGBA{0, 128, 0, 128}), time.Millisecond*50, 1) + return 0 }) - }, Loop: func() bool { - return playerAlive }}) oak.Init("tds") } diff --git a/examples/top-down-shooter-tutorial/3-enemies/enemies.go b/examples/top-down-shooter-tutorial/3-enemies/enemies.go index 65ce4577..f34b0599 100644 --- a/examples/top-down-shooter-tutorial/3-enemies/enemies.go +++ b/examples/top-down-shooter-tutorial/3-enemies/enemies.go @@ -25,18 +25,18 @@ const ( ) var ( - playerAlive = true // Vectors are backed by pointers, // so despite this not being a pointer, // this does update according to the player's // position so long as we don't reset // the player's position vector playerPos physics.Vector + + destroy = event.RegisterEvent[struct{}]() ) func main() { oak.AddScene("tds", scene.Scene{Start: func(ctx *scene.Context) { - playerAlive = true char := entities.NewMoving(100, 100, 32, 32, render.NewColorBox(32, 32, color.RGBA{0, 255, 0, 255}), nil, 0, 0) @@ -45,8 +45,8 @@ func main() { playerPos = char.Point.Vector render.Draw(char.R) - char.Bind(event.Enter, func(id event.CID, _ interface{}) int { - char := event.GetEntity(id).(*entities.Moving) + event.Bind(ctx, event.Enter, char, func(char *entities.Moving, ev event.EnterPayload) event.Response { + char.Delta.Zero() if oak.IsDown(key.W) { char.Delta.ShiftY(-char.Speed.Y()) @@ -63,21 +63,18 @@ func main() { char.ShiftPos(char.Delta.X(), char.Delta.Y()) hit := char.HitLabel(Enemy) if hit != nil { - playerAlive = false + ctx.Window.NextScene() } - return 0 }) - char.Bind(mouse.Press, func(id event.CID, me interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - mevent := me.(*mouse.Event) + event.Bind(ctx, mouse.Press, char, func(char *entities.Moving, mevent *mouse.Event) event.Response { x := char.X() + char.W/2 y := char.Y() + char.H/2 ray.DefaultCaster.CastDistance = floatgeom.Point2{x, y}.Sub(floatgeom.Point2{mevent.X(), mevent.Y()}).Magnitude() hits := ray.CastTo(floatgeom.Point2{x, y}, floatgeom.Point2{mevent.X(), mevent.Y()}) for _, hit := range hits { - hit.Zone.CID.Trigger("Destroy", nil) + event.TriggerForCallerOn(ctx, hit.Zone.CID, destroy, struct{}{}) } ctx.DrawForTime( render.NewLine(x, y, mevent.X(), mevent.Y(), color.RGBA{0, 128, 0, 128}), @@ -86,16 +83,13 @@ func main() { return 0 }) - event.GlobalBind(event.Enter, func(_ event.CID, frames interface{}) int { - enterPayload := frames.(event.EnterPayload) + event.GlobalBind(ctx, event.Enter, func(enterPayload event.EnterPayload) event.Response { if enterPayload.FramesElapsed%EnemyRefresh == 0 { go NewEnemy(ctx) } return 0 }) - }, Loop: func() bool { - return playerAlive }}) oak.Init("tds") } @@ -117,21 +111,17 @@ func NewEnemy(ctx *scene.Context) { render.Draw(enemy.R) enemy.UpdateLabel(Enemy) - - enemy.Bind(event.Enter, func(id event.CID, _ interface{}) int { - enemy := event.GetEntity(id).(*entities.Solid) + event.Bind(ctx, event.Enter, enemy, func(e *entities.Solid, ev event.EnterPayload) event.Response { // move towards the player - x, y := enemy.GetPos() + x, y := e.GetPos() pt := floatgeom.Point2{x, y} pt2 := floatgeom.Point2{playerPos.X(), playerPos.Y()} delta := pt2.Sub(pt).Normalize().MulConst(EnemySpeed) - enemy.ShiftPos(delta.X(), delta.Y()) + e.ShiftPos(delta.X(), delta.Y()) return 0 }) - - enemy.Bind("Destroy", func(id event.CID, _ interface{}) int { - enemy := event.GetEntity(id).(*entities.Solid) - enemy.Destroy() + event.Bind(ctx, destroy, enemy, func(e *entities.Solid, nothing struct{}) event.Response { + e.Destroy() return 0 }) } diff --git a/examples/top-down-shooter-tutorial/4-sprites/sprites.go b/examples/top-down-shooter-tutorial/4-sprites/sprites.go index 26cbd871..933701ed 100644 --- a/examples/top-down-shooter-tutorial/4-sprites/sprites.go +++ b/examples/top-down-shooter-tutorial/4-sprites/sprites.go @@ -29,7 +29,6 @@ const ( ) var ( - playerAlive = true // Vectors are backed by pointers, // so despite this not being a pointer, // this does update according to the player's @@ -37,13 +36,15 @@ var ( // the player's position vector playerPos physics.Vector + destroy = event.RegisterEvent[struct{}]() + sheet [][]*render.Sprite ) func main() { oak.AddScene("tds", scene.Scene{Start: func(ctx *scene.Context) { // Initialization - playerAlive = true + sprites, err := render.GetSheet("sheet.png") dlog.ErrorCheck(err) sheet = sprites.ToSprites() @@ -67,8 +68,7 @@ func main() { playerPos = char.Point.Vector render.Draw(char.R, 2) - char.Bind(event.Enter, func(id event.CID, _ interface{}) int { - char := event.GetEntity(id).(*entities.Moving) + event.Bind(ctx, event.Enter, char, func(char *entities.Moving, ev event.EnterPayload) event.Response { char.Delta.Zero() if oak.IsDown(key.W) { char.Delta.ShiftY(-char.Speed.Y()) @@ -85,7 +85,7 @@ func main() { char.ShiftPos(char.Delta.X(), char.Delta.Y()) hit := char.HitLabel(Enemy) if hit != nil { - playerAlive = false + ctx.Window.NextScene() } // update animation @@ -103,15 +103,13 @@ func main() { return 0 }) - char.Bind(mouse.Press, func(id event.CID, me interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - mevent := me.(*mouse.Event) + event.Bind(ctx, mouse.Press, char, func(char *entities.Moving, mevent *mouse.Event) event.Response { x := char.X() + char.W/2 y := char.Y() + char.H/2 ray.DefaultCaster.CastDistance = floatgeom.Point2{x, y}.Sub(floatgeom.Point2{mevent.X(), mevent.Y()}).Magnitude() hits := ray.CastTo(floatgeom.Point2{x, y}, floatgeom.Point2{mevent.X(), mevent.Y()}) for _, hit := range hits { - hit.Zone.CID.Trigger("Destroy", nil) + event.TriggerForCallerOn(ctx, hit.Zone.CID, destroy, struct{}{}) } ctx.DrawForTime( render.NewLine(x, y, mevent.X(), mevent.Y(), color.RGBA{0, 128, 0, 128}), @@ -121,8 +119,7 @@ func main() { }) // Create enemies periodically - event.GlobalBind(event.Enter, func(_ event.CID, frames interface{}) int { - enterPayload := frames.(event.EnterPayload) + event.GlobalBind(ctx, event.Enter, func(enterPayload event.EnterPayload) event.Response { if enterPayload.FramesElapsed%EnemyRefresh == 0 { go NewEnemy(ctx) } @@ -140,8 +137,6 @@ func main() { } } - }, Loop: func() bool { - return playerAlive }}) oak.Init("tds", func(c oak.Config) (oak.Config, error) { @@ -178,8 +173,7 @@ func NewEnemy(ctx *scene.Context) { enemy.UpdateLabel(Enemy) - enemy.Bind(event.Enter, func(id event.CID, _ interface{}) int { - enemy := event.GetEntity(id).(*entities.Solid) + event.Bind(ctx, event.Enter, enemy, func(e *entities.Solid, ev event.EnterPayload) event.Response { // move towards the player x, y := enemy.GetPos() pt := floatgeom.Point2{x, y} @@ -201,9 +195,8 @@ func NewEnemy(ctx *scene.Context) { return 0 }) - enemy.Bind("Destroy", func(id event.CID, _ interface{}) int { - enemy := event.GetEntity(id).(*entities.Solid) - enemy.Destroy() + event.Bind(ctx, destroy, enemy, func(e *entities.Solid, nothing struct{}) event.Response { + e.Destroy() return 0 }) } diff --git a/examples/top-down-shooter-tutorial/5-viewport/viewport.go b/examples/top-down-shooter-tutorial/5-viewport/viewport.go index f3ceab38..4900b62c 100644 --- a/examples/top-down-shooter-tutorial/5-viewport/viewport.go +++ b/examples/top-down-shooter-tutorial/5-viewport/viewport.go @@ -29,7 +29,6 @@ const ( ) var ( - playerAlive = true // Vectors are backed by pointers, // so despite this not being a pointer, // this does update according to the player's @@ -37,6 +36,8 @@ var ( // the player's position vector playerPos physics.Vector + destroy = event.RegisterEvent[struct{}]() + sheet [][]*render.Sprite ) @@ -49,7 +50,6 @@ func main() { oak.AddScene("tds", scene.Scene{Start: func(ctx *scene.Context) { // Initialization - playerAlive = true sprites, err := render.GetSheet("sheet.png") dlog.ErrorCheck(err) sheet = sprites.ToSprites() @@ -75,8 +75,7 @@ func main() { playerPos = char.Point.Vector render.Draw(char.R, 2) - char.Bind(event.Enter, func(id event.CID, _ interface{}) int { - char := event.GetEntity(id).(*entities.Moving) + event.Bind(ctx, event.Enter, char, func(char *entities.Moving, ev event.EnterPayload) event.Response { char.Delta.Zero() if oak.IsDown(key.W) { char.Delta.ShiftY(-char.Speed.Y()) @@ -108,7 +107,7 @@ func main() { ) hit := char.HitLabel(Enemy) if hit != nil { - playerAlive = false + ctx.Window.NextScene() } // update animation @@ -126,9 +125,7 @@ func main() { return 0 }) - char.Bind(mouse.Press, func(id event.CID, me interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - mevent := me.(*mouse.Event) + event.Bind(ctx, mouse.Press, char, func(char *entities.Moving, mevent *mouse.Event) event.Response { x := char.X() + char.W/2 y := char.Y() + char.H/2 vp := ctx.Window.Viewport() @@ -137,7 +134,7 @@ func main() { ray.DefaultCaster.CastDistance = floatgeom.Point2{x, y}.Sub(floatgeom.Point2{mx, my}).Magnitude() hits := ray.CastTo(floatgeom.Point2{x, y}, floatgeom.Point2{mx, my}) for _, hit := range hits { - hit.Zone.CID.Trigger("Destroy", nil) + event.TriggerForCallerOn(ctx, hit.Zone.CID, destroy, struct{}{}) } ctx.DrawForTime( render.NewLine(x, y, mx, my, color.RGBA{0, 128, 0, 128}), @@ -147,10 +144,9 @@ func main() { }) // Create enemies periodically - event.GlobalBind(event.Enter, func(_ event.CID, frames interface{}) int { - enterPayload := frames.(event.EnterPayload) + event.GlobalBind(ctx, event.Enter, func(enterPayload event.EnterPayload) event.Response { if enterPayload.FramesElapsed%EnemyRefresh == 0 { - go NewEnemy() + go NewEnemy(ctx) } return 0 }) @@ -166,8 +162,6 @@ func main() { } } - }, Loop: func() bool { - return playerAlive }}) oak.Init("tds", func(c oak.Config) (oak.Config, error) { @@ -184,7 +178,7 @@ const ( ) // NewEnemy creates an enemy for a top down shooter -func NewEnemy() { +func NewEnemy(ctx *scene.Context) { x, y := enemyPos() enemyFrame := sheet[0][0].Copy() @@ -200,8 +194,7 @@ func NewEnemy() { enemy.UpdateLabel(Enemy) - enemy.Bind(event.Enter, func(id event.CID, _ interface{}) int { - enemy := event.GetEntity(id).(*entities.Solid) + event.Bind(ctx, event.Enter, enemy, func(e *entities.Solid, ev event.EnterPayload) event.Response { // move towards the player x, y := enemy.GetPos() pt := floatgeom.Point2{x, y} @@ -223,9 +216,8 @@ func NewEnemy() { return 0 }) - enemy.Bind("Destroy", func(id event.CID, _ interface{}) int { - enemy := event.GetEntity(id).(*entities.Solid) - enemy.Destroy() + event.Bind(ctx, destroy, enemy, func(e *entities.Solid, nothing struct{}) event.Response { + e.Destroy() return 0 }) } diff --git a/examples/top-down-shooter-tutorial/6-performance/performance.go b/examples/top-down-shooter-tutorial/6-performance/performance.go index 7cfaead4..dfe1284e 100644 --- a/examples/top-down-shooter-tutorial/6-performance/performance.go +++ b/examples/top-down-shooter-tutorial/6-performance/performance.go @@ -28,7 +28,6 @@ const ( ) var ( - playerAlive = true // Vectors are backed by pointers, // so despite this not being a pointer, // this does update according to the player's @@ -36,6 +35,8 @@ var ( // the player's position vector playerPos physics.Vector + destroy = event.RegisterEvent[struct{}]() + sheet [][]*render.Sprite ) @@ -52,7 +53,6 @@ func main() { // render.Draw(debugtools.NewThickRTree(ctx, collision.DefaultTree, 5), 2, 3) // Initialization - playerAlive = true sprites, err := render.GetSheet("sheet.png") dlog.ErrorCheck(err) sheet = sprites.ToSprites() @@ -81,21 +81,18 @@ func main() { float64(ctx.Window.Height()) / 2, } - char.Bind(event.Enter, func(id event.CID, payload interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - - enterPayload := payload.(event.EnterPayload) + event.Bind(ctx, event.Enter, char, func(char *entities.Moving, ev event.EnterPayload) event.Response { if oak.IsDown(key.W) { - char.Delta.ShiftY(-char.Speed.Y() * enterPayload.TickPercent) + char.Delta.ShiftY(-char.Speed.Y() * ev.TickPercent) } if oak.IsDown(key.A) { - char.Delta.ShiftX(-char.Speed.X() * enterPayload.TickPercent) + char.Delta.ShiftX(-char.Speed.X() * ev.TickPercent) } if oak.IsDown(key.S) { - char.Delta.ShiftY(char.Speed.Y() * enterPayload.TickPercent) + char.Delta.ShiftY(char.Speed.Y() * ev.TickPercent) } if oak.IsDown(key.D) { - char.Delta.ShiftX(char.Speed.X() * enterPayload.TickPercent) + char.Delta.ShiftX(char.Speed.X() * ev.TickPercent) } ctx.Window.(*oak.Window).DoBetweenDraws(func() { char.ShiftPos(char.Delta.X(), char.Delta.Y()) @@ -119,7 +116,7 @@ func main() { hit := char.HitLabel(Enemy) if hit != nil { - playerAlive = false + ctx.Window.NextScene() } // update animation @@ -137,9 +134,7 @@ func main() { return 0 }) - char.Bind(mouse.Press, func(id event.CID, me interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - mevent := me.(*mouse.Event) + event.Bind(ctx, mouse.Press, char, func(char *entities.Moving, mevent *mouse.Event) event.Response { x := char.X() + char.W/2 y := char.Y() + char.H/2 vp := ctx.Window.Viewport() @@ -148,7 +143,7 @@ func main() { ray.DefaultCaster.CastDistance = floatgeom.Point2{x, y}.Sub(floatgeom.Point2{mx, my}).Magnitude() hits := ray.CastTo(floatgeom.Point2{x, y}, floatgeom.Point2{mx, my}) for _, hit := range hits { - hit.Zone.CID.Trigger("Destroy", nil) + event.TriggerForCallerOn(ctx, hit.Zone.CID, destroy, struct{}{}) } ctx.DrawForTime( render.NewLine(x, y, mx, my, color.RGBA{0, 128, 0, 128}), @@ -158,10 +153,9 @@ func main() { }) // Create enemies periodically - event.GlobalBind(event.Enter, func(_ event.CID, frames interface{}) int { - enterPayload := frames.(event.EnterPayload) + event.GlobalBind(ctx, event.Enter, func(enterPayload event.EnterPayload) event.Response { if enterPayload.FramesElapsed%EnemyRefresh == 0 { - go NewEnemy() + go NewEnemy(ctx) } return 0 }) @@ -177,8 +171,6 @@ func main() { } } - }, Loop: func() bool { - return playerAlive }}) render.SetDrawStack( @@ -206,7 +198,7 @@ const ( ) // NewEnemy creates an enemy for a top down shooter -func NewEnemy() { +func NewEnemy(ctx *scene.Context) { x, y := enemyPos() enemyFrame := sheet[0][0].Copy() @@ -222,14 +214,12 @@ func NewEnemy() { enemy.UpdateLabel(Enemy) - enemy.Bind(event.Enter, func(id event.CID, payload interface{}) int { - enemy := event.GetEntity(id).(*entities.Solid) - enterPayload := payload.(event.EnterPayload) + event.Bind(ctx, event.Enter, enemy, func(e *entities.Solid, ev event.EnterPayload) event.Response { // move towards the player x, y := enemy.GetPos() pt := floatgeom.Point2{x, y} pt2 := floatgeom.Point2{playerPos.X(), playerPos.Y()} - delta := pt2.Sub(pt).Normalize().MulConst(EnemySpeed * enterPayload.TickPercent) + delta := pt2.Sub(pt).Normalize().MulConst(EnemySpeed * ev.TickPercent) enemy.ShiftPos(delta.X(), delta.Y()) // update animation @@ -246,9 +236,8 @@ func NewEnemy() { return 0 }) - enemy.Bind("Destroy", func(id event.CID, _ interface{}) int { - enemy := event.GetEntity(id).(*entities.Solid) - enemy.Destroy() + event.Bind(ctx, destroy, enemy, func(e *entities.Solid, nothing struct{}) event.Response { + e.Destroy() return 0 }) } diff --git a/examples/zooming/main.go b/examples/zooming/main.go index c48ee22d..b28f831d 100644 --- a/examples/zooming/main.go +++ b/examples/zooming/main.go @@ -20,7 +20,7 @@ var ( ) func main() { - oak.AddScene("demo", scene.Scene{Start: func(*scene.Context) { + oak.AddScene("demo", scene.Scene{Start: func(ctx *scene.Context) { render.Draw(render.NewText("Controls: Arrow keys", 500, 440)) // Get an image that we will illustrate zooming with later @@ -39,7 +39,7 @@ func main() { render.Draw(zoomer) // To illustrate zooming allow for arrow keys to control the main zoomable renderable. - event.GlobalBind(event.Enter, func(i event.CID, _ interface{}) int { + event.GlobalBind(event.DefaultBus, event.Enter, func(event.EnterPayload) event.Response { if oak.IsDown(key.UpArrow) { zoomOutFactorY *= .98 } @@ -52,7 +52,6 @@ func main() { if oak.IsDown(key.LeftArrow) { zoomOutFactorX *= .98 } - return 0 }) diff --git a/go.mod b/go.mod index 535122e7..f8b12964 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,12 @@ module github.com/oakmound/oak/v3 -go 1.16 +go 1.18 require ( dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037 github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 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 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 @@ -22,3 +21,8 @@ require ( golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20220111092808-5a964db01320 ) + +require ( + github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d // indirect + golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect +) diff --git a/init.go b/init.go index f96ac657..d7aa2291 100644 --- a/init.go +++ b/init.go @@ -65,9 +65,6 @@ func (w *Window) Init(firstScene string, configOptions ...ConfigOption) error { if w.config.TrackInputChanges { trackJoystickChanges(w.eventHandler) } - if w.config.EventRefreshRate != 0 { - w.eventHandler.SetRefreshRate(time.Duration(w.config.EventRefreshRate)) - } if !w.config.SkipRNGSeed { // seed math/rand with time.Now, useful for minimal examples @@ -77,14 +74,7 @@ func (w *Window) Init(firstScene string, configOptions ...ConfigOption) error { overrideInit(w) - go w.sceneLoop(firstScene, w.config.TrackInputChanges) - if w.config.BatchLoad { - w.startupLoading = true - go func() { - w.loadAssets(w.config.Assets.ImagePath, w.config.Assets.AudioPath) - w.endLoad() - }() - } + go w.sceneLoop(firstScene, w.config.TrackInputChanges, w.config.BatchLoad) if w.config.EnableDebugConsole { go w.debugConsole(os.Stdin, os.Stdout) } diff --git a/inputLoop.go b/inputLoop.go index 1f7e814b..9ad0bfe1 100644 --- a/inputLoop.go +++ b/inputLoop.go @@ -1,6 +1,7 @@ package oak import ( + "github.com/oakmound/oak/v3/alg/intgeom" "github.com/oakmound/oak/v3/event" "github.com/oakmound/oak/v3/timing" @@ -13,6 +14,17 @@ import ( "golang.org/x/mobile/event/size" ) +var ( + // ViewportUpdate: Triggered when the position of of the viewport changes + ViewportUpdate = event.RegisterEvent[intgeom.Point2]() + // OnStop: Triggered when the engine is stopped. + OnStop = event.RegisterEvent[struct{}]() + // FocusGain: Triggered when the window gains focus + FocusGain = event.RegisterEvent[struct{}]() + // FocusLoss: Triggered when the window loses focus + FocusLoss = event.RegisterEvent[struct{}]() +) + func (w *Window) inputLoop() { for { switch e := w.windowControl.NextEvent().(type) { @@ -21,27 +33,25 @@ func (w *Window) inputLoop() { switch e.To { case lifecycle.StageDead: dlog.Info(dlog.WindowClosed) - // OnStop needs to be sent through TriggerBack, otherwise the - // program will close before the stop events get propagated. - <-w.eventHandler.TriggerBack(event.OnStop, nil) + <-event.TriggerOn(w.eventHandler, OnStop, struct{}{}) close(w.quitCh) return case lifecycle.StageFocused: w.inFocus = true // If you are in focused state, we don't care how you got there w.DrawTicker.Reset(timing.FPSToFrameDelay(w.DrawFrameRate)) - w.eventHandler.Trigger(event.FocusGain, nil) + event.TriggerOn(w.eventHandler, FocusGain, struct{}{}) case lifecycle.StageVisible: // If the last state was focused, this means the app is out of focus // otherwise, we're visible for the first time if e.From > e.To { w.inFocus = false w.DrawTicker.Reset(timing.FPSToFrameDelay(w.IdleDrawFrameRate)) - w.eventHandler.Trigger(event.FocusLoss, nil) + event.TriggerOn(w.eventHandler, FocusLoss, struct{}{}) } else { w.inFocus = true w.DrawTicker.Reset(timing.FPSToFrameDelay(w.DrawFrameRate)) - w.eventHandler.Trigger(event.FocusGain, nil) + event.TriggerOn(w.eventHandler, FocusGain, struct{}{}) } } // Send key events @@ -76,7 +86,7 @@ func (w *Window) inputLoop() { // Mouse events all receive an x, y, and button string. case mouse.Event: button := omouse.Button(e.Button) - eventName := omouse.GetEventName(e.Direction, e.Button) + ev := omouse.GetEvent(e.Direction, e.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 @@ -85,7 +95,7 @@ func (w *Window) inputLoop() { float64((((e.X - float32(w.windowRect.Min.X)) / float32(w.windowRect.Max.X-w.windowRect.Min.X)) * float32(w.ScreenWidth))), float64((((e.Y - float32(w.windowRect.Min.Y)) / float32(w.windowRect.Max.Y-w.windowRect.Min.Y)) * float32(w.ScreenHeight))), button, - eventName, + ev, ) w.TriggerMouseEvent(mevent) @@ -102,10 +112,9 @@ func (w *Window) inputLoop() { // From the perspective of the event handler this is indistinguishable // from a real keypress. func (w *Window) TriggerKeyDown(e okey.Event) { - k := e.Code.String()[4:] - w.SetDown(k) - w.eventHandler.Trigger(okey.Down, e) - w.eventHandler.Trigger(okey.Down+k, e) + w.State.SetDown(e.Code) + event.TriggerOn(w.eventHandler, okey.AnyDown, e) + event.TriggerOn(w.eventHandler, okey.Down(e.Code), e) } // TriggerKeyUp triggers a software-emulated key release. @@ -113,10 +122,9 @@ func (w *Window) TriggerKeyDown(e okey.Event) { // From the perspective of the event handler this is indistinguishable // from a real key release. func (w *Window) TriggerKeyUp(e okey.Event) { - k := e.Code.String()[4:] - w.SetUp(k) - w.eventHandler.Trigger(okey.Up, e) - w.eventHandler.Trigger(okey.Up+k, e) + w.State.SetUp(e.Code) + event.TriggerOn(w.eventHandler, okey.AnyUp, e) + event.TriggerOn(w.eventHandler, okey.Up(e.Code), e) } // TriggerKeyHeld triggers a software-emulated key hold signal. @@ -124,9 +132,8 @@ func (w *Window) TriggerKeyUp(e okey.Event) { // From the perspective of the event handler this is indistinguishable // from a real key hold signal. func (w *Window) TriggerKeyHeld(e okey.Event) { - k := e.Code.String()[4:] - w.eventHandler.Trigger(okey.Held, e) - w.eventHandler.Trigger(okey.Held+k, e) + event.TriggerOn(w.eventHandler, okey.AnyHeld, e) + event.TriggerOn(w.eventHandler, okey.Held(e.Code), e) } // TriggerMouseEvent triggers a software-emulated mouse event. @@ -136,12 +143,21 @@ func (w *Window) TriggerKeyHeld(e okey.Event) { func (w *Window) TriggerMouseEvent(mevent omouse.Event) { w.LastMouseEvent = mevent omouse.LastEvent = mevent - w.Propagate(mevent.Event+"On", mevent) - w.eventHandler.Trigger(mevent.Event, &mevent) + on, onOk := omouse.EventOn(mevent.EventType) + if onOk { + w.Propagate(on, mevent) + } + event.TriggerOn(w.eventHandler, mevent.EventType, &mevent) - relativeEvent := mevent - relativeEvent.Point2[0] += float64(w.viewPos[0]) - relativeEvent.Point2[1] += float64(w.viewPos[1]) - w.LastRelativeMouseEvent = relativeEvent - w.Propagate(relativeEvent.Event+"OnRelative", relativeEvent) + if onOk { + rel, ok := omouse.EventRelative(on) + if ok { + relativeEvent := mevent + relativeEvent.Point2[0] += float64(w.viewPos[0]) + relativeEvent.Point2[1] += float64(w.viewPos[1]) + w.LastRelativeMouseEvent = relativeEvent + + w.Propagate(rel, relativeEvent) + } + } } diff --git a/inputTracker.go b/inputTracker.go index f8cbef27..067b2455 100644 --- a/inputTracker.go +++ b/inputTracker.go @@ -12,33 +12,39 @@ import ( ) // InputType expresses some form of input to the engine to represent a player -type InputType = int32 +type InputType int32 + +// InputChange is triggered when the most recent input device changes (e.g. keyboard to joystick or vice versa) +var InputChange = event.RegisterEvent[InputType]() + +var trackingJoystickChange = event.RegisterEvent[struct{}]() // Supported Input Types const ( - InputKeyboardMouse InputType = iota - InputJoystick InputType = iota + InputKeyboard InputType = iota + InputMouse + InputJoystick ) func (w *Window) trackInputChanges() { - w.eventHandler.GlobalBind(key.Down, func(event.CID, interface{}) int { - old := atomic.SwapInt32(&w.mostRecentInput, InputKeyboardMouse) - if old != InputKeyboardMouse { - w.eventHandler.Trigger(event.InputChange, InputKeyboardMouse) + event.GlobalBind(w.eventHandler, key.AnyDown, func(key.Event) event.Response { + old := atomic.SwapInt32(&w.mostRecentInput, int32(InputKeyboard)) + if InputType(old) != InputKeyboard { + event.TriggerOn(w.eventHandler, InputChange, InputKeyboard) } return 0 }) - w.eventHandler.GlobalBind(mouse.Press, func(event.CID, interface{}) int { - old := atomic.SwapInt32(&w.mostRecentInput, InputKeyboardMouse) - if old != InputKeyboardMouse { - w.eventHandler.Trigger(event.InputChange, InputKeyboardMouse) + event.GlobalBind(w.eventHandler, mouse.Press, func(*mouse.Event) event.Response { + old := atomic.SwapInt32(&w.mostRecentInput, int32(InputMouse)) + if InputType(old) != InputMouse { + event.TriggerOn(w.eventHandler, InputChange, InputMouse) } return 0 }) - w.eventHandler.GlobalBind("Tracking"+joystick.Change, func(event.CID, interface{}) int { - old := atomic.SwapInt32(&w.mostRecentInput, InputJoystick) - if old != InputJoystick { - w.eventHandler.Trigger(event.InputChange, InputJoystick) + event.GlobalBind(w.eventHandler, trackingJoystickChange, func(struct{}) event.Response { + old := atomic.SwapInt32(&w.mostRecentInput, int32(InputMouse)) + if InputType(old) != InputJoystick { + event.TriggerOn(w.eventHandler, InputChange, InputJoystick) } return 0 }) @@ -48,8 +54,11 @@ type joyHandler struct { handler event.Handler } -func (jh *joyHandler) Trigger(ev string, state interface{}) { - jh.handler.Trigger("Tracking"+ev, state) +func (jh *joyHandler) Trigger(eventID event.UnsafeEventID, data interface{}) chan struct{} { + jh.handler.Trigger(trackingJoystickChange.UnsafeEventID, struct{}{}) + ch := make(chan struct{}) + close(ch) + return ch } func trackJoystickChanges(handler event.Handler) { diff --git a/inputTracker_test.go b/inputTracker_test.go index b8730c6c..322374e9 100644 --- a/inputTracker_test.go +++ b/inputTracker_test.go @@ -5,7 +5,6 @@ import ( "time" "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/joystick" "github.com/oakmound/oak/v3/key" "github.com/oakmound/oak/v3/mouse" "github.com/oakmound/oak/v3/scene" @@ -13,7 +12,7 @@ import ( func TestTrackInputChanges(t *testing.T) { c1 := NewWindow() - c1.SetLogicHandler(event.NewBus(nil)) + c1.SetLogicHandler(event.NewBus(event.NewCallerMap())) c1.AddScene("1", scene.Scene{}) go c1.Init("1", func(c Config) (Config, error) { c.TrackInputChanges = true @@ -21,10 +20,9 @@ func TestTrackInputChanges(t *testing.T) { }) time.Sleep(2 * time.Second) expectedType := new(InputType) - *expectedType = InputKeyboardMouse + *expectedType = InputKeyboard failed := false - c1.eventHandler.GlobalBind(event.InputChange, func(_ event.CID, payload interface{}) int { - mode := payload.(InputType) + event.GlobalBind(c1.eventHandler, InputChange, func(mode InputType) event.Response { if mode != *expectedType { failed = true } @@ -36,18 +34,19 @@ func TestTrackInputChanges(t *testing.T) { t.Fatalf("keyboard change failed") } *expectedType = InputJoystick - c1.eventHandler.Trigger("Tracking"+joystick.Change, &joystick.State{}) + event.TriggerOn(c1.eventHandler, trackingJoystickChange, struct{}{}) time.Sleep(2 * time.Second) if failed { t.Fatalf("joystick change failed") } - *expectedType = InputKeyboardMouse - c1.TriggerMouseEvent(mouse.Event{Event: mouse.Press}) + *expectedType = InputMouse + c1.TriggerMouseEvent(mouse.Event{EventType: mouse.Press}) time.Sleep(2 * time.Second) if failed { t.Fatalf("mouse change failed") } - c1.mostRecentInput = InputJoystick + *expectedType = InputKeyboard + c1.mostRecentInput = int32(InputJoystick) c1.TriggerKeyDown(key.Event{}) time.Sleep(2 * time.Second) if failed { diff --git a/joystick/joystick.go b/joystick/joystick.go index 4632bb35..1500dc15 100644 --- a/joystick/joystick.go +++ b/joystick/joystick.go @@ -4,9 +4,11 @@ package joystick import ( "math" + "sync" "time" "github.com/oakmound/oak/v3/dlog" + "github.com/oakmound/oak/v3/event" ) type Input string @@ -29,16 +31,17 @@ const ( InputRightStick Input = "RightStick" ) -// Events. All events include a *State payload. -const ( - Change = "JoystickChange" - ButtonDown = "ButtonDown" - ButtonUp = "ButtonUp" - RtTriggerChange = "RtTriggerChange" - LtTriggerChange = "LtTriggerChange" - RtStickChange = "RtStickChange" - LtStickChange = "LtStickChange" - Disconnected = "JoystickDisconnected" +// Events. All events but Disconnected include a *State payload. +var ( + Change = event.RegisterEvent[*State]() + ButtonDown = event.RegisterEvent[*State]() + ButtonUp = event.RegisterEvent[*State]() + RtTriggerChange = event.RegisterEvent[*State]() + LtTriggerChange = event.RegisterEvent[*State]() + RtStickChange = event.RegisterEvent[*State]() + LtStickChange = event.RegisterEvent[*State]() + // Disconnected includes the ID of the joystick that disconnected. + Disconnected = event.RegisterEvent[uint32]() ) // Init calls any os functions necessary to detect joysticks @@ -49,7 +52,7 @@ func Init() error { // A Triggerer can either be an event bus or event CID, allowing // joystick triggers to be listened to globally or sent to particular entities. type Triggerer interface { - Trigger(string, interface{}) + Trigger(eventID event.UnsafeEventID, data interface{}) chan struct{} } // A Joystick represents a (usually) physical controller connected to the machine. @@ -121,7 +124,7 @@ func (lo *ListenOptions) sendFn() func(Triggerer, *State, *State) { var fn func(Triggerer, *State, *State) if lo.JoystickChanges { fn = func(h Triggerer, cur, last *State) { - h.Trigger(Change, cur) + h.Trigger(Change.UnsafeEventID, cur) } } if lo.GenericButtonPresses { @@ -134,13 +137,13 @@ func (lo *ListenOptions) sendFn() func(Triggerer, *State, *State) { for k, v := range cur.Buttons { if v != last.Buttons[k] { if v && !downTriggered { - h.Trigger(ButtonDown, cur) + h.Trigger(ButtonDown.UnsafeEventID, cur) downTriggered = true if upTriggered { return } } else if !v && !upTriggered { - h.Trigger(ButtonUp, cur) + h.Trigger(ButtonUp.UnsafeEventID, cur) upTriggered = true if downTriggered { return @@ -156,13 +159,13 @@ func (lo *ListenOptions) sendFn() func(Triggerer, *State, *State) { for k, v := range cur.Buttons { if v != last.Buttons[k] { if v && !downTriggered { - h.Trigger(ButtonDown, cur) + h.Trigger(ButtonDown.UnsafeEventID, cur) downTriggered = true if upTriggered { return } } else if !v && !upTriggered { - h.Trigger(ButtonUp, cur) + h.Trigger(ButtonUp.UnsafeEventID, cur) upTriggered = true if downTriggered { return @@ -181,9 +184,9 @@ func (lo *ListenOptions) sendFn() func(Triggerer, *State, *State) { for k, v := range cur.Buttons { if v != last.Buttons[k] { if v { - h.Trigger(k+ButtonDown, cur) + h.Trigger(Down(k).UnsafeEventID, cur) } else { - h.Trigger(k+ButtonUp, cur) + h.Trigger(Up(k).UnsafeEventID, cur) } } } @@ -193,9 +196,9 @@ func (lo *ListenOptions) sendFn() func(Triggerer, *State, *State) { for k, v := range cur.Buttons { if v != last.Buttons[k] { if v { - h.Trigger(k+ButtonDown, cur) + h.Trigger(Down(k).UnsafeEventID, cur) } else { - h.Trigger(k+ButtonUp, cur) + h.Trigger(Up(k).UnsafeEventID, cur) } } } @@ -209,22 +212,22 @@ func (lo *ListenOptions) sendFn() func(Triggerer, *State, *State) { prevFn(h, cur, last) if deltaExceedsThreshold(cur.StickLX, last.StickLX, lo.StickDeadzoneLX) || deltaExceedsThreshold(cur.StickLY, last.StickLY, lo.StickDeadzoneLY) { - h.Trigger(LtStickChange, cur) + h.Trigger(LtStickChange.UnsafeEventID, cur) } if deltaExceedsThreshold(cur.StickRX, last.StickRX, lo.StickDeadzoneRX) || deltaExceedsThreshold(cur.StickRY, last.StickRY, lo.StickDeadzoneRY) { - h.Trigger(RtStickChange, cur) + h.Trigger(RtStickChange.UnsafeEventID, cur) } } } else { fn = func(h Triggerer, cur, last *State) { if deltaExceedsThreshold(cur.StickLX, last.StickLX, lo.StickDeadzoneLX) || deltaExceedsThreshold(cur.StickLY, last.StickLY, lo.StickDeadzoneLY) { - h.Trigger(LtStickChange, cur) + h.Trigger(LtStickChange.UnsafeEventID, cur) } if deltaExceedsThreshold(cur.StickRX, last.StickRX, lo.StickDeadzoneRX) || deltaExceedsThreshold(cur.StickRY, last.StickRY, lo.StickDeadzoneRY) { - h.Trigger(RtStickChange, cur) + h.Trigger(RtStickChange.UnsafeEventID, cur) } } } @@ -235,19 +238,19 @@ func (lo *ListenOptions) sendFn() func(Triggerer, *State, *State) { fn = func(h Triggerer, cur, last *State) { prevFn(h, cur, last) if cur.TriggerL != last.TriggerL { - h.Trigger(LtTriggerChange, cur) + h.Trigger(LtTriggerChange.UnsafeEventID, cur) } if cur.TriggerR != last.TriggerR { - h.Trigger(RtTriggerChange, cur) + h.Trigger(RtTriggerChange.UnsafeEventID, cur) } } } else { fn = func(h Triggerer, cur, last *State) { if cur.TriggerL != last.TriggerL { - h.Trigger(LtTriggerChange, cur) + h.Trigger(LtTriggerChange.UnsafeEventID, cur) } if cur.TriggerR != last.TriggerR { - h.Trigger(RtTriggerChange, cur) + h.Trigger(RtTriggerChange.UnsafeEventID, cur) } } } @@ -255,6 +258,34 @@ func (lo *ListenOptions) sendFn() func(Triggerer, *State, *State) { return fn } +var upEventsLock sync.Mutex +var upEvents = map[string]event.EventID[*State]{} + +func Up(s string) event.EventID[*State] { + upEventsLock.Lock() + defer upEventsLock.Unlock() + if ev, ok := upEvents[s]; ok { + return ev + } + ev := event.RegisterEvent[*State]() + upEvents[s] = ev + return ev +} + +var downEventsLock sync.Mutex +var downEvents = map[string]event.EventID[*State]{} + +func Down(s string) event.EventID[*State] { + downEventsLock.Lock() + defer downEventsLock.Unlock() + if ev, ok := downEvents[s]; ok { + return ev + } + ev := event.RegisterEvent[*State]() + downEvents[s] = ev + return ev +} + func deltaExceedsThreshold(old, new, threshold int16) bool { return intAbs(old-new) > threshold } @@ -306,7 +337,7 @@ func (j *Joystick) Listen(opts *ListenOptions) (cancel func()) { } state, err := j.GetState() if err != nil { - j.Handler.Trigger(Disconnected, j.id) + j.Handler.Trigger(Disconnected.UnsafeEventID, j.id) dlog.Error(err) t.Stop() j.Close() diff --git a/key/events.go b/key/events.go index ec96bd36..cfbd06f6 100644 --- a/key/events.go +++ b/key/events.go @@ -1,38 +1,83 @@ package key import ( + "sync" + "github.com/oakmound/oak/v3/event" "golang.org/x/mobile/event/key" ) -const ( +var ( // Down is sent when a key is pressed. It is sent both as // Down, and as Down + the key name. - Down = "KeyDown" + AnyDown = event.RegisterEvent[Event]() // Up is sent when a key is released. It is sent both as // Up, and as Up + the key name. - Up = "KeyUp" + AnyUp = event.RegisterEvent[Event]() // Held is sent when a key is held down. It is sent both as // Held, and as Held + the key name. - Held = "KeyHeld" + AnyHeld = event.RegisterEvent[Event]() ) // An Event is sent as the payload for all key bindings. -type Event = key.Event - -// A code is a unique integer code for a given common key -type Code = key.Code - -// Binding will convert a function that accepts a typecast key.Event into a generic event binding -// -// Example: -// bus.Bind(key.Down, key.Binding(keyHandler)) -func Binding(fn func(event.CID, Event) int) func(event.CID, interface{}) int { - return func(cid event.CID, iface interface{}) int { - ke, ok := iface.(Event) - if !ok { - return event.UnbindSingle - } - return fn(cid, ke) +type Event key.Event + +type Modifiers = key.Modifiers + +const ( + ModShift Modifiers = 1 << 0 + ModControl Modifiers = 1 << 1 + ModAlt Modifiers = 1 << 2 + ModMeta Modifiers = 1 << 3 // called "Command" on OS X +) + +type Direction = key.Direction + +const ( + DirNone Direction = 0 + DirPress Direction = 1 + DirRelease Direction = 2 +) + +var upEventsLock sync.Mutex +var upEvents = map[Code]event.EventID[Event]{} + +// Up checks +func Up(code Code) event.EventID[Event] { + upEventsLock.Lock() + defer upEventsLock.Unlock() + if ev, ok := upEvents[code]; ok { + return ev + } + ev := event.RegisterEvent[Event]() + upEvents[code] = ev + return ev +} + +var downEventsLock sync.Mutex +var downEvents = map[Code]event.EventID[Event]{} + +func Down(code Code) event.EventID[Event] { + downEventsLock.Lock() + defer downEventsLock.Unlock() + if ev, ok := downEvents[code]; ok { + return ev + } + ev := event.RegisterEvent[Event]() + downEvents[code] = ev + return ev +} + +var heldEventsLock sync.Mutex +var heldEvents = map[Code]event.EventID[Event]{} + +func Held(code Code) event.EventID[Event] { + heldEventsLock.Lock() + defer heldEventsLock.Unlock() + if ev, ok := heldEvents[code]; ok { + return ev } + ev := event.RegisterEvent[Event]() + heldEvents[code] = ev + return ev } diff --git a/key/keycodes.go b/key/keycodes.go new file mode 100644 index 00000000..4c06915a --- /dev/null +++ b/key/keycodes.go @@ -0,0 +1,141 @@ +package key + +import "golang.org/x/mobile/event/key" + +// Code is the identity of a key relative to a notional "standard" keyboard. +// It is a straight copy of mobile package's key codes cleaned up for ease of binding in oak. +// See AllKeys for string mappers. +type Code = key.Code + +const ( + Unknown Code = 0 + A Code = 4 + B Code = 5 + C Code = 6 + D Code = 7 + E Code = 8 + F Code = 9 + G Code = 10 + H Code = 11 + I Code = 12 + J Code = 13 + K Code = 14 + L Code = 15 + M Code = 16 + N Code = 17 + O Code = 18 + P Code = 19 + Q Code = 20 + R Code = 21 + S Code = 22 + T Code = 23 + U Code = 24 + V Code = 25 + W Code = 26 + X Code = 27 + Y Code = 28 + Z Code = 29 + + Num1 Code = 30 + Num2 Code = 31 + Num3 Code = 32 + Num4 Code = 33 + Num5 Code = 34 + Num6 Code = 35 + Num7 Code = 36 + Num8 Code = 37 + Num9 Code = 38 + Num0 Code = 39 + + ReturnEnter Code = 40 + Escape Code = 41 + DeleteBackspace Code = 42 + Tab Code = 43 + Spacebar Code = 44 + HyphenMinus Code = 45 + EqualSign Code = 46 + LeftSquareBracket Code = 47 + RightSquareBracket Code = 48 + Backslash Code = 49 + Semicolon Code = 51 + Apostrophe Code = 52 + GraveAccent Code = 53 + Comma Code = 54 + FullStop Code = 55 + Slash Code = 56 + CapsLock Code = 57 + + F1 Code = 58 + F2 Code = 59 + F3 Code = 60 + F4 Code = 61 + F5 Code = 62 + F6 Code = 63 + F7 Code = 64 + F8 Code = 65 + F9 Code = 66 + F10 Code = 67 + F11 Code = 68 + F12 Code = 69 + + Pause Code = 72 + Insert Code = 73 + Home Code = 74 + PageUp Code = 75 + DeleteForward Code = 76 + End Code = 77 + PageDown Code = 78 + + RightArrow Code = 79 + LeftArrow Code = 80 + DownArrow Code = 81 + UpArrow Code = 82 + + KeypadNumLock Code = 83 + KeypadSlash Code = 84 + KeypadAsterisk Code = 85 + KeypadHyphenMinus Code = 86 + KeypadPlusSign Code = 87 + KeypadEnter Code = 88 + Keypad1 Code = 89 + Keypad2 Code = 90 + Keypad3 Code = 91 + Keypad4 Code = 92 + Keypad5 Code = 93 + Keypad6 Code = 94 + Keypad7 Code = 95 + Keypad8 Code = 96 + Keypad9 Code = 97 + Keypad0 Code = 98 + KeypadFullStop Code = 99 + KeypadEqualSign Code = 103 + + F13 Code = 104 + F14 Code = 105 + F15 Code = 106 + F16 Code = 107 + F17 Code = 108 + F18 Code = 109 + F19 Code = 110 + F20 Code = 111 + F21 Code = 112 + F22 Code = 113 + F23 Code = 114 + F24 Code = 115 + + Help Code = 117 + + Mute Code = 127 + VolumeUp Code = 128 + VolumeDown Code = 129 + + LeftControl Code = 224 + LeftShift Code = 225 + LeftAlt Code = 226 + LeftGUI Code = 227 + RightControl Code = 228 + RightShift Code = 229 + RightAlt Code = 230 + RightGUI Code = 231 + Compose Code = 0x10000 +) diff --git a/key/keys.go b/key/keys.go index 1cb73fc9..64bb3d95 100644 --- a/key/keys.go +++ b/key/keys.go @@ -1,277 +1,135 @@ package key -// This lists the keys sent through oak's input events. -// This list is not used internally by oak, but was generated from -// the expected output from x/mobile/key. -// -// These strings are sent as payloads to Key.Down and Key.Up events, -// and through "KeyDown"+$a, "KeyUp"+$a for any $a in the const. -const ( - Unknown = "Unknown" - - A = "A" - B = "B" - C = "C" - D = "D" - E = "E" - F = "F" - G = "G" - H = "H" - I = "I" - J = "J" - K = "K" - L = "L" - M = "M" - N = "N" - O = "O" - P = "P" - Q = "Q" - R = "R" - S = "S" - T = "T" - U = "U" - V = "V" - W = "W" - X = "X" - Y = "Y" - Z = "Z" - - One = "1" - Two = "2" - Three = "3" - Four = "4" - Five = "5" - Six = "6" - Seven = "7" - Eight = "8" - Nine = "9" - Zero = "0" - - ReturnEnter = "ReturnEnter" - Enter = ReturnEnter - Escape = "Escape" - DeleteBackspace = "DeleteBackspace" - Tab = "Tab" - Spacebar = "Spacebar" - HyphenMinus = "HyphenMinus" //- - EqualSign = "EqualSign" //= - LeftSquareBracket = "LeftSquareBracket" //[ - RightSquareBracket = "RightSquareBracket" //] - Backslash = "Backslash" //\ - Semicolon = "Semicolon" //; - Apostrophe = "Apostrophe" //' - GraveAccent = "GraveAccent" //` - Comma = "Comma" //, - FullStop = "FullStop" //. - Period = FullStop - Slash = "Slash" /// - CapsLock = "CapsLock" - - F1 = "F1" - F2 = "F2" - F3 = "F3" - F4 = "F4" - F5 = "F5" - F6 = "F6" - F7 = "F7" - F8 = "F8" - F9 = "F9" - F10 = "F10" - F11 = "F11" - F12 = "F12" - - Pause = "Pause" - Insert = "Insert" - Home = "Home" - PageUp = "PageUp" - DeleteForward = "DeleteForward" - End = "End" - PageDown = "PageDown" - - RightArrow = "RightArrow" - LeftArrow = "LeftArrow" - DownArrow = "DownArrow" - UpArrow = "UpArrow" - - KeypadNumLock = "KeypadNumLock" - KeypadSlash = "KeypadSlash" /// - KeypadAsterisk = "KeypadAsterisk" //* - KeypadHyphenMinus = "KeypadHyphenMinus" //- - KeypadPlusSign = "KeypadPlusSign" //+ - KeypadEnter = "KeypadEnter" - Keypad1 = "Keypad1" - Keypad2 = "Keypad2" - Keypad3 = "Keypad3" - Keypad4 = "Keypad4" - Keypad5 = "Keypad5" - Keypad6 = "Keypad6" - Keypad7 = "Keypad7" - Keypad8 = "Keypad8" - Keypad9 = "Keypad9" - Keypad0 = "Keypad0" - KeypadFullStop = "KeypadFullStop" //. - KeypadPeriod = KeypadFullStop - KeypadEqualSign = "KeypadEqualSign" //= - - F13 = "F13" - F14 = "F14" - F15 = "F15" - F16 = "F16" - F17 = "F17" - F18 = "F18" - F19 = "F19" - F20 = "F20" - F21 = "F21" - F22 = "F22" - F23 = "F23" - F24 = "F24" - - Help = "Help" - - Mute = "Mute" - VolumeUp = "VolumeUp" - VolumeDown = "VolumeDown" - - LeftControl = "LeftControl" - LeftShift = "LeftShift" - LeftAlt = "LeftAlt" - LeftGUI = "LeftGUI" - RightControl = "RightControl" - RightShift = "RightShift" - RightAlt = "RightAlt" - RightGUI = "RightGUI" -) - // AllKeys is the set of all defined key codes -var AllKeys = map[string]struct{}{ - Unknown: {}, - - A: {}, - B: {}, - C: {}, - D: {}, - E: {}, - F: {}, - G: {}, - H: {}, - I: {}, - J: {}, - K: {}, - L: {}, - M: {}, - N: {}, - O: {}, - P: {}, - Q: {}, - R: {}, - S: {}, - T: {}, - U: {}, - V: {}, - W: {}, - X: {}, - Y: {}, - Z: {}, - - One: {}, - Two: {}, - Three: {}, - Four: {}, - Five: {}, - Six: {}, - Seven: {}, - Eight: {}, - Nine: {}, - Zero: {}, - - ReturnEnter: {}, - Escape: {}, - DeleteBackspace: {}, - Tab: {}, - Spacebar: {}, - HyphenMinus: {}, - EqualSign: {}, - LeftSquareBracket: {}, - RightSquareBracket: {}, - Backslash: {}, - Semicolon: {}, - Apostrophe: {}, - GraveAccent: {}, - Comma: {}, - FullStop: {}, - Slash: {}, - CapsLock: {}, - - F1: {}, - F2: {}, - F3: {}, - F4: {}, - F5: {}, - F6: {}, - F7: {}, - F8: {}, - F9: {}, - F10: {}, - F11: {}, - F12: {}, - - Pause: {}, - Insert: {}, - Home: {}, - PageUp: {}, - DeleteForward: {}, - End: {}, - PageDown: {}, - - RightArrow: {}, - LeftArrow: {}, - DownArrow: {}, - UpArrow: {}, - - KeypadNumLock: {}, - KeypadSlash: {}, - KeypadAsterisk: {}, - KeypadHyphenMinus: {}, - KeypadPlusSign: {}, - KeypadEnter: {}, - Keypad1: {}, - Keypad2: {}, - Keypad3: {}, - Keypad4: {}, - Keypad5: {}, - Keypad6: {}, - Keypad7: {}, - Keypad8: {}, - Keypad9: {}, - Keypad0: {}, - KeypadFullStop: {}, - KeypadEqualSign: {}, - - F13: {}, - F14: {}, - F15: {}, - F16: {}, - F17: {}, - F18: {}, - F19: {}, - F20: {}, - F21: {}, - F22: {}, - F23: {}, - F24: {}, - - Help: {}, - - Mute: {}, - VolumeUp: {}, - VolumeDown: {}, - - LeftControl: {}, - LeftShift: {}, - LeftAlt: {}, - LeftGUI: {}, - RightControl: {}, - RightShift: {}, - RightAlt: {}, - RightGUI: {}, +var AllKeys = map[Code]string{ + Unknown: "Unknown", + + A: "A", + B: "B", + C: "C", + D: "D", + E: "E", + F: "F", + G: "G", + H: "H", + I: "I", + J: "J", + K: "K", + L: "L", + M: "M", + N: "N", + O: "O", + P: "P", + Q: "Q", + R: "R", + S: "S", + T: "T", + U: "U", + V: "V", + W: "W", + X: "X", + Y: "Y", + Z: "Z", + + Num1: "1", + Num2: "2", + Num3: "3", + Num4: "4", + Num5: "5", + Num6: "6", + Num7: "7", + Num8: "8", + Num9: "9", + Num0: "0", + + ReturnEnter: "ReturnEnter", + Escape: "Escape", + DeleteBackspace: "DeleteBackspace", + Tab: "Tab", + Spacebar: "Spacebar", + HyphenMinus: "HyphenMinus", + EqualSign: "EqualSign", + LeftSquareBracket: "LeftSquareBracket", + RightSquareBracket: "RightSquareBracket", + Backslash: "Backslash", + Semicolon: "Semicolon", + Apostrophe: "Apostrophe", + GraveAccent: "GraveAccent", + Comma: "Comma", + FullStop: "FullStop", + Slash: "Slash", + CapsLock: "CapsLock", + + F1: "F1", + F2: "F2", + F3: "F3", + F4: "F4", + F5: "F5", + F6: "F6", + F7: "F7", + F8: "F8", + F9: "F9", + F10: "F10", + F11: "F11", + F12: "F12", + + Pause: "Pause", + Insert: "Insert", + Home: "Home", + PageUp: "PageUp", + DeleteForward: "DeleteForward", + End: "End", + PageDown: "PageDown", + + RightArrow: "RightArrow", + LeftArrow: "LeftArrow", + DownArrow: "DownArrow", + UpArrow: "UpArrow", + + KeypadNumLock: "KeypadNumLock", + KeypadSlash: "KeypadSlash", + KeypadAsterisk: "KeypadAsterisk", + KeypadHyphenMinus: "KeypadHyphenMinus", + KeypadPlusSign: "KeypadPlusSign", + KeypadEnter: "KeypadEnter", + Keypad1: "Keypad1", + Keypad2: "Keypad2", + Keypad3: "Keypad3", + Keypad4: "Keypad4", + Keypad5: "Keypad5", + Keypad6: "Keypad6", + Keypad7: "Keypad7", + Keypad8: "Keypad8", + Keypad9: "Keypad9", + Keypad0: "Keypad0", + KeypadFullStop: "KeypadFullStop", + KeypadEqualSign: "KeypadEqualSign", + + F13: "F13", + F14: "F14", + F15: "F15", + F16: "F16", + F17: "F17", + F18: "F18", + F19: "F19", + F20: "F20", + F21: "F21", + F22: "F22", + F23: "F23", + F24: "F24", + + Help: "Help", + + Mute: "Mute", + VolumeUp: "VolumeUp", + VolumeDown: "VolumeDown", + + LeftControl: "LeftControl", + LeftShift: "LeftShift", + LeftAlt: "LeftAlt", + LeftGUI: "LeftGUI", + RightControl: "RightControl", + RightShift: "RightShift", + RightAlt: "RightAlt", + RightGUI: "RightGUI", } diff --git a/key/state.go b/key/state.go index 428d955c..0165b0fc 100644 --- a/key/state.go +++ b/key/state.go @@ -8,8 +8,8 @@ import ( // A State tracks what keys of a keyboard are currently pressed and for how long they have been // pressed if they are held down. type State struct { - state map[string]bool - durations map[string]time.Time + state map[Code]bool + durations map[Code]time.Time stateLock sync.RWMutex durationLock sync.RWMutex } @@ -17,8 +17,8 @@ type State struct { // NewState creates a state object for tracking keyboard state. func NewState() State { return State{ - state: make(map[string]bool), - durations: make(map[string]time.Time), + state: make(map[Code]bool), + durations: make(map[Code]time.Time), } } @@ -27,7 +27,7 @@ func NewState() State { // 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 (ks *State) SetUp(key string) { +func (ks *State) SetUp(key Code) { ks.stateLock.Lock() ks.durationLock.Lock() delete(ks.state, key) @@ -41,7 +41,7 @@ func (ks *State) SetUp(key string) { // 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 (ks *State) SetDown(key string) { +func (ks *State) SetDown(key Code) { ks.stateLock.Lock() ks.state[key] = true ks.durations[key] = time.Now() @@ -49,7 +49,7 @@ func (ks *State) SetDown(key string) { } // IsDown returns whether a key is held down -func (ks *State) IsDown(key string) (k bool) { +func (ks *State) IsDown(key Code) (k bool) { ks.stateLock.RLock() k = ks.state[key] ks.stateLock.RUnlock() @@ -58,7 +58,7 @@ func (ks *State) IsDown(key string) (k bool) { // IsHeld returns whether a key is held down, and for how long // it has been held. -func (ks *State) IsHeld(key string) (k bool, d time.Duration) { +func (ks *State) IsHeld(key Code) (k bool, d time.Duration) { ks.stateLock.RLock() k = ks.state[key] ks.stateLock.RUnlock() diff --git a/key/state_test.go b/key/state_test.go index 82b11cbb..a9d5cc70 100644 --- a/key/state_test.go +++ b/key/state_test.go @@ -7,29 +7,29 @@ import ( func TestState(t *testing.T) { ks := NewState() - ks.SetDown("Test") - if !ks.IsDown("Test") { - t.Fatalf("test was not set down") + ks.SetDown(A) + if !ks.IsDown(A) { + t.Fatalf("a was not set down") } - ks.SetUp("Test") - if ks.IsDown("Test") { - t.Fatalf("test was not set up") + ks.SetUp(A) + if ks.IsDown(A) { + t.Fatalf("a was not set up") } - ks.SetDown("Test") + ks.SetDown(A) time.Sleep(2 * time.Second) - ok, d := ks.IsHeld("Test") + ok, d := ks.IsHeld(A) if !ok { - t.Fatalf("test was not held down") + t.Fatalf("a was not held down") } if d < 2000*time.Millisecond { - t.Fatalf("test was not held down for sleep length") + t.Fatalf("a was not held down for sleep length") } - ks.SetUp("Test") - ok, d = ks.IsHeld("Test") + ks.SetUp(A) + ok, d = ks.IsHeld(A) if ok { - t.Fatalf("test was not released") + t.Fatalf("a was not released") } if d != 0 { - t.Fatalf("test hold was not reset") + t.Fatalf("a hold was not reset") } } diff --git a/lifecycle.go b/lifecycle.go index 795a0860..49511503 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -77,9 +77,7 @@ func (w *Window) SetAspectRatio(xToY float64) { // ChangeWindow sets the width and height of the game window. Although exported, // calling it without a size event will probably not act as expected. func (w *Window) ChangeWindow(width, height int) error { - // Draw a black frame to cover up smears - // Todo: could restrict the black to -just- the area not covered by the - // scaled screen buffer + // Draw the background to cover up smears buff, err := w.screenControl.NewImage(image.Point{width, height}) if err == nil { draw.Draw(buff.RGBA(), buff.Bounds(), w.bkgFn(), zeroPoint, draw.Src) diff --git a/loading.go b/loading.go index 7293da61..70c68e8c 100644 --- a/loading.go +++ b/loading.go @@ -38,7 +38,7 @@ func (w *Window) loadAssets(imageDir, audioDir string) { func (w *Window) endLoad() { dlog.Verb("Done Loading") - w.startupLoading = false + w.NextScene() } // SetFS updates all calls oak or oak's subpackages will make to read from the given filesystem. diff --git a/mouse/bindings.go b/mouse/bindings.go deleted file mode 100644 index cedfe8dd..00000000 --- a/mouse/bindings.go +++ /dev/null @@ -1,17 +0,0 @@ -package mouse - -import "github.com/oakmound/oak/v3/event" - -// Binding will convert a function that accepts a typecast *mouse.Event into a generic event binding -// -// Example: -// bus.Bind(mouse.ClickOn, mouse.Binding(clickHandler)) -func Binding(fn func(event.CID, *Event) int) func(event.CID, interface{}) int { - return func(cid event.CID, iface interface{}) int { - me, ok := iface.(*Event) - if !ok { - return event.UnbindSingle - } - return fn(cid, me) - } -} diff --git a/mouse/event.go b/mouse/event.go index c185bc7d..b17cbf06 100644 --- a/mouse/event.go +++ b/mouse/event.go @@ -3,17 +3,17 @@ package mouse import ( "github.com/oakmound/oak/v3/alg/floatgeom" "github.com/oakmound/oak/v3/collision" + "github.com/oakmound/oak/v3/event" ) var ( // LastEvent is the last triggered mouse event, // tracked for continuous mouse responsiveness on events // that don't take in a mouse event - LastEvent = NewZeroEvent(0, 0) + LastEvent = Event{} // LastPress is the last triggered mouse event, // where the mouse event was a press. - // If TrackMouseClicks is set to false then this will not be tracked - LastPress = NewZeroEvent(0, 0) + LastPress = Event{} ) // An Event is passed in through all Mouse related event bindings to @@ -22,7 +22,7 @@ var ( type Event struct { floatgeom.Point2 Button - Event string + EventType event.EventID[*Event] // Set StopPropagation on a mouse event to prevent it from triggering on // lower layers of mouse collision spaces while in flight @@ -30,19 +30,14 @@ type Event struct { } // NewEvent creates an event. -func NewEvent(x, y float64, button Button, event string) Event { +func NewEvent(x, y float64, button Button, ev event.EventID[*Event]) Event { return Event{ - Point2: floatgeom.Point2{x, y}, - Button: button, - Event: event, + Point2: floatgeom.Point2{x, y}, + Button: button, + EventType: ev, } } -// NewZeroEvent creates an event with no button or event name. -func NewZeroEvent(x, y float64) Event { - return NewEvent(x, y, ButtonNone, "") -} - // ToSpace converts a mouse event into a collision space func (e Event) ToSpace() *collision.Space { sp := collision.NewUnassignedSpace(e.X(), e.Y(), 0.1, 0.1) diff --git a/mouse/event_test.go b/mouse/event_test.go index 145bfcd7..e77c2f37 100644 --- a/mouse/event_test.go +++ b/mouse/event_test.go @@ -7,7 +7,7 @@ import ( ) func TestEventConversions(t *testing.T) { - me := NewZeroEvent(1.0, 1.0) + me := NewEvent(1.0, 1.0, ButtonLeft, Drag) s := me.ToSpace() Add(collision.NewUnassignedSpace(1.0, 1.0, .1, .1)) if len(Hits(s)) == 0 { diff --git a/mouse/events.go b/mouse/events.go new file mode 100644 index 00000000..8ed4832f --- /dev/null +++ b/mouse/events.go @@ -0,0 +1,77 @@ +package mouse + +import "github.com/oakmound/oak/v3/event" + +var ( + // Press is triggered when a mouse key is pressed down + Press = event.RegisterEvent[*Event]() + // Release is triggered when a mouse key, pressed, is released + Release = event.RegisterEvent[*Event]() + // ScrollDown is triggered when a mouse's scroll wheel scrolls downward + ScrollDown = event.RegisterEvent[*Event]() + // ScrollUp is triggered when a mouse's scroll wheel scrolls upward + ScrollUp = event.RegisterEvent[*Event]() + // Click is triggered when a Release follows a press for the same mouse key without + // other mouse key presses intertwining. + Click = event.RegisterEvent[*Event]() + // Drag is triggered when the mouse is moved. + Drag = event.RegisterEvent[*Event]() + + // The 'On' Variants of all mouse events are triggered when a mouse event occurs on + // a specific entity in a mouse collision tree. + PressOn = event.RegisterEvent[*Event]() + ReleaseOn = event.RegisterEvent[*Event]() + ScrollDownOn = event.RegisterEvent[*Event]() + ScrollUpOn = event.RegisterEvent[*Event]() + ClickOn = event.RegisterEvent[*Event]() + DragOn = event.RegisterEvent[*Event]() + + // Relative variants are like 'On' variants, but their mouse position data is relative to + // the window's current viewport. E.g. if the viewport is at 100,100 and a click happens at + // 100,100 on the window-- Relative will report 100,100, and non-relative will report 200,200. + // TODO: re-evaluate relative vs non-relative mouse events + RelativePressOn = event.RegisterEvent[*Event]() + RelativeReleaseOn = event.RegisterEvent[*Event]() + RelativeScrollDownOn = event.RegisterEvent[*Event]() + RelativeScrollUpOn = event.RegisterEvent[*Event]() + RelativeClickOn = event.RegisterEvent[*Event]() + RelativeDragOn = event.RegisterEvent[*Event]() +) + +// EventOn converts a generic positioned mouse event into its variant indicating +// it occurred on a CallerID targetted entity +func EventOn(ev event.EventID[*Event]) (event.EventID[*Event], bool) { + switch ev { + case Press: + return PressOn, true + case Release: + return ReleaseOn, true + case ScrollDown: + return ScrollDownOn, true + case ScrollUp: + return ScrollUpOn, true + case Click: + return ClickOn, true + case Drag: + return DragOn, true + } + return event.EventID[*Event]{}, false +} + +func EventRelative(ev event.EventID[*Event]) (event.EventID[*Event], bool) { + switch ev { + case PressOn: + return RelativePressOn, true + case ReleaseOn: + return RelativeReleaseOn, true + case ScrollDownOn: + return RelativeScrollDownOn, true + case ScrollUpOn: + return RelativeScrollUpOn, true + case ClickOn: + return RelativeClickOn, true + case DragOn: + return RelativeDragOn, true + } + return event.EventID[*Event]{}, false +} diff --git a/mouse/mouse.go b/mouse/mouse.go index 7fcd47c1..5caa8498 100644 --- a/mouse/mouse.go +++ b/mouse/mouse.go @@ -1,6 +1,7 @@ package mouse import ( + "github.com/oakmound/oak/v3/event" "golang.org/x/mobile/event/mouse" ) @@ -21,7 +22,7 @@ const ( ) // GetEventName returns a string event name given some mobile/mouse information -func GetEventName(d mouse.Direction, b mouse.Button) string { +func GetEvent(d mouse.Direction, b mouse.Button) event.EventID[*Event] { switch d { case mouse.DirPress: return Press diff --git a/mouse/mouse_test.go b/mouse/mouse_test.go index 8d04a33e..ac0686d3 100644 --- a/mouse/mouse_test.go +++ b/mouse/mouse_test.go @@ -7,19 +7,19 @@ import ( ) func TestEventNameIdentity(t *testing.T) { - if GetEventName(mouse.DirPress, 0) != "MousePress" { - t.Fatalf("event name mismatch for event %v, expected %v", mouse.DirPress, "MousePress") + if GetEvent(mouse.DirPress, 0) != Press { + t.Fatalf("event mismatch for event %v, expected %v", mouse.DirPress, "MousePress") } - if GetEventName(mouse.DirRelease, 0) != "MouseRelease" { - t.Fatalf("event name mismatch for event %v, expected %v", mouse.DirRelease, "MouseRelease") + if GetEvent(mouse.DirRelease, 0) != Release { + t.Fatalf("event mismatch for event %v, expected %v", mouse.DirRelease, "MouseRelease") } - if GetEventName(mouse.DirNone, -2) != "MouseScrollDown" { - t.Fatalf("event name mismatch for event %v, expected %v", mouse.DirNone, "MouseScrollDown") + if GetEvent(mouse.DirNone, -2) != ScrollDown { + t.Fatalf("event mismatch for event %v, expected %v", mouse.DirNone, "MouseScrollDown") } - if GetEventName(mouse.DirNone, -1) != "MouseScrollUp" { - t.Fatalf("event name mismatch for event %v, expected %v", mouse.DirNone, "MouseScrollUp") + if GetEvent(mouse.DirNone, -1) != ScrollUp { + t.Fatalf("event mismatch for event %v, expected %v", mouse.DirNone, "MouseScrollUp") } - if GetEventName(mouse.DirNone, 0) != "MouseDrag" { - t.Fatalf("event name mismatch for event %v, expected %v", mouse.DirNone, "MouseDrag") + if GetEvent(mouse.DirNone, 0) != Drag { + t.Fatalf("event mismatch for event %v, expected %v", mouse.DirNone, "MouseDrag") } } diff --git a/mouse/onCollision.go b/mouse/onCollision.go index 31af0bde..7c77abaf 100644 --- a/mouse/onCollision.go +++ b/mouse/onCollision.go @@ -11,6 +11,7 @@ import ( // enable PhaseCollision on the struct. See PhaseCollision. type CollisionPhase struct { OnCollisionS *collision.Space + CallerMap *event.CallerMap LastEvent *Event wasTouching bool @@ -20,6 +21,10 @@ func (cp *CollisionPhase) getCollisionPhase() *CollisionPhase { return cp } +func (cp *CollisionPhase) CID() event.CallerID { + return cp.OnCollisionS.CID +} + type collisionPhase interface { getCollisionPhase() *CollisionPhase } @@ -27,32 +32,35 @@ type collisionPhase interface { // PhaseCollision binds to the entity behind the space's CID so that it will // receive MouseCollisionStart and MouseCollisionStop events, appropriately when // the mouse begins to hover or stops hovering over the input space. -func PhaseCollision(s *collision.Space) error { - en := s.CID.E() +func PhaseCollision(s *collision.Space, handler event.Handler) error { + en := handler.GetCallerMap().GetEntity(s.CID) if cp, ok := en.(collisionPhase); ok { oc := cp.getCollisionPhase() oc.OnCollisionS = s - s.CID.Bind(event.Enter, phaseCollisionEnter) + oc.CallerMap = handler.GetCallerMap() + handler.UnsafeBind(event.Enter.UnsafeEventID, s.CID, phaseCollisionEnter) return nil } return errors.New("This space's entity does not implement collisionPhase") } // MouseCollisionStart/Stop: see collision Start/Stop, for mouse collision -// Payload: (*mouse.Event) -const ( - Start = "MouseCollisionStart" - Stop = "MouseCollisionStop" +var ( + Start = event.RegisterEvent[*Event]() + Stop = event.RegisterEvent[*Event]() ) -func phaseCollisionEnter(id event.CID, nothing interface{}) int { - e := id.E().(collisionPhase) +func phaseCollisionEnter(id event.CallerID, handler event.Handler, _ interface{}) event.Response { + e, ok := handler.GetCallerMap().GetEntity(id).(collisionPhase) + if !ok { + return event.ResponseUnbindThisBinding + } oc := e.getCollisionPhase() if oc == nil || oc.OnCollisionS == nil { return 0 } - // TODO: think about how this can more cleanly work with multiple controllers + // TODO: think about how this can more cleanly work with multiple windows ev := oc.LastEvent if ev == nil { ev = &LastEvent @@ -63,12 +71,12 @@ func phaseCollisionEnter(id event.CID, nothing interface{}) int { if oc.OnCollisionS.Contains(ev.ToSpace()) { if !oc.wasTouching { - id.Trigger(Start, ev) + event.TriggerForCallerOn(handler, id, Start, ev) oc.wasTouching = true } } else { if oc.wasTouching { - id.Trigger(Stop, ev) + event.TriggerForCallerOn(handler, id, Stop, ev) oc.wasTouching = false } } diff --git a/mouse/onCollision_test.go b/mouse/onCollision_test.go index fbff918d..12fe61a5 100644 --- a/mouse/onCollision_test.go +++ b/mouse/onCollision_test.go @@ -11,52 +11,56 @@ import ( type cphase struct { CollisionPhase -} - -func (cp *cphase) Init() event.CID { - return event.NextID(cp) + callers *event.CallerMap } func TestCollisionPhase(t *testing.T) { - go event.ResolveChanges() + b := event.NewBus(event.NewCallerMap()) go func() { for { <-time.After(5 * time.Millisecond) - <-event.TriggerBack(event.Enter, nil) + <-event.TriggerOn(b, event.Enter, event.EnterPayload{}) } }() - cp := cphase{} - cid := cp.Init() + cp := &cphase{} + cid := b.GetCallerMap().Register(cp) s := collision.NewSpace(10, 10, 10, 10, cid) - if PhaseCollision(s) != nil { - t.Fatalf("phase collision errored") + err := PhaseCollision(s, b) + if err != nil { + t.Fatalf("phase collision failed: %v", err) } - var active bool - cid.Bind("MouseCollisionStart", func(event.CID, interface{}) int { - active = true + activeCh := make(chan bool, 5) + b1 := event.Bind(b, Start, cp, func(_ *cphase, _ *Event) event.Response { + activeCh <- true return 0 }) - cid.Bind("MouseCollisionStop", func(event.CID, interface{}) int { - active = false + b2 := event.Bind(b, Stop, cp, func(_ *cphase, _ *Event) event.Response { + activeCh <- false return 0 }) - time.Sleep(200 * time.Millisecond) + <-b1.Bound + <-b2.Bound LastEvent = Event{ Point2: floatgeom.Point2{10, 10}, } - time.Sleep(200 * time.Millisecond) - if !active { - t.Fatalf("phase collision did not trigger") + if active := <-activeCh; !active { + t.Fatalf("collision should be active") } + LastEvent = Event{ Point2: floatgeom.Point2{21, 21}, } time.Sleep(200 * time.Millisecond) - if active { - t.Fatalf("phase collision triggered innapropriately") + if active := <-activeCh; active { + t.Fatalf("collision should be inactive") } - s = collision.NewSpace(10, 10, 10, 10, 5) - if PhaseCollision(s) == nil { - t.Fatalf("phase collision did not error on invalid space") +} + +func TestPhaseCollision_Unembedded(t *testing.T) { + t.Parallel() + s3 := collision.NewSpace(10, 10, 10, 10, 5) + err := PhaseCollision(s3, event.DefaultBus) + if err == nil { + t.Fatalf("phase collision should have failed") } } diff --git a/mouse/strings.go b/mouse/strings.go deleted file mode 100644 index cdc1c24e..00000000 --- a/mouse/strings.go +++ /dev/null @@ -1,19 +0,0 @@ -package mouse - -// Mouse events: MousePress, MouseRelease, MouseScrollDown, MouseScrollUp, MouseDrag -// Payload: (*mouse.Event) details of the mouse event -const ( - Press = "MousePress" - Release = "MouseRelease" - ScrollDown = "MouseScrollDown" - ScrollUp = "MouseScrollUp" - Click = "MouseClick" - Drag = "MouseDrag" - // - PressOn = Press + "On" - ReleaseOn = Release + "On" - ScrollDownOn = ScrollDown + "On" - ScrollUpOn = ScrollUp + "On" - ClickOn = Click + "On" - DragOn = Drag + "On" -) diff --git a/render/interfaceFeatures.go b/render/interfaceFeatures.go index 14bdb756..1b8a8dfc 100644 --- a/render/interfaceFeatures.go +++ b/render/interfaceFeatures.go @@ -11,7 +11,7 @@ type NonStatic interface { // Triggerable types can have an ID set so when their animations finish, // they trigger AnimationEnd on that ID. type Triggerable interface { - SetTriggerID(event.CID) + SetTriggerID(event.CallerID) } type updates interface { diff --git a/render/logicfps.go b/render/logicfps.go index 8c06d81a..edce75c1 100644 --- a/render/logicfps.go +++ b/render/logicfps.go @@ -10,18 +10,15 @@ import ( // 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 + event.CallerID *Text fps int lastTime time.Time Smoothing float64 } -// Init satisfies event.Entity -func (lf *LogicFPS) Init() event.CID { - id := event.NextID(lf) - lf.CID = id - return id +func (lf LogicFPS) CID() event.CallerID { + return lf.CallerID } // NewLogicFPS returns a LogicFPS, which will render a counter of how fast it receives event.Enter events. @@ -38,14 +35,14 @@ func NewLogicFPS(smoothing float64, font *Font, x, y float64) *LogicFPS { lastTime: time.Now(), } lf.Text = font.NewIntText(&lf.fps, x, y) - lf.Init() - lf.Bind(event.Enter, logicFPSBind) + lf.CallerID = event.DefaultCallerMap.Register(lf) + // TODO: not default bus + event.Bind(event.DefaultBus, event.Enter, lf, logicFPSBind) return lf } -func logicFPSBind(id event.CID, nothing interface{}) int { - lf := event.GetEntity(id).(*LogicFPS) +func logicFPSBind(lf *LogicFPS, _ event.EnterPayload) event.Response { t := time.Now() lf.fps = int((timing.FPS(lf.lastTime, t) * lf.Smoothing) + (float64(lf.fps) * (1 - lf.Smoothing))) lf.lastTime = t diff --git a/render/logicfps_test.go b/render/logicfps_test.go index c6c86e6a..62e908fa 100644 --- a/render/logicfps_test.go +++ b/render/logicfps_test.go @@ -3,12 +3,14 @@ package render import ( "image" "testing" + + "github.com/oakmound/oak/v3/event" ) func TestLogicFPS(t *testing.T) { lfps := NewLogicFPS(0, nil, 0, 0) lfps.Draw(image.NewRGBA(image.Rect(0, 0, 100, 100)), 10, 10) - logicFPSBind(lfps.CID, nil) + logicFPSBind(lfps, event.EnterPayload{}) if lfps.fps == 0 { t.Fatalf("fps not set by binding") } diff --git a/render/particle/allocator.go b/render/particle/allocator.go index 1d6f80e9..83706c5d 100644 --- a/render/particle/allocator.go +++ b/render/particle/allocator.go @@ -10,24 +10,24 @@ const ( // An Allocator can allocate ids for particles type Allocator struct { - particleBlocks map[int]event.CID + particleBlocks map[int]event.CallerID nextOpenCh chan int freeCh chan int - allocCh chan event.CID + allocCh chan event.CallerID requestCh chan int - responseCh chan event.CID + responseCh chan event.CallerID stopCh chan struct{} } // NewAllocator creates a new allocator func NewAllocator() *Allocator { return &Allocator{ - particleBlocks: make(map[int]event.CID), + particleBlocks: make(map[int]event.CallerID), nextOpenCh: make(chan int), freeCh: make(chan int), - allocCh: make(chan event.CID), + allocCh: make(chan event.CallerID), requestCh: make(chan int), - responseCh: make(chan event.CID), + responseCh: make(chan event.CallerID), stopCh: make(chan struct{}), } } @@ -82,7 +82,7 @@ func (a *Allocator) freereceive(i int) int { } // Allocate requests a new block in the particle space for the given cid -func (a *Allocator) Allocate(id event.CID) int { +func (a *Allocator) Allocate(id event.CallerID) int { nextOpen := <-a.nextOpenCh a.allocCh <- id return nextOpen @@ -97,7 +97,8 @@ func (a *Allocator) Deallocate(block int) { func (a *Allocator) LookupSource(id int) *Source { a.requestCh <- id owner := <-a.responseCh - return event.GetEntity(owner).(*Source) + // TODO: not default? + return event.DefaultCallerMap.GetEntity(owner).(*Source) } // Lookup requests a specific particle in the particle space diff --git a/render/particle/allocator_test.go b/render/particle/allocator_test.go index e11c945f..bbf04497 100644 --- a/render/particle/allocator_test.go +++ b/render/particle/allocator_test.go @@ -10,7 +10,7 @@ func TestAllocate(t *testing.T) { a := NewAllocator() go a.Run() for i := 0; i < 100; i++ { - if a.Allocate(event.CID(i)) != i { + if a.Allocate(event.CallerID(i)) != i { t.Fatalf("expected allocation of id %d to match id", i) } } @@ -32,8 +32,8 @@ func TestAllocatorLookup(t *testing.T) { a := NewAllocator() go a.Run() - src := NewSource(NewColorGenerator(), 0) - cid := src.CID + src := NewDefaultSource(NewColorGenerator(), 0) + cid := src.CID() pidBlock := a.Allocate(cid) src2 := a.LookupSource(pidBlock * blockSize) if src != src2 { diff --git a/render/particle/collisonGenerator.go b/render/particle/collisonGenerator.go index 983e30de..f4d269f5 100644 --- a/render/particle/collisonGenerator.go +++ b/render/particle/collisonGenerator.go @@ -49,7 +49,7 @@ func (cg *CollisionGenerator) GenerateParticle(bp *baseParticle) Particle { pos := p.GetPos() return &CollisionParticle{ p, - collision.NewReactiveSpace(collision.NewFullSpace(pos.X(), pos.Y(), w, h, 0, event.CID(bp.pID)), cg.HitMap), + collision.NewReactiveSpace(collision.NewFullSpace(pos.X(), pos.Y(), w, h, 0, event.CallerID(bp.pID)), cg.HitMap), } } diff --git a/render/particle/colorGenerator.go b/render/particle/colorGenerator.go index 4a7a7c32..4b22ceeb 100644 --- a/render/particle/colorGenerator.go +++ b/render/particle/colorGenerator.go @@ -51,7 +51,7 @@ func (cg *ColorGenerator) Generate(layer int) *Source { if cg.Rotation != nil { cg.Rotation = cg.Rotation.Mult(alg.DegToRad) } - return NewSource(cg, layer) + return NewDefaultSource(cg, layer) } // GenerateParticle creates a particle from a generator diff --git a/render/particle/gradientGenerator.go b/render/particle/gradientGenerator.go index d2a947d7..405e3cac 100644 --- a/render/particle/gradientGenerator.go +++ b/render/particle/gradientGenerator.go @@ -45,7 +45,7 @@ func (gg *GradientGenerator) Generate(layer int) *Source { if gg.Rotation != nil { gg.Rotation = gg.Rotation.Mult(alg.DegToRad) } - return NewSource(gg, layer) + return NewDefaultSource(gg, layer) } // GenerateParticle creates a particle from a generator diff --git a/render/particle/source.go b/render/particle/source.go index e1abe60d..9a3c089e 100644 --- a/render/particle/source.go +++ b/render/particle/source.go @@ -19,9 +19,11 @@ type Source struct { Generator Generator *Allocator - particles [blockSize]Particle - nextPID int - CID event.CID + rotateBinding event.Binding + + particles [blockSize]Particle + nextPID int + event.CallerID pIDBlock int stackLevel int EndFunc func() @@ -31,25 +33,31 @@ type Source struct { stopped bool } -// NewSource creates a new source -func NewSource(g Generator, stackLevel int) *Source { +// NewDefaultSource creates a new sourceattached to the default event bus. +func NewDefaultSource(g Generator, stackLevel int) *Source { + return NewSource(event.DefaultBus, g, stackLevel) +} + +// NewSource for particles constructed from a generator with specifications on how the particles should be handled. +func NewSource(handler event.Handler, g Generator, stackLevel int) *Source { ps := new(Source) ps.Generator = g ps.stackLevel = stackLevel ps.Allocator = DefaultAllocator - ps.Init() + cid := handler.GetCallerMap().Register(ps) + ps.stopRotateAt = time.Now().Add( + time.Duration(ps.Generator.GetBaseGenerator().Duration.Poll()) * time.Millisecond) + + ps.CallerID = cid // cid must be set before the following bind call + ps.rotateBinding = event.Bind(handler, event.Enter, ps, rotateParticles) + + ps.pIDBlock = ps.Allocate(ps.CallerID) return ps } -// Init allows a source to be considered as an entity, and initializes it -func (ps *Source) Init() event.CID { - CID := event.NextID(ps) - ps.stopRotateAt = time.Now().Add( - time.Duration(ps.Generator.GetBaseGenerator().Duration.Poll()) * time.Millisecond) - CID.Bind(event.Enter, rotateParticles) - ps.CID = CID - ps.pIDBlock = ps.Allocate(ps.CID) - return CID +// CID of our particle source +func (ps *Source) CID() event.CallerID { + return ps.CallerID } func (ps *Source) cycleParticles() bool { @@ -176,8 +184,7 @@ func (ps *Source) addParticles() { // rotateParticles updates particles over time as long // as a Source is active. -func rotateParticles(id event.CID, payload interface{}) int { - ps := id.E().(*Source) +func rotateParticles(ps *Source, _ event.EnterPayload) event.Response { if ps.stopped { return 0 } @@ -197,33 +204,31 @@ func rotateParticles(id event.CID, payload interface{}) int { // clearParticles is used after a Source has been stopped // to continue moving old particles for as long as they exist. -func clearParticles(id event.CID, nothing interface{}) int { - if ps, ok := id.E().(*Source); ok { - if !ps.paused { - if ps.cycleParticles() { - } else { - if ps.EndFunc != nil { - ps.EndFunc() - } - event.DestroyEntity(id) - ps.Deallocate(ps.pIDBlock) - return event.UnbindEvent +func clearParticles(ps *Source, _ event.EnterPayload) event.Response { + if !ps.paused { + if ps.cycleParticles() { + } else { + if ps.EndFunc != nil { + ps.EndFunc() } + // TODO: not default + event.DefaultCallerMap.RemoveEntity(ps.CID()) + ps.Deallocate(ps.pIDBlock) + return event.ResponseUnbindThisBinding } - - return 0 } - return event.UnbindEvent + return 0 } // Stop manually stops a Source, if its duration is infinite -// or if it should be stopped before expriring naturally. +// or if it should be stopped before expiring naturally. func (ps *Source) Stop() { if ps == nil { return } ps.stopped = true - ps.CID.UnbindAllAndRebind([]event.Bindable{clearParticles}, []string{event.Enter}) + ps.rotateBinding.Unbind() + event.Bind(event.DefaultBus, event.Enter, ps, clearParticles) } // Pause on a Source just stops the repetition diff --git a/render/particle/source_test.go b/render/particle/source_test.go index c70c1f76..728d68d5 100644 --- a/render/particle/source_test.go +++ b/render/particle/source_test.go @@ -48,9 +48,9 @@ func TestSource(t *testing.T) { } for i := 0; i < 1000; i++ { - rotateParticles(src.CID, nil) + rotateParticles(src, event.EnterPayload{}) } - for clearParticles(src.CID, nil) != event.UnbindEvent { + for clearParticles(src, event.EnterPayload{}) != event.ResponseUnbindThisBinding { } if !ended { @@ -87,14 +87,3 @@ func TestSource(t *testing.T) { var src2 *Source src2.Stop() } - -func TestClearParticles(t *testing.T) { - t.Parallel() - t.Run("BadTypeBinding", func(t *testing.T) { - t.Parallel() - result := clearParticles(10000, nil) - if result != event.UnbindEvent { - t.Fatalf("expected UnbindEvent result, got %v", result) - } - }) -} diff --git a/render/particle/spriteGenerator.go b/render/particle/spriteGenerator.go index 1cd5c760..111b6102 100644 --- a/render/particle/spriteGenerator.go +++ b/render/particle/spriteGenerator.go @@ -37,7 +37,7 @@ func (sg *SpriteGenerator) Generate(layer int) *Source { if sg.Rotation != nil { sg.Rotation = sg.Rotation.Mult(alg.DegToRad) } - return NewSource(sg, layer) + return NewDefaultSource(sg, layer) } // GenerateParticle creates a particle from a generator diff --git a/render/reverting.go b/render/reverting.go index c93d3823..bafa6d68 100644 --- a/render/reverting.go +++ b/render/reverting.go @@ -119,7 +119,7 @@ func (rv *Reverting) update() { } // SetTriggerID sets the ID AnimationEnd will trigger on for animating subtypes. -func (rv *Reverting) SetTriggerID(cid event.CID) { +func (rv *Reverting) SetTriggerID(cid event.CallerID) { if t, ok := rv.Modifiable.(Triggerable); ok { t.SetTriggerID(cid) } diff --git a/render/reverting_test.go b/render/reverting_test.go index d4d324e1..49cae688 100644 --- a/render/reverting_test.go +++ b/render/reverting_test.go @@ -157,7 +157,7 @@ func TestRevertingCascadeFns(t *testing.T) { t.Fatalf("reverting unpause did not resume underlying sequence") } rv.SetTriggerID(1) - if sq.cID != 1 { + if sq.CallerID != 1 { t.Fatalf("reverting cID did not set underlying squence cID") } rv.update() diff --git a/render/sequence.go b/render/sequence.go index d1a9bc9f..0ce1afad 100644 --- a/render/sequence.go +++ b/render/sequence.go @@ -20,7 +20,7 @@ type Sequence struct { lastChange time.Time sheetPos int frameTime int64 - cID event.CID + event.CallerID } // NewSequence returns a new sequence from the input modifiables, playing at @@ -68,18 +68,21 @@ func (sq *Sequence) Copy() Modifiable { return newSq } +var AnimationEnd = event.RegisterEvent[struct{}]() + // SetTriggerID sets the ID that AnimationEnd will be triggered on when this // sequence loops over from its last frame to its first -func (sq *Sequence) SetTriggerID(id event.CID) { - sq.cID = id +func (sq *Sequence) SetTriggerID(id event.CallerID) { + sq.CallerID = id } func (sq *Sequence) update() { if sq.playing && time.Since(sq.lastChange).Nanoseconds() > sq.frameTime { sq.lastChange = time.Now() sq.sheetPos = (sq.sheetPos + 1) % len(sq.rs) - if sq.sheetPos == (len(sq.rs)-1) && sq.cID != 0 { - sq.cID.Trigger(event.AnimationEnd, nil) + if sq.sheetPos == (len(sq.rs)-1) && sq.CallerID != 0 { + // TODO: not default bus + event.TriggerForCallerOn(event.DefaultBus, sq.CallerID, AnimationEnd, struct{}{}) } } } diff --git a/render/sequence_test.go b/render/sequence_test.go index 1a0c4ae4..45b56f29 100644 --- a/render/sequence_test.go +++ b/render/sequence_test.go @@ -12,31 +12,19 @@ import ( "github.com/oakmound/oak/v3/render/mod" ) -type Dummy struct{} - -func (d Dummy) Init() event.CID { - return event.NextID(d) +type Dummy struct { + event.CallerID } func TestSequenceTrigger(t *testing.T) { sq := NewSequence(5, NewColorBox(10, 10, color.RGBA{255, 0, 0, 255}), NewColorBox(10, 10, color.RGBA{0, 255, 0, 255})) - go event.ResolveChanges() - cid := Dummy{}.Init() - sq.SetTriggerID(cid) + d := Dummy{} + d.CallerID = event.DefaultCallerMap.Register(d) + sq.SetTriggerID(d.CallerID) triggerCh := make(chan struct{}) - cid.Bind(event.AnimationEnd, func(event.CID, interface{}) int { - // This is a bad idea in real code, this will lock up - // unbindings because the function that triggered this owns - // the lock on the event bus until this function exits. - // It is for this reason that all triggers, bindings, - // and unbindings do nothing when they are called, just put - // off work to be done-- to make sure no one is expecting a - // result from one of those functions, from within a triggered - // function, causing a deadlock. - // - // For this test this is the easiest way to do this though + event.Bind(event.DefaultBus, AnimationEnd, d, func(_ Dummy, _ struct{}) event.Response { triggerCh <- struct{}{} return 0 }) diff --git a/render/switch.go b/render/switch.go index 9387205f..e493bc8a 100644 --- a/render/switch.go +++ b/render/switch.go @@ -187,7 +187,7 @@ func (c *Switch) IsStatic() bool { // Todo: standardize this with the other interface Set functions so that it // also only acts on the current subRenderable, or the other way around, or // somehow offer both options -func (c *Switch) SetTriggerID(cid event.CID) { +func (c *Switch) SetTriggerID(cid event.CallerID) { c.lock.RLock() for _, r := range c.subRenderables { if t, ok := r.(Triggerable); ok { diff --git a/scene/context.go b/scene/context.go index 4121b2b9..79e28aee 100644 --- a/scene/context.go +++ b/scene/context.go @@ -15,18 +15,19 @@ import ( // and a reference to the OS window itself. When a scene ends, modifications made // to these structures will be reset, excluding window modifications. // TODO oak v4: consider embedding these system objects on the context to change -// ctx.DrawStack.Draw to ctx.Draw and ctx.EventHandler.Bind to ctx.Bind +// ctx.DrawStack.Draw to ctx.Draw and ctx.Handler.Bind to ctx.Bind type Context struct { // This context will be canceled when the scene ends context.Context + *event.CallerMap + event.Handler PreviousScene string SceneInput interface{} Window window.Window - DrawStack *render.DrawStack - EventHandler event.Handler - CallerMap *event.CallerMap + DrawStack *render.DrawStack + MouseTree *collision.Tree CollisionTree *collision.Tree KeyState *key.State diff --git a/scene/map.go b/scene/map.go index 6687d260..deaa77e8 100644 --- a/scene/map.go +++ b/scene/map.go @@ -48,9 +48,6 @@ func (m *Map) AddScene(name string, s Scene) error { if s.Start == nil { s.Start = func(*Context) {} } - if s.Loop == nil { - s.Loop = func() bool { return true } - } if s.End == nil { s.End = GoTo(name) } diff --git a/scene/map_test.go b/scene/map_test.go index 91e4c424..3678c1b0 100644 --- a/scene/map_test.go +++ b/scene/map_test.go @@ -77,9 +77,6 @@ func TestAddScene(t *testing.T) { t.Fatalf("getting test scene failed") } - if !test1.Loop() { - t.Fatalf("test loop failed") - } eStr, _ := test1.End() if eStr != "test1" { t.Fatalf("looping test end did not return test1, got %v", eStr) diff --git a/scene/scene.go b/scene/scene.go index 2e7d7420..5f5cf641 100644 --- a/scene/scene.go +++ b/scene/scene.go @@ -12,9 +12,6 @@ type Scene struct { // what scene came before this one and a direct reference to clean data structures // for event handling and rendering Start func(ctx *Context) - // If Loop returns true, the scene will continue - // If Loop returns false, End will be called to determine the next scene - Loop func() (cont bool) // End is a function returning the next scene and a SceneResult of // input settings for the next scene. End func() (nextScene string, result *Result) @@ -27,19 +24,6 @@ type Result struct { Transition } -// BooleanLoop returns a Loop function that will end a scene as soon as the -// input boolean is false, resetting it to true in the process for the -// next scene -func BooleanLoop(b *bool) func() (cont bool) { - return func() bool { - if !(*b) { - *b = true - return false - } - return true - } -} - // GoTo returns an End function that, without any other customization possible, // will change to the input next scene. func GoTo(nextScene string) func() (nextScene string, result *Result) { diff --git a/scene/scene_test.go b/scene/scene_test.go index 5d4946c7..683f240e 100644 --- a/scene/scene_test.go +++ b/scene/scene_test.go @@ -5,21 +5,6 @@ import ( "testing" ) -func TestBooleanLoop(t *testing.T) { - v := true - l := BooleanLoop(&v) - if !l() { - t.Fatalf("boolean loop should loop when true") - } - v = false - if l() { - t.Fatalf("boolean loop should not loop when false") - } - if !l() { - t.Fatalf("boolean loop should resume looping after returning false") - } -} - func randString() string { length := rand.Intn(100) data := make([]byte, length) diff --git a/sceneLoop.go b/sceneLoop.go index e4a31e8c..2d5e4bb1 100644 --- a/sceneLoop.go +++ b/sceneLoop.go @@ -8,16 +8,24 @@ import ( "github.com/oakmound/oak/v3/event" "github.com/oakmound/oak/v3/oakerr" "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v3/timing" ) // the oak loading scene is a reserved scene // for preloading assets const oakLoadingScene = "oak:loading" -func (w *Window) sceneLoop(first string, trackingInputs bool) { +func (w *Window) sceneLoop(first string, trackingInputs, batchLoad bool) { w.SceneMap.AddScene(oakLoadingScene, scene.Scene{ - Loop: func() bool { - return w.startupLoading + Start: func(ctx *scene.Context) { + if batchLoad { + go func() { + w.loadAssets(w.config.Assets.ImagePath, w.config.Assets.AudioPath) + w.endLoad() + }() + } else { + go w.endLoad() + } }, End: func() (string, *scene.Result) { return w.firstScene, &scene.Result{ @@ -67,7 +75,7 @@ func (w *Window) sceneLoop(first string, trackingInputs bool) { PreviousScene: prevScene, SceneInput: result.NextSceneInput, DrawStack: w.DrawStack, - EventHandler: w.eventHandler, + Handler: w.eventHandler, CallerMap: w.CallerMap, MouseTree: w.MouseTree, CollisionTree: w.CollisionTree, @@ -88,7 +96,7 @@ func (w *Window) sceneLoop(first string, trackingInputs bool) { dlog.Info(dlog.SceneLooping) cont := true - dlog.ErrorCheck(w.eventHandler.UpdateLoop(w.FrameRate, w.sceneCh)) + enterCancel := event.EnterLoop(w.eventHandler, timing.FPSToFrameDelay(w.FrameRate)) nextSceneOverride := "" @@ -98,8 +106,6 @@ func (w *Window) sceneLoop(first string, trackingInputs bool) { case <-w.quitCh: cancel() return - case <-w.sceneCh: - cont = scen.Loop() case nextSceneOverride = <-w.skipSceneCh: cont = false } @@ -108,7 +114,7 @@ func (w *Window) sceneLoop(first string, trackingInputs bool) { dlog.Info(dlog.SceneEnding, w.SceneMap.CurrentScene) // We don't want enterFrames going off between scenes - dlog.ErrorCheck(w.eventHandler.Stop()) + enterCancel() prevScene = w.SceneMap.CurrentScene // Send a signal to stop drawing @@ -124,15 +130,8 @@ func (w *Window) sceneLoop(first string, trackingInputs bool) { // be triggered and attempt to access an entity w.CollisionTree.Clear() w.MouseTree.Clear() - if w.CallerMap == event.DefaultCallerMap { - event.ResetCallerMap() - w.CallerMap = event.DefaultCallerMap - } else { - w.CallerMap = event.NewCallerMap() - } - if cmr, ok := w.eventHandler.(event.CallerMapper); ok { - cmr.SetCallerMap(w.CallerMap) - } + w.CallerMap.Clear() + w.eventHandler.SetCallerMap(w.CallerMap) w.DrawStack.Clear() w.DrawStack.PreDraw() diff --git a/testdata/default.config b/testdata/default.config index bdab51fe..61b8427d 100644 --- a/testdata/default.config +++ b/testdata/default.config @@ -22,6 +22,5 @@ "language": "English", "title": "Oak Window", "batchLoad": false, - "gestureSupport": false, - "refreshRate": "50ms" + "gestureSupport": false } \ No newline at end of file diff --git a/viewport.go b/viewport.go index 0cf386f6..8ad51ea4 100644 --- a/viewport.go +++ b/viewport.go @@ -34,7 +34,7 @@ func (w *Window) setViewport(pt intgeom.Point2) { } else { w.viewPos = pt } - w.eventHandler.Trigger(event.ViewportUpdate, w.viewPos) + event.TriggerOn(w.eventHandler, ViewportUpdate, w.viewPos) } // GetViewportBounds reports what bounds the viewport has been set to, if any. diff --git a/window.go b/window.go index 93c38069..9f109d07 100644 --- a/window.go +++ b/window.go @@ -43,11 +43,6 @@ type Window struct { // TODO: most of these channels are not closed cleanly transitionCh chan struct{} - // The Scene channel receives a signal - // when a scene's .loop() function should - // be called. - sceneCh chan struct{} - // The skip scene channel receives a debug // signal to forcibly go to the next // scene. @@ -152,14 +147,12 @@ type Window struct { config Config - mostRecentInput InputType + mostRecentInput int32 exitError error ParentContext context.Context - TrackMouseClicks bool - startupLoading bool - useViewBounds bool + useViewBounds bool // UseAspectRatio determines whether new window changes will distort or // maintain the relative width to height ratio of the screen buffer. UseAspectRatio bool @@ -176,7 +169,6 @@ func NewWindow() *Window { c := &Window{ State: key.NewState(), transitionCh: make(chan struct{}), - sceneCh: make(chan struct{}), skipSceneCh: make(chan string), quitCh: make(chan struct{}), drawCh: make(chan struct{}), @@ -194,7 +186,6 @@ func NewWindow() *Window { c.CollisionTree = collision.DefaultTree c.CallerMap = event.DefaultCallerMap c.DrawStack = render.GlobalDrawStack - c.TrackMouseClicks = true c.commands = make(map[string]func([]string)) c.ControllerID = atomic.AddInt32(nextControllerID, 1) c.ParentContext = context.Background() @@ -202,55 +193,54 @@ func NewWindow() *Window { } // Propagate triggers direct mouse events on entities which are clicked -func (w *Window) Propagate(eventName string, me mouse.Event) { +func (w *Window) Propagate(ev event.EventID[*mouse.Event], me mouse.Event) { hits := w.MouseTree.SearchIntersect(me.ToSpace().Bounds()) sort.Slice(hits, func(i, j int) bool { return hits[i].Location.Min.Z() < hits[i].Location.Max.Z() }) for _, sp := range hits { - <-sp.CID.TriggerBus(eventName, &me, w.eventHandler) + <-event.TriggerForCallerOn(w.eventHandler, sp.CID, ev, &me) if me.StopPropagation { break } } me.StopPropagation = false - if w.TrackMouseClicks { - if eventName == mouse.PressOn+"Relative" { - w.lastRelativePress = me - } else if eventName == mouse.PressOn { - w.LastMousePress = me - } else if eventName == mouse.ReleaseOn { - if me.Button == w.LastMousePress.Button { - pressHits := w.MouseTree.SearchIntersect(w.LastMousePress.ToSpace().Bounds()) - sort.Slice(pressHits, func(i, j int) bool { - return pressHits[i].Location.Min.Z() < pressHits[i].Location.Max.Z() - }) - for _, sp1 := range pressHits { - for _, sp2 := range hits { - if sp1.CID == sp2.CID { - w.eventHandler.Trigger(mouse.Click, &me) - <-sp1.CID.TriggerBus(mouse.ClickOn, &me, w.eventHandler) - if me.StopPropagation { - return - } + if ev == mouse.RelativePressOn { + w.lastRelativePress = me + } else if ev == mouse.PressOn { + w.LastMousePress = me + } else if ev == mouse.ReleaseOn { + if me.Button == w.LastMousePress.Button { + event.TriggerOn(w.eventHandler, mouse.Click, &me) + + pressHits := w.MouseTree.SearchIntersect(w.LastMousePress.ToSpace().Bounds()) + sort.Slice(pressHits, func(i, j int) bool { + return pressHits[i].Location.Min.Z() < pressHits[i].Location.Max.Z() + }) + for _, sp1 := range pressHits { + for _, sp2 := range hits { + if sp1.CID == sp2.CID { + <-event.TriggerForCallerOn(w.eventHandler, sp1.CID, mouse.ClickOn, &me) + if me.StopPropagation { + return } } } } - } else if eventName == mouse.ReleaseOn+"Relative" { - if me.Button == w.lastRelativePress.Button { - pressHits := w.MouseTree.SearchIntersect(w.lastRelativePress.ToSpace().Bounds()) - sort.Slice(pressHits, func(i, j int) bool { - return pressHits[i].Location.Min.Z() < pressHits[i].Location.Max.Z() - }) - for _, sp1 := range pressHits { - for _, sp2 := range hits { - if sp1.CID == sp2.CID { - sp1.CID.Trigger(mouse.ClickOn+"Relative", &me) - if me.StopPropagation { - return - } + } + } else if ev == mouse.RelativeReleaseOn { + if me.Button == w.lastRelativePress.Button { + pressHits := w.MouseTree.SearchIntersect(w.lastRelativePress.ToSpace().Bounds()) + sort.Slice(pressHits, func(i, j int) bool { + return pressHits[i].Location.Min.Z() < pressHits[i].Location.Max.Z() + }) + for _, sp1 := range pressHits { + for _, sp2 := range hits { + if sp1.CID == sp2.CID { + <-event.TriggerForCallerOn(w.eventHandler, sp1.CID, mouse.RelativeClickOn, &me) + if me.StopPropagation { + return } } } @@ -345,10 +335,10 @@ func (w *Window) EventHandler() event.Handler { } // MostRecentInput returns the most recent input type (e.g keyboard/mouse or joystick) -// recognized by the window. This value will only change if the controller's Config is +// recognized by the window. This value will only change if the window is // set to TrackInputChanges func (w *Window) MostRecentInput() InputType { - return w.mostRecentInput + return InputType(w.mostRecentInput) } func (w *Window) exitWithError(err error) { @@ -360,3 +350,7 @@ func (w *Window) debugConsole(input io.Reader, output io.Writer) { debugstream.AttachToStream(w.ParentContext, input, output) debugstream.AddDefaultsForScope(w.ControllerID, w) } + +func (w *Window) GetCallerMap() *event.CallerMap { + return w.CallerMap +} diff --git a/window/window.go b/window/window.go index 8d13bae4..926df662 100644 --- a/window/window.go +++ b/window/window.go @@ -32,4 +32,5 @@ type Window interface { Quit() EventHandler() event.Handler + GetCallerMap() *event.CallerMap } diff --git a/window_test.go b/window_test.go index 23c15f79..cdd02d7b 100644 --- a/window_test.go +++ b/window_test.go @@ -11,69 +11,83 @@ import ( func TestMouseClicks(t *testing.T) { c1 := NewWindow() - sp := collision.NewFullSpace(0, 0, 100, 100, 1, 0) - var triggered bool - go event.ResolveChanges() - event.GlobalBind(mouse.Click, func(event.CID, interface{}) int { - triggered = true + c1.MouseTree = collision.NewTree() + ch := make(chan struct{}) + c1.eventHandler = event.NewBus(event.NewCallerMap()) + bnd := event.GlobalBind(c1.eventHandler, mouse.Click, func(_ *mouse.Event) event.Response { + close(ch) return 0 }) - time.Sleep(2 * time.Second) - mouse.DefaultTree.Add(sp) + select { + case <-time.After(2 * time.Second): + t.Fatalf("click binding never bound") + case <-bnd.Bound: + } + sp := collision.NewFullSpace(0, 0, 100, 100, 1, 0) + c1.MouseTree.Add(sp) c1.Propagate(mouse.PressOn, mouse.NewEvent(5, 5, mouse.ButtonLeft, mouse.PressOn)) c1.Propagate(mouse.ReleaseOn, mouse.NewEvent(5, 5, mouse.ButtonLeft, mouse.ReleaseOn)) - time.Sleep(2 * time.Second) - if !triggered { + select { + case <-time.After(2 * time.Second): t.Fatalf("propagation failed to trigger click binding") + case <-ch: } } func TestMouseClicksRelative(t *testing.T) { c1 := NewWindow() - sp := collision.NewFullSpace(0, 0, 100, 100, 1, 0) - var triggered bool - go c1.eventHandler.(*event.Bus).ResolveChanges() - c1.eventHandler.GlobalBind(mouse.ClickOn+"Relative", func(event.CID, interface{}) int { - triggered = true + c1.MouseTree = collision.NewTree() + ch := make(chan struct{}) + c1.eventHandler = event.NewBus(event.NewCallerMap()) + bnd := event.GlobalBind(c1.eventHandler, mouse.RelativeClickOn, func(_ *mouse.Event) event.Response { + close(ch) return 0 }) - time.Sleep(2 * time.Second) + select { + case <-time.After(2 * time.Second): + t.Fatalf("click binding never bound") + case <-bnd.Bound: + } + sp := collision.NewFullSpace(0, 0, 100, 100, 1, 0) c1.MouseTree.Add(sp) - c1.Propagate(mouse.PressOn+"Relative", mouse.NewEvent(5, 5, mouse.ButtonLeft, mouse.PressOn)) - c1.Propagate(mouse.ReleaseOn+"Relative", mouse.NewEvent(5, 5, mouse.ButtonLeft, mouse.ReleaseOn)) - time.Sleep(3 * time.Second) - if !triggered { + defer c1.MouseTree.Clear() + c1.Propagate(mouse.RelativePressOn, mouse.NewEvent(5, 5, mouse.ButtonLeft, mouse.PressOn)) + c1.Propagate(mouse.RelativeReleaseOn, mouse.NewEvent(5, 5, mouse.ButtonLeft, mouse.ReleaseOn)) + select { + case <-time.After(2 * time.Second): t.Fatalf("propagation failed to trigger click binding") + case <-ch: } } -type ent struct{} - -func (e ent) Init() event.CID { - return 0 +type ent struct { + event.CallerID } func TestPropagate(t *testing.T) { c1 := NewWindow() - go event.ResolveChanges() - var triggered bool - cid := event.CID(0).Parse(ent{}) - s := collision.NewSpace(10, 10, 10, 10, cid) - s.CID.Bind("MouseDownOn", func(event.CID, interface{}) int { - triggered = true + c1.eventHandler = event.NewBus(event.NewCallerMap()) + + thisEnt := ent{} + thisEnt.CallerID = c1.eventHandler.GetCallerMap().Register(thisEnt) + ch := make(chan struct{}) + s := collision.NewSpace(10, 10, 10, 10, thisEnt.CallerID) + event.Bind(c1.eventHandler, mouse.PressOn, thisEnt, func(ent, *mouse.Event) event.Response { + close(ch) return 0 }) - mouse.Add(s) - time.Sleep(200 * time.Millisecond) - c1.Propagate("MouseUpOn", mouse.NewEvent(15, 15, mouse.ButtonLeft, "MouseUp")) - time.Sleep(200 * time.Millisecond) - if triggered { - t.Fatalf("mouse up triggered binding") + c1.MouseTree = collision.NewTree() + c1.MouseTree.Add(s) + c1.Propagate(mouse.ReleaseOn, mouse.NewEvent(15, 15, mouse.ButtonLeft, mouse.Release)) + select { + case <-ch: + t.Fatalf("release propagation triggered press binding") + case <-time.After(1 * time.Second): } - time.Sleep(200 * time.Millisecond) - c1.Propagate("MouseDownOn", mouse.NewEvent(15, 15, mouse.ButtonLeft, "MouseDown")) - time.Sleep(200 * time.Millisecond) - if !triggered { - t.Fatalf("mouse down failed to trigger binding") + c1.Propagate(mouse.PressOn, mouse.NewEvent(15, 15, mouse.ButtonLeft, mouse.Press)) + select { + case <-time.After(2 * time.Second): + t.Fatalf("propagation failed to trigger press binding") + case <-ch: } }