Skip to content

Commit

Permalink
Make all async events pause the VM
Browse files Browse the repository at this point in the history
Aside from consistency, pausing on other events may be useful to show animations, etc.
  • Loading branch information
DrJosh9000 committed May 8, 2023
1 parent 65f521a commit ffe2578
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 99 deletions.
69 changes: 42 additions & 27 deletions async_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ func (s VMState) String() string {
return fmt.Sprintf("(invalid VMState %d)", s)
}

// VMStateMismatchErr is returned when AsyncAdapter is told to do something (either
// by the user calling Go, GoWithChoice, or Abort, or the VM calling a
// DialogueHandler method) but this requires AsyncAdapter to be in a different state
// than the state it is in.
// VMStateMismatchErr is returned when AsyncAdapter is told to do something
// (either by the user calling Go, GoWithChoice, or Abort, or the VM calling a
// DialogueHandler method) but this requires AsyncAdapter to be in a different
// state than the state it is in.
type VMStateMismatchErr struct {
// The VM was in state Got, but we wanted it to be in state Want in order
// to change it to state Next.
Expand All @@ -75,9 +75,8 @@ func (e VMStateMismatchErr) Error() string {

// AsyncAdapter is a DialogueHandler that exposes an interface that is similar
// to the mainline YarnSpinner VM dialogue handler. Instead of manually blocking
// inside the DialogueHandler Line, Options, and Command callbacks, AsyncAdapter
// does this for you, until you call Go, GoWithChoice, or Abort (as
// appropriate).
// inside the DialogueHandler callbacks, AsyncAdapter does this for you, until
// you call Go, GoWithChoice, or Abort (as appropriate).
type AsyncAdapter struct {
state atomic.Int32
handler AsyncDialogueHandler
Expand All @@ -90,9 +89,9 @@ func NewAsyncAdapter(h AsyncDialogueHandler) *AsyncAdapter {
handler: h,
// The user might call Go from within their handler's Line method
// (or however many other ways to try to continue the VM immediately).
// If ch was unbuffered, calling Go would wait forever trying to send on
// the channel, because AsyncAdapter only receives on ch after their method
// returns.
// If msgCh was unbuffered, calling Go would wait forever trying to send
// on the channel, because AsyncAdapter only receives on msgCh after
// their method returns.
msgCh: make(chan asyncMsg, 1),
}
}
Expand All @@ -105,8 +104,8 @@ func (a *AsyncAdapter) State() VMState {
func (a *AsyncAdapter) stateTransition(old, new int32) error {
if !a.state.CompareAndSwap(old, new) {
// This races (between CAS and a.State, something else could switch the
// state around). While I try to make the error maximally useful
// for debugging ... YOLO?
// state around). While I try to make the error maximally useful for
// debugging ... YOLO?
return VMStateMismatchErr{
Got: a.State(),
Want: VMState(old),
Expand All @@ -117,7 +116,8 @@ func (a *AsyncAdapter) stateTransition(old, new int32) error {
}

// Go will continue the VM after it has delivered any event (other than
// Options). If the VM is not waiting for an event, an error will be returned.
// Options). If the VM is not paused following any event other than Options, an
// error will be returned.
func (a *AsyncAdapter) Go() error {
if err := a.stateTransition(VMStatePaused, VMStateRunning); err != nil {
return err
Expand All @@ -127,7 +127,8 @@ func (a *AsyncAdapter) Go() error {
}

// GoWithChoice will continue the VM after it has delivered an Options event.
// Pass
// Pass the ID of the chosen option. If the VM is not paused following an
// Options event, an error will be returned.
func (a *AsyncAdapter) GoWithChoice(id int) error {
if err := a.stateTransition(VMStatePausedOptions, VMStateRunning); err != nil {
return err
Expand All @@ -139,7 +140,8 @@ func (a *AsyncAdapter) GoWithChoice(id int) error {
// Abort stops the VM with the given error as soon as possible (either within
// the current event, or on the next event). If a nil error is passed, Abort
// will replace it with Stop (so that NodeComplete and DialogueComplete still
// fire).
// fire). If the VM is already stopped (either through Abort, or after the
// DialogueComplete event) an error will be returned.
func (a *AsyncAdapter) Abort(err error) error {
if old := a.state.Swap(VMStateStopped); old == VMStateStopped {
return ErrAlreadyStopped
Expand Down Expand Up @@ -183,16 +185,22 @@ func (a *AsyncAdapter) waitForChoice() (int, error) {

// --- DialogueHandler implementation --- \\

// NodeStart is called by the VM and passed through to the
// AsyncDialogueHandler directly.
// NodeStart is called by the VM and blocks until Go or Abort is called.
func (a *AsyncAdapter) NodeStart(nodeName string) error {
return a.handler.NodeStart(nodeName)
if err := a.stateTransition(VMStateRunning, VMStatePaused); err != nil {
return err
}
a.handler.NodeStart(nodeName)
return a.waitForGo()
}

// PrepareForLines is called by the VM and passed through to the
// AsyncDialogueHandler directly.
// PrepareForLines is called by the VM and blocks until Go or Abort is called.
func (a *AsyncAdapter) PrepareForLines(lineIDs []string) error {
return a.handler.PrepareForLines(lineIDs)
if err := a.stateTransition(VMStateRunning, VMStatePaused); err != nil {
return err
}
a.handler.PrepareForLines(lineIDs)
return a.waitForGo()
}

// Line is called by the VM and blocks until Go or Abort is called.
Expand Down Expand Up @@ -222,16 +230,23 @@ func (a *AsyncAdapter) Command(command string) error {
return a.waitForGo()
}

// NodeComplete is called by the VM and passed through to the
// AsyncDialogueHandler directly.
// NodeComplete is called by the VM and blocks until Go or Abort is called.
func (a *AsyncAdapter) NodeComplete(nodeName string) error {
return a.handler.NodeComplete(nodeName)
if err := a.stateTransition(VMStateRunning, VMStatePaused); err != nil {
return err
}
a.handler.NodeComplete(nodeName)
return a.waitForGo()

}

// DialogueComplete is called by the VM and passed through to the
// AsyncDialogueHandler directly.
// DialogueComplete is called by the VM and blocks until Go or Abort is called.
func (a *AsyncAdapter) DialogueComplete() error {
return a.handler.DialogueComplete()
if err := a.stateTransition(VMStateRunning, VMStatePaused); err != nil {
return err
}
a.handler.DialogueComplete()
return a.waitForGo()
}

// --- AsyncAdapter messages --- \\
Expand Down
84 changes: 52 additions & 32 deletions async_adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,22 @@ type syncAdapter struct {
t *testing.T
}

func (s *syncAdapter) NodeStart(nodeName string) error {
return s.dh.NodeStart(nodeName)
func (s *syncAdapter) NodeStart(nodeName string) {
if err := s.dh.NodeStart(nodeName); err != nil {
s.t.Errorf("syncAdapter.DialogueHandler.NodeStart(%q) = %v", nodeName, err)
}
if err := s.aa.Go(); err != nil {
s.t.Errorf("syncAdapter.AsyncAdapter.Go() = %v", err)
}
}

func (s *syncAdapter) PrepareForLines(lineIDs []string) error {
return s.dh.PrepareForLines(lineIDs)
func (s *syncAdapter) PrepareForLines(lineIDs []string) {
if err := s.dh.PrepareForLines(lineIDs); err != nil {
s.t.Errorf("syncAdapter.DialogueHandler.PrepareForLines(%q) = %v", lineIDs, err)
}
if err := s.aa.Go(); err != nil {
s.t.Errorf("syncAdapter.AsyncAdapter.Go() = %v", err)
}
}

func (s *syncAdapter) Line(line Line) {
Expand Down Expand Up @@ -67,12 +77,22 @@ func (s *syncAdapter) Command(command string) {
}
}

func (s *syncAdapter) NodeComplete(nodeName string) error {
return s.dh.NodeComplete(nodeName)
func (s *syncAdapter) NodeComplete(nodeName string) {
if err := s.dh.NodeComplete(nodeName); err != nil {
s.t.Errorf("syncAdapter.DialogueHandler.NodeComplete(%q) = %v", nodeName, err)
}
if err := s.aa.Go(); err != nil {
s.t.Errorf("syncAdapter.AsyncAdapter.Go() = %v", err)
}
}

func (s *syncAdapter) DialogueComplete() error {
return s.dh.DialogueComplete()
func (s *syncAdapter) DialogueComplete() {
if err := s.dh.DialogueComplete(); err != nil {
s.t.Errorf("syncAdapter.DialogueHandler.DialogueComplete() = %v", err)
}
if err := s.aa.Go(); err != nil {
s.t.Errorf("syncAdapter.AsyncAdapter.Go() = %v", err)
}
}

func TestAllTestPlansAsync(t *testing.T) {
Expand Down Expand Up @@ -191,9 +211,9 @@ func (d *decoupledAsyncHandler) Command(command string) {
}
}

func (d *decoupledAsyncHandler) DialogueComplete() error {
func (d *decoupledAsyncHandler) DialogueComplete() {
close(d.eventCh)
return nil
d.AsyncAdapter.Go()
}

func TestAsyncAdapterWithDecoupledHandler(t *testing.T) {
Expand All @@ -207,6 +227,7 @@ func TestAsyncAdapterWithDecoupledHandler(t *testing.T) {
eventCh: make(chan decoupledEvent),
}
aa := NewAsyncAdapter(dh)
dh.AsyncAdapter = aa

vm := &VirtualMachine{
Program: prog,
Expand All @@ -233,25 +254,24 @@ func TestAsyncAdapterWithDecoupledHandler(t *testing.T) {
// immediateAsyncHandler calls Go and GoWithChoice within each event.
type immediateAsyncHandler struct {
FakeAsyncDialogueHandler
aa *AsyncAdapter
t *testing.T
t *testing.T
}

func (i *immediateAsyncHandler) Line(Line) {
if err := i.aa.Go(); err != nil {
if err := i.AsyncAdapter.Go(); err != nil {
i.t.Errorf("AsyncAdapter.Go() = %v", err)
}
}

func (i *immediateAsyncHandler) Options(options []Option) {
id := options[0].ID
if err := i.aa.GoWithChoice(id); err != nil {
if err := i.AsyncAdapter.GoWithChoice(id); err != nil {
i.t.Errorf("AsyncAdapter.GoWithChoice(%d) = %v", id, err)
}
}

func (i *immediateAsyncHandler) Command(string) {
if err := i.aa.Go(); err != nil {
if err := i.AsyncAdapter.Go(); err != nil {
i.t.Errorf("AsyncAdapter.Go() = %v", err)
}
}
Expand All @@ -263,9 +283,11 @@ func TestAsyncAdapterWithImmediateHandler(t *testing.T) {
t.Fatalf("LoadFiles(%q, en) = error %v", yarnc, err)
}

ah := &immediateAsyncHandler{t: t}
ah := &immediateAsyncHandler{
t: t,
}
aa := NewAsyncAdapter(ah)
ah.aa = aa
ah.AsyncAdapter = aa

vm := &VirtualMachine{
Program: prog,
Expand All @@ -285,8 +307,7 @@ func TestAsyncAdapterWithImmediateHandler(t *testing.T) {
// ones.
type badAsyncHandler struct {
FakeAsyncDialogueHandler
aa *AsyncAdapter
t *testing.T
t *testing.T
}

func (b *badAsyncHandler) Line(Line) {
Expand All @@ -295,11 +316,11 @@ func (b *badAsyncHandler) Line(Line) {
Want: VMStatePausedOptions,
Next: VMStateRunning,
}
if diff := cmp.Diff(b.aa.GoWithChoice(6), want); diff != "" {
if diff := cmp.Diff(b.AsyncAdapter.GoWithChoice(6), want); diff != "" {
b.t.Errorf("AsyncAdapter.GoWithChoice(6) error diff (-got +want):\n%s", diff)
}
// call Go to proceed, otherwise it hangs (it's waiting for Go, duh)
if err := b.aa.Go(); err != nil {
if err := b.AsyncAdapter.Go(); err != nil {
b.t.Errorf("AsyncAdapter.Go() = %v", err)
}
}
Expand All @@ -310,12 +331,12 @@ func (b *badAsyncHandler) Options(options []Option) {
Want: VMStatePaused,
Next: VMStateRunning,
}
if diff := cmp.Diff(b.aa.Go(), want); diff != "" {
if diff := cmp.Diff(b.AsyncAdapter.Go(), want); diff != "" {
b.t.Errorf("AsyncAdapter.Go() error diff (-got +want):\n%s", diff)
}
// call GoWithChoice to proceed, otherwise it hangs
choice := options[0].ID
if err := b.aa.GoWithChoice(choice); err != nil {
if err := b.AsyncAdapter.GoWithChoice(choice); err != nil {
b.t.Errorf("AsyncAdapter.GoWithChoice(%d) = %v", choice, err)
}
}
Expand All @@ -326,11 +347,11 @@ func (b *badAsyncHandler) Command(string) {
Want: VMStatePausedOptions,
Next: VMStateRunning,
}
if diff := cmp.Diff(b.aa.GoWithChoice(0), want); diff != "" {
if diff := cmp.Diff(b.AsyncAdapter.GoWithChoice(0), want); diff != "" {
b.t.Errorf("AsyncAdapter.GoWithChoice(0) error diff (-got +want):\n%s", diff)
}
// pass Go, collect $200
if err := b.aa.Go(); err != nil {
if err := b.AsyncAdapter.Go(); err != nil {
b.t.Errorf("AsyncAdapter.Go() = %v", err)
}
}
Expand All @@ -344,7 +365,7 @@ func TestAsyncAdapterWithBadHandler(t *testing.T) {

bh := &badAsyncHandler{t: t}
aa := NewAsyncAdapter(bh)
bh.aa = aa
bh.AsyncAdapter = aa

vm := &VirtualMachine{
Program: prog,
Expand All @@ -365,24 +386,23 @@ var errDummy = errors.New("abort! abort!")
// abortAsyncHandler calls Abort within each event.
type abortAsyncHandler struct {
FakeAsyncDialogueHandler
aa *AsyncAdapter
t *testing.T
t *testing.T
}

func (a *abortAsyncHandler) Line(Line) {
if err := a.aa.Abort(errDummy); err != nil {
if err := a.AsyncAdapter.Abort(errDummy); err != nil {
a.t.Errorf("AsyncAdapter.Abort(errDummy) = %v", err)
}
}

func (a *abortAsyncHandler) Options(options []Option) {
if err := a.aa.Abort(errDummy); err != nil {
if err := a.AsyncAdapter.Abort(errDummy); err != nil {
a.t.Errorf("AsyncAdapter.Abort(errDummy) = %v", err)
}
}

func (a *abortAsyncHandler) Command(string) {
if err := a.aa.Abort(errDummy); err != nil {
if err := a.AsyncAdapter.Abort(errDummy); err != nil {
a.t.Errorf("AsyncAdapter.Abort(errDummy) = %v", err)
}
}
Expand All @@ -396,7 +416,7 @@ func TestAsyncAdapterWithAbortHandler(t *testing.T) {

bh := &abortAsyncHandler{t: t}
aa := NewAsyncAdapter(bh)
bh.aa = aa
bh.AsyncAdapter = aa

vm := &VirtualMachine{
Program: prog,
Expand Down
Loading

0 comments on commit ffe2578

Please sign in to comment.