fix(npc): wire scene notify into apply_step
notify_gateway_scene() + hints puzzle_start lived only in npc_engine_set_step(), which had NO callers — the runtime->gateway sync was dead code, so scripted incoming calls would never fire in a real game. POST /game/step goes through game_endpoint_apply_step(), which now resolves the step's scene_id (new field on scene_binding, parsed straight from the IR) and calls the new npc_engine_set_scene_by_id(). Hardware-validated: STEP_WARNING -> scene 3 -> gateway notify -> auto-ring Professeur Zacus on the PLIP.
This commit was merged in pull request #32.
This commit is contained in:
@@ -22,4 +22,5 @@ idf_component_register(
|
||||
p7_coffre
|
||||
p5_morse
|
||||
p6_nfc
|
||||
npc_engine
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "nvs_flash.h"
|
||||
|
||||
#include "hints_client.h"
|
||||
#include "npc_engine.h"
|
||||
#include "scenario_mesh.h"
|
||||
#include "puzzle_binding.h"
|
||||
#include "local_puzzles.h"
|
||||
@@ -807,6 +808,16 @@ esp_err_t game_endpoint_apply_step(const char *step_id,
|
||||
strncpy(s_current_step_id, step_id, sizeof(s_current_step_id) - 1);
|
||||
s_current_step_id[sizeof(s_current_step_id) - 1] = '\0';
|
||||
|
||||
// Sync the NPC/hints engine (and, through it, the voice gateway) with the
|
||||
// scene this step enters. This is what makes the phone NPCs hint on — and
|
||||
// scripted calls auto-ring for — the current scene, regardless of whether
|
||||
// the step was driven by the local game loop or an external POST /game/step.
|
||||
// Best-effort: notify is async + server-side idempotent and never blocks
|
||||
// the step change. Steps with no scene_id (or an unknown one) are skipped.
|
||||
if (s_current_scene.scene_id[0] != '\0') {
|
||||
(void) npc_engine_set_scene_by_id(s_current_scene.scene_id, 0);
|
||||
}
|
||||
|
||||
// P7 coffre: fire the actuator when the final win step is reached.
|
||||
// p7_coffre_unlock() is a no-op stub when CONFIG_ZACUS_P7_COFFRE_ENABLE=n.
|
||||
if (strcmp(step_id, "STEP_FINAL_WIN") == 0) {
|
||||
|
||||
@@ -55,6 +55,7 @@ esp_err_t puzzle_binding_from_ir(const char *ir_json, const char *step_id,
|
||||
#define SB_MAX_TITLE 48
|
||||
#define SB_MAX_SUBTITLE 64
|
||||
#define SB_MAX_SYMBOL 16
|
||||
#define SB_MAX_SCENE_ID 40
|
||||
|
||||
typedef enum {
|
||||
SB_FX_PULSE = 0, // default (original SceneEffect::kPulse)
|
||||
@@ -65,6 +66,8 @@ typedef enum {
|
||||
|
||||
typedef struct {
|
||||
bool present; // step has a scene object
|
||||
char scene_id[SB_MAX_SCENE_ID]; // canonical SCENE_* id (sibling of step id;
|
||||
// set even when no display scene object)
|
||||
char title[SB_MAX_TITLE];
|
||||
char subtitle[SB_MAX_SUBTITLE];
|
||||
char symbol[SB_MAX_SYMBOL];
|
||||
|
||||
@@ -239,9 +239,15 @@ esp_err_t scene_binding_from_ir(const char *ir_json, const char *step_id,
|
||||
}
|
||||
if (!step) { cJSON_Delete(root); return ESP_ERR_NOT_FOUND; }
|
||||
|
||||
// Canonical scene id (SCENE_*), sibling of the step "id". Captured
|
||||
// unconditionally so npc_engine / the voice gateway can be synced even
|
||||
// on steps that carry no display "scene" object.
|
||||
copy_scene_str(out->scene_id, sizeof(out->scene_id),
|
||||
cJSON_GetObjectItemCaseSensitive(step, "scene_id"));
|
||||
|
||||
const cJSON *scene = cJSON_GetObjectItemCaseSensitive(step, "scene");
|
||||
if (!scene || !cJSON_IsObject(scene)) {
|
||||
// No scene — present stays false, ESP_OK.
|
||||
// No scene object — present stays false, ESP_OK (scene_id may be set).
|
||||
cJSON_Delete(root);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -169,6 +169,16 @@ esp_err_t npc_engine_trigger_cue(const char *cue_id);
|
||||
// and primes the stuck timer.
|
||||
esp_err_t npc_engine_set_step(uint8_t step_id, uint32_t expected_duration_ms);
|
||||
|
||||
// Same bridge as npc_engine_set_step(), but keyed by the canonical scene
|
||||
// string id (e.g. "SCENE_WARNING") rather than its numeric index. Resolves
|
||||
// the id against the engine's internal scene table and forwards to
|
||||
// npc_engine_set_step(). Lets the game endpoint sync the NPC/hints engine
|
||||
// (and the voice gateway) straight from the scenario IR's `scene_id`.
|
||||
// Returns ESP_ERR_NOT_FOUND when the id matches no known scene (no notify
|
||||
// is sent in that case), ESP_ERR_INVALID_ARG on a NULL/empty id.
|
||||
esp_err_t npc_engine_set_scene_by_id(const char *scene_id,
|
||||
uint32_t expected_duration_ms);
|
||||
|
||||
// Request a hint for `puzzle_id` at escalation `level` (0..3). The engine
|
||||
// invokes `cb` with the resulting text. The current implementation is a
|
||||
// LOCAL STUB: it returns a hardcoded French placeholder synchronously.
|
||||
|
||||
@@ -440,6 +440,19 @@ esp_err_t npc_engine_set_step(uint8_t step_id, uint32_t expected_duration_ms) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t npc_engine_set_scene_by_id(const char *scene_id,
|
||||
uint32_t expected_duration_ms) {
|
||||
if (scene_id == NULL || scene_id[0] == '\0') return ESP_ERR_INVALID_ARG;
|
||||
for (uint8_t i = 0; i < kSceneCount; i++) {
|
||||
if (strcmp(scene_id, kSceneIds[i]) == 0) {
|
||||
return npc_engine_set_step(i, expected_duration_ms);
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "set_scene_by_id: unknown scene \"%s\" (no kSceneIds match)",
|
||||
scene_id);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
esp_err_t npc_engine_set_group_profile(const char *profile) {
|
||||
// Thin pass-through. hints_client validates the value and logs the
|
||||
// outcome. Kept on npc_engine so the rest of the firmware doesn't
|
||||
|
||||
Reference in New Issue
Block a user