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
|
p7_coffre
|
||||||
p5_morse
|
p5_morse
|
||||||
p6_nfc
|
p6_nfc
|
||||||
|
npc_engine
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
#include "nvs_flash.h"
|
#include "nvs_flash.h"
|
||||||
|
|
||||||
#include "hints_client.h"
|
#include "hints_client.h"
|
||||||
|
#include "npc_engine.h"
|
||||||
#include "scenario_mesh.h"
|
#include "scenario_mesh.h"
|
||||||
#include "puzzle_binding.h"
|
#include "puzzle_binding.h"
|
||||||
#include "local_puzzles.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);
|
strncpy(s_current_step_id, step_id, sizeof(s_current_step_id) - 1);
|
||||||
s_current_step_id[sizeof(s_current_step_id) - 1] = '\0';
|
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: 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.
|
// p7_coffre_unlock() is a no-op stub when CONFIG_ZACUS_P7_COFFRE_ENABLE=n.
|
||||||
if (strcmp(step_id, "STEP_FINAL_WIN") == 0) {
|
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_TITLE 48
|
||||||
#define SB_MAX_SUBTITLE 64
|
#define SB_MAX_SUBTITLE 64
|
||||||
#define SB_MAX_SYMBOL 16
|
#define SB_MAX_SYMBOL 16
|
||||||
|
#define SB_MAX_SCENE_ID 40
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
SB_FX_PULSE = 0, // default (original SceneEffect::kPulse)
|
SB_FX_PULSE = 0, // default (original SceneEffect::kPulse)
|
||||||
@@ -65,6 +66,8 @@ typedef enum {
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
bool present; // step has a scene object
|
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 title[SB_MAX_TITLE];
|
||||||
char subtitle[SB_MAX_SUBTITLE];
|
char subtitle[SB_MAX_SUBTITLE];
|
||||||
char symbol[SB_MAX_SYMBOL];
|
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; }
|
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");
|
const cJSON *scene = cJSON_GetObjectItemCaseSensitive(step, "scene");
|
||||||
if (!scene || !cJSON_IsObject(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);
|
cJSON_Delete(root);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,6 +169,16 @@ esp_err_t npc_engine_trigger_cue(const char *cue_id);
|
|||||||
// and primes the stuck timer.
|
// and primes the stuck timer.
|
||||||
esp_err_t npc_engine_set_step(uint8_t step_id, uint32_t expected_duration_ms);
|
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
|
// Request a hint for `puzzle_id` at escalation `level` (0..3). The engine
|
||||||
// invokes `cb` with the resulting text. The current implementation is a
|
// invokes `cb` with the resulting text. The current implementation is a
|
||||||
// LOCAL STUB: it returns a hardcoded French placeholder synchronously.
|
// 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;
|
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) {
|
esp_err_t npc_engine_set_group_profile(const char *profile) {
|
||||||
// Thin pass-through. hints_client validates the value and logs the
|
// Thin pass-through. hints_client validates the value and logs the
|
||||||
// outcome. Kept on npc_engine so the rest of the firmware doesn't
|
// outcome. Kept on npc_engine so the rest of the firmware doesn't
|
||||||
|
|||||||
Reference in New Issue
Block a user