/////////////////////////////////////////////////////////////////////////// // Copyright (C) Wizardry and Steamworks 2012 - 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 // /////////////////////////////////////////////////////////////////////////// // The UUID / Key of the scripted agent. string CORRADE = "18a56ee7-4330-48f9-835a-f7839a32cdbf"; // The name of a configured group. string GROUP = "My Group"; // The password for the configured group. string PASSWORD = "mypassword"; // How many seconds per letter are allowed before dropping a hint. integer TIME_PER_LETTER = 5; // The prefix used to trigger trivia commands. string COMMAND_PREFIX = "@"; // A string to be used as a notification tag. string NOTIFY_TAG = "TjP9Nd1boQ"; /////////////////////////////////////////////////////////////////////////// // 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); } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0 // /////////////////////////////////////////////////////////////////////////// string wasKeyValueGet(string k, string data) { if(llStringLength(data) == 0) return ""; if(llStringLength(k) == 0) return ""; list a = llParseStringKeepNulls(data, ["&", "="], []); integer i = llListFindList(llList2ListStrided(a, 0, -1, 2), [ k ]); if(i != -1) return llList2String(a, 2*i+1); return ""; } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0 // /////////////////////////////////////////////////////////////////////////// string wasKeyValueEncode(list data) { integer i = llGetListLength(data); if (i % 2 != 0 || i == 0) return ""; --i; do { data = llListInsertList( llDeleteSubList( data, i-1, i ), [ llList2String(data, i-1) + "=" + llList2String(data, i) ], i-1 ); i -= 2; } while(i > 0); return llDumpList2String(data, "&"); } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3 // /////////////////////////////////////////////////////////////////////////// // unescapes a string in conformance with RFC1738 string wasFormDecode(string i) { return llUnescapeURL( llDumpList2String( llParseString2List( llDumpList2String( llParseString2List( i, ["+"], [] ), " " ), ["%0D%0A"], [] ), "\n" ) ); } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3 // /////////////////////////////////////////////////////////////////////////// // escapes a string in conformance with RFC1738 string wasFormEncode(string i) { string o = ""; do { string c = llGetSubString(i, 0, 0); i = llDeleteSubString(i, 0, 0); if(c == "") jump continue; if(c == " ") { o += "+"; jump continue; } if(c == "\n") { o += "%0D" + llEscapeURL(c); jump continue; } o += llEscapeURL(c); @continue; } while(i != ""); return o; } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0 // /////////////////////////////////////////////////////////////////////////// string wasListToCSV(list l) { list v = []; do { string a = llDumpList2String( llParseStringKeepNulls( llList2String( l, 0 ), ["\""], [] ), "\"\"" ); if(llParseStringKeepNulls( a, [" ", ",", "\n", "\""], [] ) != (list) a ) a = "\"" + a + "\""; v += a; l = llDeleteSubList(l, 0, 0); } while(l != []); return llDumpList2String(v, ","); } list cards = []; string card = ""; integer cardIterator = 0; integer lineIterator = 0; list categories = []; string category = ""; string select = ""; string question = ""; string clue = ""; list store = []; list scores = []; list names = []; string name = ""; integer seconds = 0; string URL = ""; integer running = FALSE; key dataRequest = NULL_KEY; default { state_entry() { // DEBUG llOwnerSay("Please wait, reading brains..."); integer i = llGetInventoryNumber(INVENTORY_NOTECARD)-1; do { list brainCard = llParseString2List( llGetInventoryName( INVENTORY_NOTECARD, i ), ["_"], [""] ); if(llList2String(brainCard, 0) == "Trivia" && llList2String(brainCard, 1) == "Brain") { cards += llGetInventoryName(INVENTORY_NOTECARD, i); } } while(--i > -1); if(llGetListLength(cards) == 0) { // DEBUG llOwnerSay("Failed to find any trivia brains!"); return; } // DEBUG llOwnerSay("Categorizing brains..."); cardIterator = llGetListLength(cards)-1; card = llList2String(cards, cardIterator); // DEBUG llOwnerSay("Reading brain: " + card); lineIterator = 0; llGetNotecardLine(card, lineIterator); } dataserver(key id, string data) { if(data == EOF) { // If all cards have been read, jump to the next state. if(cardIterator-- == 0) { // DEBUG llOwnerSay("All brain cards have been read."); state url; } // Select the next card. card = llList2String(cards, cardIterator); // DEBUG llOwnerSay("Reading brain: " + card); lineIterator = 0; llGetNotecardLine(card, lineIterator); return; } category = llList2String( llParseString2List( llList2String( llParseString2List( data, ["//"], [""]), 0 ), ["/"], [""] ), 0 ); // Broken line in notecard brain. if(category == "") { // DEBUG llOwnerSay("Skipping brain line without category in " + card + " on line " + (string)lineIterator ); llGetNotecardLine(card, ++lineIterator); return; } // If category has not been added yet then add it. if(llListFindList(categories, (list)category) == -1) { categories += category; } llGetNotecardLine(card, ++lineIterator); } changed(integer change) { if(!(change & (CHANGED_INVENTORY | CHANGED_OWNER))) { return; } llResetScript(); } on_rez(integer param) { llResetScript(); } } state url { state_entry() { // DEBUG llOwnerSay("Requesting LSL URL..."); llReleaseURL(URL); llRequestURL(); } http_request(key id, string method, string body) { if(method != URL_REQUEST_GRANTED) { // DEBUG llOwnerSay("LSL URL reqest failed..."); llResetScript(); } URL = body; state rebind; } } state rebind { state_entry() { // DEBUG llOwnerSay("Removing group notification..."); llMessageLinked(LINK_THIS, 0, wasKeyValueEncode( [ "command", "notify", "group", wasFormEncode(GROUP), "password", wasFormEncode(PASSWORD), "action", "remove", "tag", wasFormEncode(NOTIFY_TAG), // afterburn "aftAction", "remove", "callback", wasFormEncode(URL) ] ), CORRADE ); llSetTimerEvent(60); } http_request(key id, string method, string body) { llHTTPResponse(id, 200, "OK"); if(wasKeyValueGet("command", body) != "notify") { return; } if(wasKeyValueGet("success", body) != "True") { // DEBUG llOwnerSay("Cannot rebind group notifications: " + wasKeyValueGet( "error", body ) ); state url; } if(wasKeyValueGet("success", body) == "True" && wasKeyValueGet("aftAction", body) == "remove") { llMessageLinked(LINK_THIS, 0, wasKeyValueEncode( [ "command", "notify", "group", wasFormEncode(GROUP), "password", wasFormEncode(PASSWORD), "action", "set", "tag", wasFormEncode(NOTIFY_TAG), "type", "group", // afterburn "aftAction", "set", "URL", wasFormEncode(URL), "callback", wasFormEncode(URL) ] ), CORRADE ); return; } // DEBUG llOwnerSay("Notification bound successfully!"); state group_messages; } timer() { // DEBUG llOwnerSay("Timeout binding to group notification."); state url; } changed(integer change) { if(!(change & (CHANGED_INVENTORY | CHANGED_OWNER))) { return; } llResetScript(); } on_rez(integer param) { llResetScript(); } state_exit() { llSetTimerEvent(0); } } state group_messages { state_entry() { // If no category has been selected, then stop and wait for commands. if(running == FALSE) { // DEBUG llOwnerSay("Waiting for command..."); return; } // A category and question has been selected so send the message to the group. llMessageLinked(LINK_THIS, 0, wasKeyValueEncode( [ "command", "tell", "group", wasFormEncode(GROUP), "password", wasFormEncode(PASSWORD), "entity", "group", "message", wasFormEncode("[" + category + "]: " + question) ] ), CORRADE ); seconds = llStringLength(clue); llSetTimerEvent(TIME_PER_LETTER); } http_request(key id, string method, string body) { llHTTPResponse(id, 200, "OK"); // Skip messages that are not for the configured group. if(wasFormDecode(wasKeyValueGet("group", body)) != GROUP) { return; } // Ignore self messages. if(wasKeyValueGet("agent", body) == CORRADE) { return; } // Skip messages that are not trivia commands. string message = wasFormDecode(wasKeyValueGet("message", body)); if(llGetSubString(message, 0, 0) != COMMAND_PREFIX) { jump interpret_answer; } list data = llParseString2List( message, [" "], [] ); // Ignore non-trivia commands. if(llList2String(data, 0) != "@trivia") { return; } if(llList2String(data, 1) == "start") { // If no category has been selected then set the category to any. if(llListFindList(categories, (list)llList2String(data, 2)) == -1) { select = "any"; } running = TRUE; state select_question; } if(llList2String(data, 1) == "help") { llMessageLinked(LINK_THIS, 0, wasKeyValueEncode( [ "command", "tell", "group", wasFormEncode(GROUP), "password", wasFormEncode(PASSWORD), "entity", "group", "message", wasFormEncode("syntax: @trivia [start <category>|stop|categories|help]") ] ), CORRADE ); return; } if(llList2String(data, 1) == "categories") { llMessageLinked(LINK_THIS, 0, wasKeyValueEncode( [ "command", "tell", "group", wasFormEncode(GROUP), "password", wasFormEncode(PASSWORD), "entity", "group", "message", wasFormEncode(llDumpList2String(categories, ", ")) ] ), CORRADE ); return; } if(llList2String(data, 1) == "stop") { // DEBUG llOwnerSay("Stopping trivia..."); llSetTimerEvent(0); running = FALSE; llMessageLinked(LINK_THIS, 0, wasKeyValueEncode( [ "command", "tell", "group", wasFormEncode(GROUP), "password", wasFormEncode(PASSWORD), "entity", "group", "message", wasFormEncode("Trivia stopped.") ] ), CORRADE ); return; } return; @interpret_answer; if(running == FALSE) { return; } integer i = llGetListLength(store) - 1; string answer = ""; do { answer = llList2String(store, i); if(llStringLength(answer) == llStringLength(message) && !(llToUpper(answer) != llToUpper(message))) { jump correct_answer; } } while(--i > -1); return; @correct_answer; llSetTimerEvent(0); // Get the name of the avatar sending the message. name = wasFormDecode( wasKeyValueGet( "firstname", body ) ) + " " + wasFormDecode( wasKeyValueGet( "lastname", body ) ); llMessageLinked(LINK_THIS, 0, wasKeyValueEncode( [ "command", "tell", "group", wasFormEncode(GROUP), "password", wasFormEncode(PASSWORD), "entity", "group", "message", wasFormEncode(name + " answered the question correctly: " + answer + " is a correct answer!") ] ), CORRADE ); i = llListFindList(names, (list)name); if(i == -1) { names = llListInsertList(names, (list)name, 0); scores = llListInsertList(scores, (list)1, 0); jump compute_score; } integer score = llList2Integer(scores, i) + 1; scores = llListReplaceList(scores, (list)score, i, i); @compute_score; llMessageLinked(LINK_THIS, 0, wasKeyValueEncode( [ "command", "tell", "group", wasFormEncode(GROUP), "password", wasFormEncode(PASSWORD), "entity", "group", "message", wasFormEncode("Score: " + llDumpList2String( wasDualQuicksort( scores, names ), " -> " ) ) ] ), CORRADE ); state select_question; } timer() { if(running == FALSE) { return; } integer i = (integer)llFrand(llStringLength(clue)); do { if(--i < 0) { i = llStringLength(clue); } } while(llGetSubString(clue, i, i) != "-"); // // f = a + i + b // // / 0..i - 1 iff. i - 1 >= 0 // a = { // \ emp iff. i - 1 < 0 // // // / 0..i + 1 iff. i + 1 < len(b) // b = { // \ emp iff. i + 1 >= len(b) // // string answer = llList2String(store, 0); clue = llList2String( [ llGetSubString(clue, 0, i - 1), "" ], i - 1 < 0 ) + llGetSubString(answer, i, i) + llList2String( [ llGetSubString(clue, i + 1, -1), "" ], i + 1 >= llStringLength(clue) ); llMessageLinked(LINK_THIS, 0, wasKeyValueEncode( [ "command", "tell", "group", wasFormEncode(GROUP), "password", wasFormEncode(PASSWORD), "entity", "group", "message", wasFormEncode("Clue: " + "[" + clue + "]") ] ), CORRADE ); // The question has not been answered yet so return. if(--seconds != 0) { return; } llMessageLinked(LINK_THIS, 0, wasKeyValueEncode( [ "command", "tell", "group", wasFormEncode(GROUP), "password", wasFormEncode(PASSWORD), "entity", "group", "message", wasFormEncode("A correct answer would have been: " + answer + ".") ] ), CORRADE ); state select_question; } changed(integer change) { if(!(change & (CHANGED_INVENTORY | CHANGED_OWNER))) { return; } llResetScript(); } on_rez(integer param) { llResetScript(); } state_exit() { llSetTimerEvent(0); } } state select_question { state_entry() { // DEBUG llOwnerSay("Selecting a line..."); if(cardIterator < 0) { cardIterator = (integer)llFrand(llGetListLength(cards)); } card = llList2String(cards, cardIterator); dataRequest = llGetNumberOfNotecardLines(card); } dataserver(key id, string data) { if(dataRequest == id) { lineIterator = (integer)llFrand((integer)data); // DEBUG llOwnerSay("Selecting a question..."); card = llList2String(cards, cardIterator); llGetNotecardLine(card, lineIterator); return; } if(data == EOF) { --cardIterator; if(cardIterator < 0) { cardIterator = (integer)llFrand(llGetListLength(cards)); } card = llList2String(cards, cardIterator); dataRequest = llGetNumberOfNotecardLines(card); return; } // Skip empty notecard lines. if(data == "") { llGetNotecardLine(card, ++lineIterator); return; } list q = llParseString2List( data, ["//"], [""] ); list c = llParseString2List( llList2String(q, 0), ["/"], [""] ); if(select != "any" && select != llList2String(c, 0)) { // DEBUG llOwnerSay("Question for category not found, continuing..."); llGetNotecardLine(card, ++lineIterator); return; } category = llList2String(c, 0); question = llList2String(q, 1); // Build a clue string. c = llDeleteSubList(c, 0, 0); string head = llList2String(c, 0); integer i = llStringLength(head) - 1; clue = ""; do { clue += "-"; } while(--i > -1); // DEBUG llOwnerSay("Generated clue: " + clue); store = (list)head; do { store += llList2String(c, 0); c = llDeleteSubList(c, 0, 0); } while(llGetListLength(c) != 0); ++lineIterator; // DEBUG llOwnerSay("Question selected..."); state group_messages; } changed(integer change) { if(!(change & (CHANGED_INVENTORY | CHANGED_OWNER))) { return; } llResetScript(); } on_rez(integer param) { llResetScript(); } }