mirror of
https://github.com/DM67-Developer/opensim-ai-npc-framework.git
synced 2026-08-14 08:52:27 +00:00
292 lines
12 KiB
Plaintext
292 lines
12 KiB
Plaintext
// Gestures.lsl — Gesture Animation System for Delphina NPC
|
|
// Handles facial expressions and body language animations
|
|
//
|
|
// TIMER COORDINATION: Acts on Cycle 3 of 4-cycle master timer system
|
|
// Cycle 0: Actions.lsl (movement updates)
|
|
// Cycle 1: Senses.lsl (environment scanning)
|
|
// Cycle 2: Chat.lsl (HTTP timeout detection)
|
|
// Cycle 3: Gestures.lsl (animation management) ← THIS SCRIPT
|
|
//
|
|
// This script receives LM_GESTURE_TRIGGER messages from Chat.lsl when the AI
|
|
// model outputs gesture markers (e.g., %smile%, %wave%, %nod_head%) and plays
|
|
// the corresponding NPC animations with automatic duration tracking and cleanup.
|
|
|
|
// ---- Opcodes (match other modules)
|
|
integer LM_INIT = 1;
|
|
integer LM_CONFIG = 300;
|
|
integer LM_GESTURE_TRIGGER = 600; // str: gesture_type, id: target_key
|
|
integer LM_TIMER_TICK = 250; // Master timer tick broadcast
|
|
integer LM_TIMER_CONFIG = 251; // Timer configuration update
|
|
|
|
// ---- Config and runtime
|
|
key NPC = NULL_KEY;
|
|
string CONFIG_CARD = "general.cfg";
|
|
float MASTER_TIMER_INTERVAL = 1.0;
|
|
integer GESTURE_CYCLE_OFFSET = 3; // Act on cycle 3
|
|
|
|
// ---- Gesture Configuration
|
|
integer GESTURE_ENABLED = TRUE;
|
|
integer GESTURE_AUTO_STOP = TRUE; // Auto-stop previous gesture when starting new one
|
|
float GESTURE_DURATION = 3.0; // Default duration in seconds (0.0 = play full animation)
|
|
integer GESTURE_DEBUG = FALSE;
|
|
|
|
// ---- Gesture Markers (configurable via general.cfg)
|
|
string ACTION_MARKER_SMILE = "%smile%";
|
|
string ACTION_MARKER_FROWN = "%frown%";
|
|
string ACTION_MARKER_CRY = "%cry%";
|
|
string ACTION_MARKER_APPLAUSE = "%applause%";
|
|
string ACTION_MARKER_SHAKE_HEAD = "%shake_head%";
|
|
string ACTION_MARKER_NOD_HEAD = "%nod_head%";
|
|
string ACTION_MARKER_NOD_HEAD_SMILE = "%nod_head_smile%";
|
|
string ACTION_MARKER_SHRUG = "%shrug%";
|
|
string ACTION_MARKER_CURTSY = "%curtsy%";
|
|
string ACTION_MARKER_WAVE = "%wave%";
|
|
|
|
// ---- Gesture Animation Names (configurable via general.cfg, must match inventory)
|
|
// Note: Animation files use capitalized first letter and snake_case for multi-word names
|
|
string GESTURE_ANIM_SMILE = "Smile";
|
|
string GESTURE_ANIM_FROWN = "Frown";
|
|
string GESTURE_ANIM_CRY = "Cry";
|
|
string GESTURE_ANIM_APPLAUSE = "Applause";
|
|
string GESTURE_ANIM_SHAKE_HEAD = "Shake_Head";
|
|
string GESTURE_ANIM_NOD_HEAD = "Nod_Head";
|
|
string GESTURE_ANIM_NOD_HEAD_SMILE = "Nod_Head_Smile";
|
|
string GESTURE_ANIM_SHRUG = "Shrug";
|
|
string GESTURE_ANIM_CURTSY = "Curtsy";
|
|
string GESTURE_ANIM_WAVE = "Wave";
|
|
|
|
// ---- Runtime tracking
|
|
string current_gesture = "";
|
|
string current_anim_name = "";
|
|
float gesture_start_time = 0.0;
|
|
integer gesture_timer_active = FALSE;
|
|
|
|
// ---- Cycle tracking for master timer coordination
|
|
integer master_cycle = 0;
|
|
|
|
// ---- Helper Functions
|
|
string get_card(string name) {
|
|
if (llGetInventoryType(name) != INVENTORY_NOTECARD) return "";
|
|
return osGetNotecard(name);
|
|
}
|
|
|
|
list split_pipe(string s) {
|
|
return llParseString2List(s, ["|"], []);
|
|
}
|
|
|
|
// ---- Configuration Loading
|
|
integer load_config() {
|
|
string txt = get_card(CONFIG_CARD);
|
|
if (txt == "") return FALSE;
|
|
|
|
list lines = llParseString2List(txt, ["\n"], []);
|
|
integer i;
|
|
for (i = 0; i < llGetListLength(lines); i++) {
|
|
string line = llStringTrim(llList2String(lines, i), STRING_TRIM);
|
|
if (line == "") jump cont;
|
|
if (llGetSubString(line, 0, 0) == "#") jump cont;
|
|
if (llGetSubString(line, 0, 1) == "//") jump cont;
|
|
|
|
integer eq = llSubStringIndex(line, "=");
|
|
if (eq <= 0) jump cont;
|
|
|
|
string k = llStringTrim(llGetSubString(line, 0, eq-1), STRING_TRIM);
|
|
string v = llStringTrim(llGetSubString(line, eq+1, -1), STRING_TRIM);
|
|
|
|
// Gesture System Configuration
|
|
if (k == "GESTURE_ENABLED") GESTURE_ENABLED = (integer)v;
|
|
else if (k == "GESTURE_AUTO_STOP") GESTURE_AUTO_STOP = (integer)v;
|
|
else if (k == "GESTURE_DURATION") {
|
|
GESTURE_DURATION = llFabs((float)v); // Prevent negative durations
|
|
}
|
|
else if (k == "GESTURE_DEBUG") GESTURE_DEBUG = (integer)v;
|
|
|
|
// Gesture Markers
|
|
else if (k == "ACTION_MARKER_SMILE") ACTION_MARKER_SMILE = v;
|
|
else if (k == "ACTION_MARKER_FROWN") ACTION_MARKER_FROWN = v;
|
|
else if (k == "ACTION_MARKER_CRY") ACTION_MARKER_CRY = v;
|
|
else if (k == "ACTION_MARKER_APPLAUSE") ACTION_MARKER_APPLAUSE = v;
|
|
else if (k == "ACTION_MARKER_SHAKE_HEAD") ACTION_MARKER_SHAKE_HEAD = v;
|
|
else if (k == "ACTION_MARKER_NOD_HEAD") ACTION_MARKER_NOD_HEAD = v;
|
|
else if (k == "ACTION_MARKER_NOD_HEAD_SMILE") ACTION_MARKER_NOD_HEAD_SMILE = v;
|
|
else if (k == "ACTION_MARKER_SHRUG") ACTION_MARKER_SHRUG = v;
|
|
else if (k == "ACTION_MARKER_CURTSY") ACTION_MARKER_CURTSY = v;
|
|
else if (k == "ACTION_MARKER_WAVE") ACTION_MARKER_WAVE = v;
|
|
|
|
// Gesture Animation Names
|
|
else if (k == "GESTURE_ANIM_SMILE") GESTURE_ANIM_SMILE = v;
|
|
else if (k == "GESTURE_ANIM_FROWN") GESTURE_ANIM_FROWN = v;
|
|
else if (k == "GESTURE_ANIM_CRY") GESTURE_ANIM_CRY = v;
|
|
else if (k == "GESTURE_ANIM_APPLAUSE") GESTURE_ANIM_APPLAUSE = v;
|
|
else if (k == "GESTURE_ANIM_SHAKE_HEAD") GESTURE_ANIM_SHAKE_HEAD = v;
|
|
else if (k == "GESTURE_ANIM_NOD_HEAD") GESTURE_ANIM_NOD_HEAD = v;
|
|
else if (k == "GESTURE_ANIM_NOD_HEAD_SMILE") GESTURE_ANIM_NOD_HEAD_SMILE = v;
|
|
else if (k == "GESTURE_ANIM_SHRUG") GESTURE_ANIM_SHRUG = v;
|
|
else if (k == "GESTURE_ANIM_CURTSY") GESTURE_ANIM_CURTSY = v;
|
|
else if (k == "GESTURE_ANIM_WAVE") GESTURE_ANIM_WAVE = v;
|
|
|
|
@cont;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
// ---- Animation Management
|
|
string get_gesture_animation(string gesture_type) {
|
|
if (gesture_type == "smile") return GESTURE_ANIM_SMILE;
|
|
else if (gesture_type == "frown") return GESTURE_ANIM_FROWN;
|
|
else if (gesture_type == "cry") return GESTURE_ANIM_CRY;
|
|
else if (gesture_type == "applause") return GESTURE_ANIM_APPLAUSE;
|
|
else if (gesture_type == "shake_head") return GESTURE_ANIM_SHAKE_HEAD;
|
|
else if (gesture_type == "nod_head") return GESTURE_ANIM_NOD_HEAD;
|
|
else if (gesture_type == "nod_head_smile") return GESTURE_ANIM_NOD_HEAD_SMILE;
|
|
else if (gesture_type == "shrug") return GESTURE_ANIM_SHRUG;
|
|
else if (gesture_type == "curtsy") return GESTURE_ANIM_CURTSY;
|
|
else if (gesture_type == "wave") return GESTURE_ANIM_WAVE;
|
|
return "";
|
|
}
|
|
|
|
integer start_gesture(string gesture_type) {
|
|
if (!GESTURE_ENABLED || NPC == NULL_KEY) return FALSE;
|
|
|
|
string anim_name = get_gesture_animation(gesture_type);
|
|
if (anim_name == "") {
|
|
llOwnerSay("Gestures: Unknown gesture type '" + gesture_type + "'");
|
|
return FALSE;
|
|
}
|
|
|
|
// IDEMPOTENCE CHECK: Don't restart if same animation still active
|
|
// Check if the SAME animation is currently playing AND not expired
|
|
// This is more robust than checking gesture_type because:
|
|
// 1. It handles config changes (new anim name for same gesture type)
|
|
// 2. It validates temporal state (is the animation still active?)
|
|
// 3. It prevents stale state from blocking new gestures
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
if (current_anim_name == anim_name && current_anim_name != "") {
|
|
// Animation name matches, but is it still actively playing?
|
|
float elapsed = llGetTime() - gesture_start_time;
|
|
|
|
if (GESTURE_DURATION > 0.0 && elapsed < GESTURE_DURATION) {
|
|
// Animation is still within its duration window
|
|
if (GESTURE_DEBUG) {
|
|
llOwnerSay("Gestures: Animation '" + anim_name + "' still playing (" +
|
|
(string)((integer)(GESTURE_DURATION - elapsed)) + "s remaining), skipping restart");
|
|
}
|
|
return TRUE; // Return success, but don't restart
|
|
}
|
|
else if (GESTURE_DURATION <= 0.0) {
|
|
// Infinite duration gestures - always skip restart if same anim
|
|
if (GESTURE_DEBUG) {
|
|
llOwnerSay("Gestures: Infinite animation '" + anim_name + "' already playing, skipping restart");
|
|
}
|
|
return TRUE;
|
|
}
|
|
// If we reach here: elapsed >= GESTURE_DURATION
|
|
// The animation should have expired, so allow restart
|
|
if (GESTURE_DEBUG) {
|
|
llOwnerSay("Gestures: Animation '" + anim_name + "' expired (" +
|
|
(string)((integer)elapsed) + "s elapsed), allowing restart");
|
|
}
|
|
}
|
|
|
|
// Check if animation exists in inventory
|
|
if (llGetInventoryType(anim_name) != INVENTORY_ANIMATION) {
|
|
llOwnerSay("Gestures: Animation '" + anim_name + "' not found in inventory");
|
|
return FALSE;
|
|
}
|
|
|
|
// Stop current gesture if auto-stop is enabled
|
|
if (GESTURE_AUTO_STOP && current_anim_name != "") {
|
|
stop_current_gesture();
|
|
}
|
|
|
|
// Start the new animation
|
|
osNpcPlayAnimation(NPC, anim_name);
|
|
current_gesture = gesture_type;
|
|
current_anim_name = anim_name;
|
|
gesture_start_time = llGetTime();
|
|
|
|
// Enable duration tracking (handled by master timer cycle now)
|
|
if (GESTURE_DURATION > 0.0) {
|
|
gesture_timer_active = TRUE;
|
|
}
|
|
|
|
if (GESTURE_DEBUG) {
|
|
llOwnerSay("Gestures: Started '" + gesture_type + "' (" + anim_name + ")");
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
// No return value - stops current NPC animation
|
|
stop_current_gesture() {
|
|
if (current_anim_name != "" && NPC != NULL_KEY) {
|
|
// Store animation name before clearing (in case OSSL call fails)
|
|
string stopping_anim = current_anim_name;
|
|
string stopping_gesture = current_gesture;
|
|
|
|
// Clear state BEFORE OSSL call to prevent partial cleanup
|
|
current_gesture = "";
|
|
current_anim_name = "";
|
|
gesture_start_time = 0.0;
|
|
gesture_timer_active = FALSE;
|
|
|
|
// Now attempt to stop the animation
|
|
// If this fails, state is already cleared (fail-safe)
|
|
osNpcStopAnimation(NPC, stopping_anim);
|
|
|
|
if (GESTURE_DEBUG) {
|
|
llOwnerSay("Gestures: Stopped '" + stopping_gesture + "' (" + stopping_anim + ")");
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- DEFAULT STATE
|
|
default {
|
|
state_entry() {
|
|
load_config();
|
|
llOwnerSay("Gestures: Gesture Animation System Ready");
|
|
if (!GESTURE_ENABLED) {
|
|
llOwnerSay("Gestures: System disabled via config");
|
|
}
|
|
}
|
|
|
|
link_message(integer sender, integer num, string str, key id) {
|
|
if (num == LM_CONFIG) {
|
|
NPC = id;
|
|
llOwnerSay("Gestures: NPC key configured: " + (string)NPC);
|
|
}
|
|
else if (num == LM_TIMER_CONFIG) {
|
|
list L = split_pipe(str);
|
|
integer i;
|
|
for (i = 0; i < llGetListLength(L); i++) {
|
|
string kv = llList2String(L, i);
|
|
integer eq = llSubStringIndex(kv, "=");
|
|
if (eq >= 0) {
|
|
string k = llStringTrim(llGetSubString(kv, 0, eq-1), STRING_TRIM);
|
|
string v = llStringTrim(llGetSubString(kv, eq+1, -1), STRING_TRIM);
|
|
if (k == "MASTER_INTERVAL") MASTER_TIMER_INTERVAL = (float)v;
|
|
}
|
|
}
|
|
}
|
|
else if (num == LM_TIMER_TICK) {
|
|
master_cycle++;
|
|
|
|
// Cycle gate: act only once every four master ticks
|
|
if ((master_cycle % 4) != GESTURE_CYCLE_OFFSET) return;
|
|
|
|
// Check for gesture expiration using master timer pacing
|
|
if (gesture_timer_active && current_anim_name != "") {
|
|
float elapsed = llGetTime() - gesture_start_time;
|
|
if (elapsed >= GESTURE_DURATION) {
|
|
stop_current_gesture();
|
|
gesture_timer_active = FALSE;
|
|
if (GESTURE_DEBUG) llOwnerSay("Gestures: Auto-stopped expired gesture");
|
|
}
|
|
}
|
|
}
|
|
else if (num == LM_GESTURE_TRIGGER) {
|
|
// Direct gesture trigger from other modules
|
|
start_gesture(str);
|
|
}
|
|
}
|
|
} |