/////////////////////////////////////////////////////////////////////////// // Copyright (C) Wizardry and Steamworks 2013 - License: GNU GPLv3 // // Please see: http://www.gnu.org/licenses/gpl.html for legal details, // // rights of fair usage, the disclaimer and warranty conditions. // /////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////// // CONFIGURATION // ////////////////////////////////////////////////////////// // How likely is it (from 0 to 100), that Racter will // answer even if it is not directly spoken to. integer SPEAK_FREELY = 100; // These are the responses that racter will answer with // in case no keywords have been matched from the brain. // They can be changed or altered in any way but should // be general enough to be a passable valid answer to // any statement or question that the avatar asks. list MISS_RESPONSES = [ "yeah, i guess", "doesn't make much sense to me", "i see", "aha, ok", "i'm not sure", "come again?", "i hear you", "my thoughts are cloudy", "perhaps", "hmm" ]; /////////////////////////////////////////////////////////////////////////// // INTERNALS // /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3 // /////////////////////////////////////////////////////////////////////////// list wasDualQuicksort(list a, list b) { if(llGetListLength(a) <= 1) return a+b; integer pivot_a = llList2Integer(a, 0); a = llDeleteSubList(a, 0, 0); string pivot_b = llList2String(b, 0); b = llDeleteSubList(b, 0, 0); list less = []; list less_b = []; list more = []; list more_b = []; do { if(llList2Integer(a, 0) > pivot_a) { less += llList2List(a, 0, 0); less_b += llList2List(b, 0, 0); jump continue; } more += llList2List(a, 0, 0); more_b += llList2List(b, 0, 0); @continue; a = llDeleteSubList(a, 0, 0); b = llDeleteSubList(b, 0, 0); } while(llGetListLength(a)); return wasDualQuicksort(less, less_b) + [ pivot_a ] + [ pivot_b ] + wasDualQuicksort(more, more_b); } key eQuery = NULL_KEY; list brain = []; list brain_cards = []; string current_card = ""; integer nLine = 0; string sayLine = ""; default { state_entry() { integer i = llGetInventoryNumber(INVENTORY_NOTECARD)-1; do { list tCard = llParseString2List(llGetInventoryName(INVENTORY_NOTECARD,i), ["_"], [""]); if(llGetListLength(tCard) != 2 || llList2String(tCard, 0) != llGetScriptName()) jump continue; brain_cards += llGetInventoryName(INVENTORY_NOTECARD, i); @continue; } while(--i>-1); if(llGetListLength(brain_cards) != 0) jump found_notecards; llSay(DEBUG_CHANNEL, "No brain cards found."); return; @found_notecards; llOwnerSay("Reading brain files... Please wait..."); current_card = llList2String(brain_cards, 0); brain_cards = llDeleteSubList(brain_cards, 0, 0); eQuery = llGetNotecardLine(current_card, nLine); } dataserver(key id, string data) { if(id != eQuery) return; if(data == EOF) { // check if we are done with notecards. if(llGetListLength(brain_cards) != 0) { // ... if we are not, continue reading the next notecard current_card = llList2String(brain_cards, 0); brain_cards = llDeleteSubList(brain_cards, 0, 0); nLine = 0; eQuery = llGetNotecardLine(current_card, nLine); return; } // now sort the brain in decreasing order by weights. list keyData = []; list weights = []; do { list split = llParseString2List(llList2String(brain, 0), ["#"], [""]); brain = llDeleteSubList(brain, 0, 0); keyData += llList2String(split, 0) + "#" + llList2String(split, 1); weights += llList2String(split, 2); } while(llGetListLength(brain)); list sorted = wasDualQuicksort(weights, keyData); keyData = llList2ListStrided(llDeleteSubList(sorted, 0, 0), 0, llGetListLength(sorted)-1, 2); weights = llList2ListStrided(sorted, 0, llGetListLength(sorted)-1, 2); do { brain += llList2String(keyData, 0) + "#" + llList2String(weights, 0); keyData = llDeleteSubList(keyData, 0, 0); weights = llDeleteSubList(weights, 0, 0); } while(llGetListLength(keyData)); // brain is organized now, switch to racter. llOwnerSay("Done reading brain files... Racter ready."); state racter; } if(data == "") jump next_line; list data_line = llParseString2List(data, ["#"], [""]); if(llList2String(data_line, 0) == "" || llList2String(data_line, 1) == "" || llList2String(data_line,2) == "") jump next_line; brain += data; @next_line; eQuery = llGetNotecardLine(current_card, ++nLine); } changed(integer change) { if(change & CHANGED_INVENTORY) llResetScript(); } on_rez(integer start_param) { llResetScript(); } } state racter { state_entry() { llListen(0, "", "", ""); } listen(integer chan, string name, key id, string mes) { // if it's an object talking, ignore it. if(llList2Key(llGetObjectDetails(id, [OBJECT_CREATOR]), 0) != NULL_KEY) return; if(llStringTrim(name, STRING_TRIM) == llGetScriptName()) return; // change the message to lowercase. mes = llToLower(mes); // compute the total score for the current keyword for all keyword lines. // we need this for the cumulative probability afterward. integer keywordScore = 0; integer i = llGetListLength(brain)-1; do { list data = llParseString2List(llList2String(brain, i), ["#"], []); list keywords = llParseString2List(llList2String(data, 0), ["/"], []); do { string keyword = llToLower(llList2String(keywords, 0)); keywords = llDeleteSubList(keywords, 0, 0); if(llSubStringIndex(mes, keyword) == -1) jump continue; keywordScore += llList2Integer(data, 2); @continue; } while(llGetListLength(keywords)); } while(--i>-1); // if the score is zero if(keywordScore != 0) jump matched; // ... and if the script name is in the message, answer with a miss response. if(llSubStringIndex(mes, llToLower(llGetScriptName())) != -1) jump miss; // ... and if we are not allowed to speak freely, return. if(SPEAK_FREELY == 0) return; // ... and if we may speak freely, and if the probability treshold is not high enough, just bail-out. if((integer)llFrand((integer)(100/SPEAK_FREELY))!=(integer)llFrand((integer)(100/SPEAK_FREELY))) return; @miss; if(llGetListLength(MISS_RESPONSES) == 0) return; sayLine = llList2String(MISS_RESPONSES, (integer)llFrand(llGetListLength(MISS_RESPONSES))); // and bail-out. state speak; @matched; // If we matched a keyword then, based on the cumulative probabilities // of the possible responses, select a response and say it in local chat. float random = llFrand(keywordScore); float cumulative = 0; i = llGetListLength(brain)-1; do { list data = llParseString2List(llList2String(brain, i), ["#"], []); list keywords = llParseString2List(llList2String(data, 0), ["/"], []); do { string keyword = llToLower(llList2String(keywords, 0)); keywords = llDeleteSubList(keywords, 0, 0); if(llSubStringIndex(mes, keyword) == -1) jump skip; cumulative += llList2Integer(data, 2); if(cumulative >= random) { if(llSubStringIndex(mes, llToLower(llGetScriptName())) != -1) jump respond; if(SPEAK_FREELY == 0) return; if((integer)llFrand((integer)(100/SPEAK_FREELY))!=(integer)llFrand((integer)(100/SPEAK_FREELY))) return; @respond; list sayOptions = llParseString2List(llList2String(data, 1), ["/"], []); sayLine = llList2String(sayOptions, (integer)llFrand(llGetListLength(sayOptions))); state speak; } @skip; } while(llGetListLength(keywords)); } while(--i>=0); } changed(integer change) { if(change & CHANGED_INVENTORY) llResetScript(); } on_rez(integer start_param) { llResetScript(); } } state speak { state_entry() { // 40 WPM = 240 characters / minute = 4 characters / second. llSetTimerEvent((float)llStringLength(sayLine)/4.0); } timer() { // now say it. llSay(0, sayLine); // and switch to racter. state racter; } on_rez(integer start_param) { llResetScript(); } }