diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e6064e5..7fa9f9fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,8 @@ set(ROLLER_GAME_BASE_SOURCES PROJECTS/ROLLER/comms.c PROJECTS/ROLLER/control.c PROJECTS/ROLLER/date.c + PROJECTS/ROLLER/fsm.c + PROJECTS/ROLLER/fsm_stack.c PROJECTS/ROLLER/drawtrk3.c PROJECTS/ROLLER/editor_camera.c PROJECTS/ROLLER/editor_api.c diff --git a/PROJECTS/ROLLER/frontend.c b/PROJECTS/ROLLER/frontend.c index f16a551f..c3e2c5f8 100644 --- a/PROJECTS/ROLLER/frontend.c +++ b/PROJECTS/ROLLER/frontend.c @@ -1,4 +1,6 @@ #include "frontend.h" +#include "fsm.h" +#include "fsm_stack.h" //------------------------------------------------------------------------------------------------- @@ -21,8 +23,10 @@ eFrontendState eFrontendNextState = eFRONTEND_STATE_NONE; #define OVERLAY_STACK_DEPTH 4 -static eFrontendState aOverlayStack[OVERLAY_STACK_DEPTH]; -static int iOverlayStackTop = 0; +static void frontend_fsm_enter(void *pContext); +static void frontend_fsm_update(void *pContext); +static void frontend_fsm_draw(void *pContext); +static void frontend_fsm_exit(void *pContext); static const tFrontendScreen aScreens[eFRONTEND_STATE_QUIT + 1] = { [eFRONTEND_STATE_COPYRIGHT] = { @@ -71,12 +75,30 @@ static const tFrontendScreen aScreens[eFRONTEND_STATE_QUIT + 1] = { [eFRONTEND_STATE_SHUTDOWN] = { frontend_shutdown_enter, frontend_shutdown_update, NULL, NULL }, }; +#define FRONTEND_STATE_COUNT ((int)(sizeof(aScreens) / sizeof(aScreens[0]))) + +static int aOverlayStack[OVERLAY_STACK_DEPTH]; +static FsmMachine sFrontendMachine = { + NULL, + { frontend_fsm_enter, frontend_fsm_update, frontend_fsm_draw, frontend_fsm_exit }, + FRONTEND_STATE_COUNT, + eFRONTEND_STATE_NONE, + eFRONTEND_STATE_NONE, + NULL +}; +static FsmStack sFrontendOverlayStack = { + &sFrontendMachine, + aOverlayStack, + OVERLAY_STACK_DEPTH, + 0 +}; + //------------------------------------------------------------------------------------------------- static int frontend_state_is_valid(eFrontendState eState) { return eState >= eFRONTEND_STATE_NONE && - eState < (eFrontendState)(sizeof(aScreens) / sizeof(aScreens[0])); + eState < (eFrontendState)FRONTEND_STATE_COUNT; } //------------------------------------------------------------------------------------------------- @@ -94,87 +116,131 @@ static eFrontendState frontend_resolve_state(eFrontendState eState) //------------------------------------------------------------------------------------------------- -static void frontend_exit_state(eFrontendState eState) +static void frontend_sync_legacy_globals(void) { - if (frontend_state_is_valid(eState) && aScreens[eState].pfnExit) - aScreens[eState].pfnExit(); + eFrontendCurrentState = (eFrontendState)fsm_current(&sFrontendMachine); + eFrontendNextState = (eFrontendState)fsm_pending(&sFrontendMachine); } //------------------------------------------------------------------------------------------------- -void frontend_set_state(eFrontendState eState) +static const tFrontendScreen *frontend_current_screen(void) { - eState = frontend_resolve_state(eState); + eFrontendState eState = (eFrontendState)fsm_current(&sFrontendMachine); - if (eState == eFrontendCurrentState) { - eFrontendNextState = eState; - return; - } + if (!frontend_state_is_valid(eState)) + return NULL; - frontend_exit_state(eFrontendCurrentState); - while (iOverlayStackTop > 0) - frontend_exit_state(aOverlayStack[--iOverlayStackTop]); + return &aScreens[eState]; +} - eFrontendCurrentState = eState; - eFrontendNextState = eState; +//------------------------------------------------------------------------------------------------- - if (aScreens[eFrontendCurrentState].pfnEnter) - aScreens[eFrontendCurrentState].pfnEnter(); +static void frontend_fsm_enter(void *pContext) +{ + const tFrontendScreen *pScreen; + (void)pContext; + + frontend_sync_legacy_globals(); + pScreen = frontend_current_screen(); + if (pScreen && pScreen->pfnEnter) + pScreen->pfnEnter(); } //------------------------------------------------------------------------------------------------- -void frontend_update(void) +static void frontend_fsm_update(void *pContext) { - if (eFrontendNextState != eFrontendCurrentState) - frontend_set_state(eFrontendNextState); + const tFrontendScreen *pScreen; + eFrontendState eRequestedState; + (void)pContext; + + frontend_sync_legacy_globals(); + pScreen = frontend_current_screen(); + if (pScreen && pScreen->pfnUpdate) + pScreen->pfnUpdate(); + + eRequestedState = frontend_resolve_state(eFrontendNextState); + if (eRequestedState != (eFrontendState)fsm_pending(&sFrontendMachine)) + (void)fsm_request(&sFrontendMachine, eRequestedState); + frontend_sync_legacy_globals(); +} - if (!frontend_state_is_valid(eFrontendCurrentState)) - return; +//------------------------------------------------------------------------------------------------- + +static void frontend_fsm_draw(void *pContext) +{ + const tFrontendScreen *pScreen; + (void)pContext; + + frontend_sync_legacy_globals(); + pScreen = frontend_current_screen(); + if (pScreen && pScreen->pfnDraw) + pScreen->pfnDraw(); +} - if (aScreens[eFrontendCurrentState].pfnUpdate) - aScreens[eFrontendCurrentState].pfnUpdate(); +//------------------------------------------------------------------------------------------------- + +static void frontend_fsm_exit(void *pContext) +{ + const tFrontendScreen *pScreen; + (void)pContext; + + frontend_sync_legacy_globals(); + pScreen = frontend_current_screen(); + if (pScreen && pScreen->pfnExit) + pScreen->pfnExit(); +} + +//------------------------------------------------------------------------------------------------- + +void frontend_set_state(eFrontendState eState) +{ + eState = frontend_resolve_state(eState); - if (eFrontendNextState != eFrontendCurrentState) { - frontend_set_state(eFrontendNextState); + if (eState == (eFrontendState)fsm_current(&sFrontendMachine)) { + (void)fsm_request(&sFrontendMachine, eState); + frontend_sync_legacy_globals(); return; } - if (aScreens[eFrontendCurrentState].pfnDraw) - aScreens[eFrontendCurrentState].pfnDraw(); + while (fsm_stack_count(&sFrontendOverlayStack) > 0) + (void)fsm_stack_pop(&sFrontendOverlayStack); + (void)fsm_transition(&sFrontendMachine, eState); + frontend_sync_legacy_globals(); } //------------------------------------------------------------------------------------------------- -void push_overlay(eFrontendState eOverlay) +void frontend_update(void) { - if (iOverlayStackTop >= OVERLAY_STACK_DEPTH || - !frontend_state_is_valid(eOverlay)) - return; + eFrontendState eRequestedState; - // Preserve current state WITHOUT calling its exit callback — it stays logically active. - aOverlayStack[iOverlayStackTop++] = eFrontendCurrentState; - eFrontendCurrentState = eOverlay; - eFrontendNextState = eOverlay; + eRequestedState = frontend_resolve_state(eFrontendNextState); + if (eRequestedState != (eFrontendState)fsm_pending(&sFrontendMachine)) + (void)fsm_request(&sFrontendMachine, eRequestedState); - if (aScreens[eFrontendCurrentState].pfnEnter) - aScreens[eFrontendCurrentState].pfnEnter(); + (void)fsm_step(&sFrontendMachine); + frontend_sync_legacy_globals(); } //------------------------------------------------------------------------------------------------- -void pop_overlay(void) +void push_overlay(eFrontendState eOverlay) { - if (iOverlayStackTop <= 0) + if (!frontend_state_is_valid(eOverlay)) return; - // Exit the overlay WITHOUT calling the restored state's enter callback. - if (frontend_state_is_valid(eFrontendCurrentState) && - aScreens[eFrontendCurrentState].pfnExit) - aScreens[eFrontendCurrentState].pfnExit(); + if (fsm_stack_push(&sFrontendOverlayStack, eOverlay) == FSM_OK) + frontend_sync_legacy_globals(); +} + +//------------------------------------------------------------------------------------------------- - eFrontendCurrentState = aOverlayStack[--iOverlayStackTop]; - eFrontendNextState = eFrontendCurrentState; +void pop_overlay(void) +{ + if (fsm_stack_pop(&sFrontendOverlayStack) == FSM_OK) + frontend_sync_legacy_globals(); } //------------------------------------------------------------------------------------------------- diff --git a/PROJECTS/ROLLER/fsm.c b/PROJECTS/ROLLER/fsm.c new file mode 100644 index 00000000..bbc2cd3b --- /dev/null +++ b/PROJECTS/ROLLER/fsm.c @@ -0,0 +1,223 @@ +#include "fsm.h" + +static void fsm_clear(FsmMachine *machine) +{ + if (!machine) + return; + + machine->states = 0; + machine->uniform_state.enter = 0; + machine->uniform_state.update = 0; + machine->uniform_state.draw = 0; + machine->uniform_state.exit = 0; + machine->state_count = 0; + machine->current_state = -1; + machine->pending_state = -1; + machine->context = 0; +} + +static FsmStepResult fsm_empty_result(FsmError error) +{ + FsmStepResult result = { 0, -1, -1, error }; + return result; +} + +int fsm_state_is_valid(const FsmMachine *machine, int state) +{ + return machine && machine->state_count > 0 && + state >= 0 && state < machine->state_count; +} + +static const FsmState *fsm_state_at(const FsmMachine *machine, int state) +{ + if (!fsm_state_is_valid(machine, state)) + return 0; + + if (machine->states) + return &machine->states[state]; + + return &machine->uniform_state; +} + +static FsmStepResult fsm_apply_transition(FsmMachine *machine, int target_state) +{ + FsmStepResult result; + const FsmState *old_state; + const FsmState *new_state; + + if (!machine) + return fsm_empty_result(FSM_ERROR_INVALID_ARGUMENT); + + old_state = fsm_state_at(machine, machine->current_state); + new_state = fsm_state_at(machine, target_state); + if (!old_state || !new_state) + return fsm_empty_result(FSM_ERROR_INVALID_STATE); + + result = fsm_empty_result(FSM_OK); + if (target_state == machine->current_state) { + machine->pending_state = target_state; + return result; + } + + result.transitioned = 1; + result.from_state = machine->current_state; + result.to_state = target_state; + + if (old_state->exit) + old_state->exit(machine->context); + + machine->current_state = target_state; + machine->pending_state = target_state; + + if (new_state->enter) + new_state->enter(machine->context); + + return result; +} + +static FsmError fsm_init_common(FsmMachine *machine, + const FsmState *states, + const FsmState *uniform_state, + int state_count, + int initial_state, + void *context) +{ + const FsmState *initial; + + if (!machine) + return FSM_ERROR_INVALID_ARGUMENT; + + fsm_clear(machine); + + if ((!states && !uniform_state) || state_count <= 0) + return FSM_ERROR_INVALID_ARGUMENT; + + if (initial_state < 0 || initial_state >= state_count) + return FSM_ERROR_INVALID_STATE; + + machine->states = states; + if (uniform_state) + machine->uniform_state = *uniform_state; + machine->state_count = state_count; + machine->current_state = initial_state; + machine->pending_state = initial_state; + machine->context = context; + + initial = fsm_state_at(machine, initial_state); + if (initial && initial->enter) + initial->enter(context); + + return FSM_OK; +} + +FsmError fsm_init(FsmMachine *machine, + const FsmState *states, + int state_count, + int initial_state, + void *context) +{ + return fsm_init_common(machine, states, 0, state_count, initial_state, context); +} + +FsmError fsm_init_uniform(FsmMachine *machine, + const FsmState *state, + int state_count, + int initial_state, + void *context) +{ + return fsm_init_common(machine, 0, state, state_count, initial_state, context); +} + +FsmError fsm_request(FsmMachine *machine, int target_state) +{ + if (!machine) + return FSM_ERROR_INVALID_ARGUMENT; + + if (!fsm_state_is_valid(machine, target_state)) + return FSM_ERROR_INVALID_STATE; + + machine->pending_state = target_state; + return FSM_OK; +} + +FsmStepResult fsm_transition(FsmMachine *machine, int target_state) +{ + return fsm_apply_transition(machine, target_state); +} + +FsmError fsm_enter_current_state(FsmMachine *machine) +{ + const FsmState *state; + + if (!machine) + return FSM_ERROR_INVALID_ARGUMENT; + + state = fsm_state_at(machine, machine->current_state); + if (!state) + return FSM_ERROR_INVALID_STATE; + + if (state->enter) + state->enter(machine->context); + + return FSM_OK; +} + +FsmError fsm_exit_current_state(FsmMachine *machine) +{ + const FsmState *state; + + if (!machine) + return FSM_ERROR_INVALID_ARGUMENT; + + state = fsm_state_at(machine, machine->current_state); + if (!state) + return FSM_ERROR_INVALID_STATE; + + if (state->exit) + state->exit(machine->context); + + return FSM_OK; +} + +FsmStepResult fsm_step(FsmMachine *machine) +{ + FsmStepResult result = { 0, -1, -1, FSM_OK }; + const FsmState *state; + + if (!machine) { + result.error = FSM_ERROR_INVALID_ARGUMENT; + return result; + } + + if (!fsm_state_is_valid(machine, machine->current_state) || + !fsm_state_is_valid(machine, machine->pending_state)) { + result.error = FSM_ERROR_INVALID_STATE; + return result; + } + + if (machine->pending_state != machine->current_state) { + result = fsm_apply_transition(machine, machine->pending_state); + if (result.error != FSM_OK) + return result; + } + + state = fsm_state_at(machine, machine->current_state); + if (state && state->update) + state->update(machine->context); + + state = fsm_state_at(machine, machine->current_state); + if (machine->pending_state == machine->current_state && state && state->draw) + state->draw(machine->context); + + return result; +} + +int fsm_current(const FsmMachine *machine) +{ + return machine ? machine->current_state : -1; +} + +int fsm_pending(const FsmMachine *machine) +{ + return machine ? machine->pending_state : -1; +} diff --git a/PROJECTS/ROLLER/fsm.h b/PROJECTS/ROLLER/fsm.h new file mode 100644 index 00000000..ca32efc9 --- /dev/null +++ b/PROJECTS/ROLLER/fsm.h @@ -0,0 +1,67 @@ +#ifndef ROLLER_FSM_H +#define ROLLER_FSM_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*FsmEnterFn)(void *context); +typedef void (*FsmUpdateFn)(void *context); +typedef void (*FsmDrawFn)(void *context); +typedef void (*FsmExitFn)(void *context); + +typedef struct { + FsmEnterFn enter; + FsmUpdateFn update; + FsmDrawFn draw; + FsmExitFn exit; +} FsmState; + +typedef enum { + FSM_OK = 0, + FSM_ERROR_INVALID_ARGUMENT, + FSM_ERROR_INVALID_STATE, + FSM_ERROR_STACK_FULL, + FSM_ERROR_STACK_EMPTY +} FsmError; + +typedef struct { + int transitioned; + int from_state; + int to_state; + FsmError error; +} FsmStepResult; + +typedef struct { + const FsmState *states; + FsmState uniform_state; + int state_count; + int current_state; + int pending_state; + void *context; +} FsmMachine; + +FsmError fsm_init(FsmMachine *machine, + const FsmState *states, + int state_count, + int initial_state, + void *context); +FsmError fsm_init_uniform(FsmMachine *machine, + const FsmState *state, + int state_count, + int initial_state, + void *context); +FsmError fsm_request(FsmMachine *machine, int target_state); +FsmStepResult fsm_transition(FsmMachine *machine, int target_state); +FsmError fsm_enter_current_state(FsmMachine *machine); +FsmError fsm_exit_current_state(FsmMachine *machine); +FsmStepResult fsm_step(FsmMachine *machine); +int fsm_current(const FsmMachine *machine); +int fsm_pending(const FsmMachine *machine); +int fsm_state_is_valid(const FsmMachine *machine, int state); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/PROJECTS/ROLLER/fsm_stack.c b/PROJECTS/ROLLER/fsm_stack.c new file mode 100644 index 00000000..c50d2c07 --- /dev/null +++ b/PROJECTS/ROLLER/fsm_stack.c @@ -0,0 +1,75 @@ +#include "fsm_stack.h" + +FsmError fsm_stack_init(FsmStack *stack, + FsmMachine *machine, + int *storage, + int capacity) +{ + if (!stack || !machine || !storage || capacity <= 0) + return FSM_ERROR_INVALID_ARGUMENT; + + stack->machine = machine; + stack->states = storage; + stack->capacity = capacity; + stack->count = 0; + return FSM_OK; +} + +FsmError fsm_stack_push(FsmStack *stack, int overlay_state) +{ + FsmMachine *machine; + + if (!stack || !stack->machine || !stack->states) + return FSM_ERROR_INVALID_ARGUMENT; + + machine = stack->machine; + if (!fsm_state_is_valid(machine, overlay_state)) + return FSM_ERROR_INVALID_STATE; + + if (stack->count >= stack->capacity) + return FSM_ERROR_STACK_FULL; + + if (!fsm_state_is_valid(machine, machine->current_state)) + return FSM_ERROR_INVALID_STATE; + + stack->states[stack->count++] = machine->current_state; + machine->current_state = overlay_state; + machine->pending_state = overlay_state; + + return fsm_enter_current_state(machine); +} + +FsmError fsm_stack_pop(FsmStack *stack) +{ + FsmMachine *machine; + FsmError exit_error; + int restored_state; + + if (!stack || !stack->machine || !stack->states) + return FSM_ERROR_INVALID_ARGUMENT; + + if (stack->count <= 0) + return FSM_ERROR_STACK_EMPTY; + + machine = stack->machine; + if (!fsm_state_is_valid(machine, machine->current_state)) + return FSM_ERROR_INVALID_STATE; + + exit_error = fsm_exit_current_state(machine); + if (exit_error != FSM_OK) + return exit_error; + + restored_state = stack->states[--stack->count]; + if (!fsm_state_is_valid(machine, restored_state)) + return FSM_ERROR_INVALID_STATE; + + machine->current_state = restored_state; + machine->pending_state = restored_state; + return FSM_OK; +} + +int fsm_stack_count(const FsmStack *stack) +{ + return stack ? stack->count : 0; +} + diff --git a/PROJECTS/ROLLER/fsm_stack.h b/PROJECTS/ROLLER/fsm_stack.h new file mode 100644 index 00000000..b025c4fa --- /dev/null +++ b/PROJECTS/ROLLER/fsm_stack.h @@ -0,0 +1,29 @@ +#ifndef ROLLER_FSM_STACK_H +#define ROLLER_FSM_STACK_H + +#include "fsm.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + FsmMachine *machine; + int *states; + int capacity; + int count; +} FsmStack; + +FsmError fsm_stack_init(FsmStack *stack, + FsmMachine *machine, + int *storage, + int capacity); +FsmError fsm_stack_push(FsmStack *stack, int overlay_state); +FsmError fsm_stack_pop(FsmStack *stack); +int fsm_stack_count(const FsmStack *stack); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/build.zig b/build.zig index 6ec93c9a..49b0fa56 100644 --- a/build.zig +++ b/build.zig @@ -74,6 +74,8 @@ pub fn build(b: *std.Build) void { "PROJECTS/ROLLER/comms.c", "PROJECTS/ROLLER/control.c", "PROJECTS/ROLLER/date.c", + "PROJECTS/ROLLER/fsm.c", + "PROJECTS/ROLLER/fsm_stack.c", "PROJECTS/ROLLER/drawtrk3.c", "PROJECTS/ROLLER/editor_camera.c", "PROJECTS/ROLLER/editor_api.c", @@ -374,6 +376,32 @@ fn configureRenderQueue3DTests( ); render_queue_tests.dependOn(&run_unit.step); + const fsm_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + }); + fsm_mod.addIncludePath(b.path("PROJECTS/ROLLER")); + fsm_mod.addCSourceFiles(.{ + .flags = c_flags, + .files = &.{ + "PROJECTS/ROLLER/fsm.c", + "PROJECTS/ROLLER/fsm_stack.c", + "tests/fsm_test.c", + }, + }); + const fsm_exe = b.addExecutable(.{ + .name = "fsm_test", + .root_module = fsm_mod, + }); + const run_fsm = b.addRunArtifact(fsm_exe); + + const fsm_tests = b.step( + "test-fsm", + "Run canonical FSM runner and stack tests", + ); + fsm_tests.dependOn(&run_fsm.step); + const sdl_image = b.dependency("SDL_image", .{ .target = target, .optimize = optimize, @@ -630,6 +658,7 @@ fn configureRenderQueue3DTests( const test_step = b.step("test", "Run focused unit tests and optional seam checks"); test_step.dependOn(render_queue_tests); + test_step.dependOn(fsm_tests); test_step.dependOn(tick_clock_tests); const roller_core_manifest_check = b.addSystemCommand(&.{ diff --git a/roller-core.srclist b/roller-core.srclist index 8f47b4e0..807c4deb 100644 --- a/roller-core.srclist +++ b/roller-core.srclist @@ -53,6 +53,8 @@ KEEP|PROJECTS/ROLLER/tower.c KEEP|PROJECTS/ROLLER/plans.c KEEP|PROJECTS/ROLLER/carplans.c KEEP|PROJECTS/ROLLER/date.c +KEEP|PROJECTS/ROLLER/fsm.c +KEEP|PROJECTS/ROLLER/fsm_stack.c KEEP|PROJECTS/ROLLER/png_writer.c KEEP|PROJECTS/ROLLER/function.c KEEP|PROJECTS/ROLLER/func2.c diff --git a/tests/fsm_test.c b/tests/fsm_test.c new file mode 100644 index 00000000..143f4dc5 --- /dev/null +++ b/tests/fsm_test.c @@ -0,0 +1,291 @@ +#include "fsm.h" +#include "fsm_stack.h" + +#include +#include + +#define TEST_STATE_COUNT 4 +#define LOG_CAPACITY 64 + +typedef struct { + char log[LOG_CAPACITY]; + int log_len; + FsmMachine *machine; + FsmStack *stack; + int request_from_update; + int request_target; +} TestContext; + +static void append_log(TestContext *context, char entry) +{ + assert(context->log_len < LOG_CAPACITY - 1); + context->log[context->log_len++] = entry; + context->log[context->log_len] = '\0'; +} + +static void enter_a(void *context) { append_log((TestContext *)context, 'A'); } +static void update_a(void *context) { append_log((TestContext *)context, 'a'); } +static void draw_a(void *context) { append_log((TestContext *)context, '1'); } +static void exit_a(void *context) { append_log((TestContext *)context, 'x'); } + +static void enter_b(void *context) { append_log((TestContext *)context, 'B'); } +static void draw_b(void *context) { append_log((TestContext *)context, '2'); } +static void exit_b(void *context) { append_log((TestContext *)context, 'y'); } + +static void update_b(void *context) +{ + TestContext *test_context = (TestContext *)context; + append_log(test_context, 'b'); + if (test_context->request_from_update) + assert(fsm_request(test_context->machine, test_context->request_target) == FSM_OK); + + if (test_context->stack) + assert(fsm_stack_pop(test_context->stack) == FSM_OK); +} + +static const FsmState test_states[TEST_STATE_COUNT] = { + { enter_a, update_a, draw_a, exit_a }, + { enter_b, update_b, draw_b, exit_b }, + { NULL, NULL, NULL, NULL }, + { NULL, NULL, NULL, NULL }, +}; + +static TestContext new_context(void) +{ + TestContext context; + memset(&context, 0, sizeof(context)); + context.request_target = -1; + return context; +} + +static void test_init_enters_initial_state(void) +{ + FsmMachine machine; + TestContext context = new_context(); + + assert(fsm_init(&machine, test_states, TEST_STATE_COUNT, 0, &context) == FSM_OK); + + assert(fsm_current(&machine) == 0); + assert(fsm_pending(&machine) == 0); + assert(strcmp(context.log, "A") == 0); +} + +static void test_request_is_deferred_until_step(void) +{ + FsmMachine machine; + TestContext context = new_context(); + + assert(fsm_init(&machine, test_states, TEST_STATE_COUNT, 0, &context) == FSM_OK); + assert(fsm_request(&machine, 1) == FSM_OK); + + assert(fsm_current(&machine) == 0); + assert(fsm_pending(&machine) == 1); + assert(strcmp(context.log, "A") == 0); +} + +static void test_uniform_state_profile_uses_same_callbacks_for_all_states(void) +{ + FsmMachine machine; + FsmStepResult result; + TestContext context = new_context(); + FsmState uniform_state = { enter_a, update_a, draw_a, exit_a }; + + assert(fsm_init_uniform(&machine, &uniform_state, TEST_STATE_COUNT, 0, &context) == FSM_OK); + assert(fsm_request(&machine, 2) == FSM_OK); + + result = fsm_step(&machine); + + assert(result.error == FSM_OK); + assert(result.transitioned); + assert(result.from_state == 0); + assert(result.to_state == 2); + assert(fsm_current(&machine) == 2); + assert(fsm_pending(&machine) == 2); + assert(strcmp(context.log, "AxAa1") == 0); +} + + +static void test_transition_applies_immediately_without_update_or_draw(void) +{ + FsmMachine machine; + FsmStepResult result; + TestContext context = new_context(); + + assert(fsm_init(&machine, test_states, TEST_STATE_COUNT, 0, &context) == FSM_OK); + + result = fsm_transition(&machine, 1); + + assert(result.error == FSM_OK); + assert(result.transitioned); + assert(result.from_state == 0); + assert(result.to_state == 1); + assert(fsm_current(&machine) == 1); + assert(fsm_pending(&machine) == 1); + assert(strcmp(context.log, "AxB") == 0); +} + +static void test_step_applies_transition_before_update_and_draw(void) +{ + FsmMachine machine; + FsmStepResult result; + TestContext context = new_context(); + + assert(fsm_init(&machine, test_states, TEST_STATE_COUNT, 0, &context) == FSM_OK); + assert(fsm_request(&machine, 1) == FSM_OK); + + result = fsm_step(&machine); + + assert(result.error == FSM_OK); + assert(result.transitioned); + assert(result.from_state == 0); + assert(result.to_state == 1); + assert(fsm_current(&machine) == 1); + assert(fsm_pending(&machine) == 1); + assert(strcmp(context.log, "AxBb2") == 0); +} + +static void test_step_skips_draw_when_update_requests_transition(void) +{ + FsmMachine machine; + FsmStepResult result; + TestContext context = new_context(); + + assert(fsm_init(&machine, test_states, TEST_STATE_COUNT, 1, &context) == FSM_OK); + context.machine = &machine; + context.request_from_update = 1; + context.request_target = 0; + + result = fsm_step(&machine); + + assert(result.error == FSM_OK); + assert(!result.transitioned); + assert(fsm_current(&machine) == 1); + assert(fsm_pending(&machine) == 0); + assert(strcmp(context.log, "Bb") == 0); +} + +static void test_step_draws_restored_state_after_overlay_pop(void) +{ + FsmMachine machine; + FsmStack stack; + int storage[1]; + FsmStepResult result; + TestContext context = new_context(); + + assert(fsm_init(&machine, test_states, TEST_STATE_COUNT, 0, &context) == FSM_OK); + assert(fsm_stack_init(&stack, &machine, storage, 1) == FSM_OK); + assert(fsm_stack_push(&stack, 1) == FSM_OK); + context.stack = &stack; + + result = fsm_step(&machine); + + assert(result.error == FSM_OK); + assert(!result.transitioned); + assert(fsm_current(&machine) == 0); + assert(fsm_pending(&machine) == 0); + assert(strcmp(context.log, "ABby1") == 0); +} + +static void test_invalid_request_returns_error_and_keeps_state(void) +{ + FsmMachine machine; + TestContext context = new_context(); + + assert(fsm_init(&machine, test_states, TEST_STATE_COUNT, 0, &context) == FSM_OK); + assert(fsm_request(&machine, TEST_STATE_COUNT) == FSM_ERROR_INVALID_STATE); + + assert(fsm_current(&machine) == 0); + assert(fsm_pending(&machine) == 0); + assert(strcmp(context.log, "A") == 0); +} + +static void test_stack_push_enters_overlay_without_exiting_suspended_state(void) +{ + FsmMachine machine; + FsmStack stack; + int storage[1]; + TestContext context = new_context(); + + assert(fsm_init(&machine, test_states, TEST_STATE_COUNT, 0, &context) == FSM_OK); + assert(fsm_stack_init(&stack, &machine, storage, 1) == FSM_OK); + + assert(fsm_stack_push(&stack, 1) == FSM_OK); + + assert(fsm_current(&machine) == 1); + assert(fsm_pending(&machine) == 1); + assert(fsm_stack_count(&stack) == 1); + assert(strcmp(context.log, "AB") == 0); +} + +static void test_stack_push_works_with_uniform_state_profile(void) +{ + FsmMachine machine; + FsmStack stack; + int storage[1]; + TestContext context = new_context(); + FsmState uniform_state = { enter_a, update_a, draw_a, exit_a }; + + assert(fsm_init_uniform(&machine, &uniform_state, TEST_STATE_COUNT, 0, &context) == FSM_OK); + assert(fsm_stack_init(&stack, &machine, storage, 1) == FSM_OK); + + assert(fsm_stack_push(&stack, 2) == FSM_OK); + + assert(fsm_current(&machine) == 2); + assert(fsm_pending(&machine) == 2); + assert(fsm_stack_count(&stack) == 1); + assert(strcmp(context.log, "AA") == 0); +} + +static void test_stack_pop_exits_overlay_without_reentering_suspended_state(void) +{ + FsmMachine machine; + FsmStack stack; + int storage[1]; + TestContext context = new_context(); + + assert(fsm_init(&machine, test_states, TEST_STATE_COUNT, 0, &context) == FSM_OK); + assert(fsm_stack_init(&stack, &machine, storage, 1) == FSM_OK); + assert(fsm_stack_push(&stack, 1) == FSM_OK); + + assert(fsm_stack_pop(&stack) == FSM_OK); + + assert(fsm_current(&machine) == 0); + assert(fsm_pending(&machine) == 0); + assert(fsm_stack_count(&stack) == 0); + assert(strcmp(context.log, "ABy") == 0); +} + +static void test_stack_overflow_and_empty_pop_return_errors(void) +{ + FsmMachine machine; + FsmStack stack; + int storage[1]; + TestContext context = new_context(); + + assert(fsm_init(&machine, test_states, TEST_STATE_COUNT, 0, &context) == FSM_OK); + assert(fsm_stack_init(&stack, &machine, storage, 1) == FSM_OK); + + assert(fsm_stack_pop(&stack) == FSM_ERROR_STACK_EMPTY); + assert(fsm_stack_push(&stack, 1) == FSM_OK); + assert(fsm_stack_push(&stack, 2) == FSM_ERROR_STACK_FULL); + assert(fsm_current(&machine) == 1); + assert(fsm_stack_count(&stack) == 1); +} + +int main(void) +{ + test_init_enters_initial_state(); + test_request_is_deferred_until_step(); + test_uniform_state_profile_uses_same_callbacks_for_all_states(); + + test_transition_applies_immediately_without_update_or_draw(); + test_step_applies_transition_before_update_and_draw(); + test_step_skips_draw_when_update_requests_transition(); + test_step_draws_restored_state_after_overlay_pop(); + test_invalid_request_returns_error_and_keeps_state(); + test_stack_push_enters_overlay_without_exiting_suspended_state(); + test_stack_push_works_with_uniform_state_profile(); + test_stack_pop_exits_overlay_without_reentering_suspended_state(); + test_stack_overflow_and_empty_pop_return_errors(); + return 0; +} diff --git a/tests/test_cmake_roller_core.py b/tests/test_cmake_roller_core.py index b869e63a..7c8f8c2d 100644 --- a/tests/test_cmake_roller_core.py +++ b/tests/test_cmake_roller_core.py @@ -110,11 +110,14 @@ def test_core_only_configuration_has_no_game_target_or_dependencies(self) -> Non source_names = { Path(source["path"]).name for source in core_target["sources"] } - self.assertEqual(len(source_names), 72) + self.assertEqual(len(source_names), 74) self.assertIn("editor_camera.c", source_names) self.assertIn("editor_api.c", source_names) self.assertIn("editor_legacy_scene.c", source_names) self.assertIn("editor_core_host.c", source_names) + + self.assertIn("fsm.c", source_names) + self.assertIn("fsm_stack.c", source_names) self.assertIn("frontend.c", source_names) self.assertIn("network.c", source_names) self.assertIn("roller_core_error.c", source_names)