@@ -3,6 +3,7 @@ package cardinal
33import (
44 "context"
55 "net/http"
6+ "sync/atomic"
67 "time"
78
89 "connectrpc.com/connect"
@@ -17,10 +18,31 @@ import (
1718 "github.com/argus-labs/world-engine/proto/gen/go/worldengine/cardinal/v1/cardinalv1connect"
1819)
1920
21+ // tickControl manages pause/resume/step/reset signaling for the tick loop.
22+ type tickControl struct {
23+ pauseCh chan chan uint64 // Request pause, receives tick height when paused
24+ resumeCh chan struct {} // Signal to resume
25+ stepCh chan chan uint64 // Request step, receives tick height after step
26+ resetCh chan chan error // Request reset, receives error result
27+ isPaused atomic.Bool // Current pause state
28+ stepReady chan struct {} // Signals that step result is ready to be read
29+ }
30+
31+ func newTickControl () * tickControl {
32+ return & tickControl {
33+ pauseCh : make (chan chan uint64 ),
34+ resumeCh : make (chan struct {}),
35+ stepCh : make (chan chan uint64 ),
36+ resetCh : make (chan chan error ),
37+ stepReady : make (chan struct {}),
38+ }
39+ }
40+
2041// TODO: add tick log here.
2142type debugModule struct {
2243 world * World
2344 server * http.Server
45+ control * tickControl
2446 reflector * jsonschema.Reflector
2547 commands map [string ]* structpb.Struct
2648 events map [string ]* structpb.Struct
@@ -30,6 +52,7 @@ type debugModule struct {
3052func newDebugModule (world * World ) debugModule {
3153 return debugModule {
3254 world : world ,
55+ control : newTickControl (),
3356 commands : make (map [string ]* structpb.Struct ),
3457 events : make (map [string ]* structpb.Struct ),
3558 components : make (map [string ]* structpb.Struct ),
@@ -144,3 +167,88 @@ func (d *debugModule) buildTypeSchemas(cache map[string]*structpb.Struct) []*car
144167 }
145168 return schemas
146169}
170+
171+ // Pause stops tick execution and returns the current tick height.
172+ func (d * debugModule ) Pause (
173+ _ context.Context ,
174+ _ * connect.Request [cardinalv1.PauseRequest ],
175+ ) (* connect.Response [cardinalv1.PauseResponse ], error ) {
176+ if d .control .isPaused .Load () {
177+ return nil , connect .NewError (connect .CodeFailedPrecondition , eris .New ("world is already paused" ))
178+ }
179+
180+ replyCh := make (chan uint64 , 1 )
181+ d .control .pauseCh <- replyCh
182+ tickHeight := <- replyCh
183+
184+ return connect .NewResponse (& cardinalv1.PauseResponse {
185+ TickHeight : tickHeight ,
186+ }), nil
187+ }
188+
189+ // Resume continues tick execution after a pause.
190+ func (d * debugModule ) Resume (
191+ _ context.Context ,
192+ _ * connect.Request [cardinalv1.ResumeRequest ],
193+ ) (* connect.Response [cardinalv1.ResumeResponse ], error ) {
194+ if ! d .control .isPaused .Load () {
195+ return nil , connect .NewError (connect .CodeFailedPrecondition , eris .New ("world is not paused" ))
196+ }
197+
198+ d .control .resumeCh <- struct {}{}
199+
200+ return connect .NewResponse (& cardinalv1.ResumeResponse {}), nil
201+ }
202+
203+ // Step executes a single tick. Only works when paused.
204+ func (d * debugModule ) Step (
205+ _ context.Context ,
206+ _ * connect.Request [cardinalv1.StepRequest ],
207+ ) (* connect.Response [cardinalv1.StepResponse ], error ) {
208+ if ! d .control .isPaused .Load () {
209+ return nil , connect .NewError (connect .CodeFailedPrecondition , eris .New ("world must be paused to step" ))
210+ }
211+
212+ replyCh := make (chan uint64 , 1 )
213+ d .control .stepCh <- replyCh
214+ tickHeight := <- replyCh
215+
216+ return connect .NewResponse (& cardinalv1.StepResponse {
217+ TickHeight : tickHeight ,
218+ }), nil
219+ }
220+
221+ // Reset restores the world to its initial state (before tick 0).
222+ func (d * debugModule ) Reset (
223+ _ context.Context ,
224+ _ * connect.Request [cardinalv1.ResetRequest ],
225+ ) (* connect.Response [cardinalv1.ResetResponse ], error ) {
226+ if ! d .control .isPaused .Load () {
227+ return nil , connect .NewError (connect .CodeFailedPrecondition , eris .New ("world must be paused to reset" ))
228+ }
229+
230+ replyCh := make (chan error , 1 )
231+ d .control .resetCh <- replyCh
232+ if err := <- replyCh ; err != nil {
233+ return nil , connect .NewError (connect .CodeInternal , err )
234+ }
235+
236+ return connect .NewResponse (& cardinalv1.ResetResponse {}), nil
237+ }
238+
239+ // GetState returns the current world state snapshot.
240+ func (d * debugModule ) GetState (
241+ _ context.Context ,
242+ _ * connect.Request [cardinalv1.GetStateRequest ],
243+ ) (* connect.Response [cardinalv1.GetStateResponse ], error ) {
244+ snapshot , err := d .world .world .SerializeToProto ()
245+ if err != nil {
246+ return nil , connect .NewError (connect .CodeInternal , eris .Wrap (err , "failed to serialize world state" ))
247+ }
248+
249+ return connect .NewResponse (& cardinalv1.GetStateResponse {
250+ TickHeight : d .world .currentTick .height ,
251+ IsPaused : d .control .isPaused .Load (),
252+ Snapshot : snapshot ,
253+ }), nil
254+ }
0 commit comments