This template is an automated hunt system that demonstrates various interactions with the Corrade scripting bot.
The script requires the following Corrade permissions:
movement
notifications
interact
inventory
economy
to be enabled.
Also, the following notifications:
permission
must be enabled.
The script assumes that the hunt item requests debit
permission from the owner and the hunt item must be in Corrade's inventory. The marketplace item contains a copy of the lootable coins bag that must be traceried to the bot's inventory.
Along with the script, a notecard called configuration
must be created and placed in the same primitive as the script. A sample configuration is the following:
######################### START CONFIGURATION ############################# # All these settings must correspond to the settings in Corrade.ini. # This is the UUID of the Corrade bot. corrade = "a6c43ad8-9086-46ee-9683-76e047062f05" # The name of the group - it can also be the UUID of the group. group = "My Group" # The password for the group. password = "mypassword" # The hunt item to rez. item = "[WaS-K] Coin Bag" # At each of these points of interest (POI), Corrade will rez a hunt item. # The POIs must be named in sequence, ie: POI_1, POI_2, etc... and you can # add as many POIs as you like. POI_1 = <166.024597, 145.024246, 3455.350342> POI_2 = <155.617996, 144.363434, 3455.368896> POI_3 = <160.74292, 155.446198, 3455.368896> ######################### END CONFIGURATION ###############################
/////////////////////////////////////////////////////////////////////////// // Copyright (C) Wizardry and Steamworks 2014 - License: CC BY 2.0 // /////////////////////////////////////////////////////////////////////////// // // This is an automated hunt system template that illustrates various // Corrade commands. You can find out more about the Corrade bot by // following the URL: http://was.fm/secondlife/scripted_agents/corrade // // This script requires the following Corrade permissions: // - movement // - notifications // - interact // - inventory // - economy // It also requires the following Corrade notifications: // - permission // // The template uses the "[WaS-K] Coin Bag" hunt item and Corrade must have // the "[WaS-K] Coin Bag" object in its inventory. Other hunt objects are // possible by setting "item" in the "configuration" notecard but the item // must be sent to Corrade such that it is in the bot's inventory. // // The "configuration" notecard inside the primitive must be changed to // reflect your settings. // // In case of panic, please see the full instructions on the project page: // http://grimore.org/secondlife/scripted_agents/corrade/projects/in_world/automated_hunt_system // or ask for help in the [Wizardry and Steamworks]:Support group or contact // Kira Komarov in-world directly. // // This script works together with a "configuration" notecard that must be // placed in the same primitive as this script. The purpose of this script // to demonstrate an automated hunt system using Corrade and you are free // to use, change, and commercialize it under the CC BY 2.0 license at: // https://creativecommons.org/licenses/by/2.0 // /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2014 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 = llParseString2List(data, ["&", "="], []); integer i = llListFindList(a, [ k ]); if(i != -1) return llList2String(a, i+1); return ""; } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2013 Wizardry and Steamworks - License: CC BY 2.0 // /////////////////////////////////////////////////////////////////////////// string wasKeyValueEncode(list data) { list k = llList2ListStrided(data, 0, -1, 2); list v = llList2ListStrided(llDeleteSubList(data, 0, 0), 0, -1, 2); data = []; do { data += llList2String(k, 0) + "=" + llList2String(v, 0); k = llDeleteSubList(k, 0, 0); v = llDeleteSubList(v, 0, 0); } while(llGetListLength(k) != 0); return llDumpList2String(data, "&"); } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2013 Wizardry and Steamworks - License: CC BY 2.0 // /////////////////////////////////////////////////////////////////////////// integer wasListCountExclude(list input, list exclude) { if(llGetListLength(input) == 0) return 0; if(llListFindList(exclude, (list)llList2String(input, 0)) == -1) return 1 + wasListCountExclude(llDeleteSubList(input, 0, 0), exclude); return wasListCountExclude(llDeleteSubList(input, 0, 0), exclude); } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0 // /////////////////////////////////////////////////////////////////////////// // escapes a string in conformance with RFC1738 string wasURLEscape(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 // /////////////////////////////////////////////////////////////////////////// // unescapes a string in conformance with RFC1738 string wasURLUnescape(string i) { return llUnescapeURL( llDumpList2String( llParseString2List( llDumpList2String( llParseString2List( i, ["+"], [] ), " " ), ["%0D%0A"], [] ), "\n" ) ); } /////////////////////////////////////////////////////////////////////////// // 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, ","); } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0 // /////////////////////////////////////////////////////////////////////////// list wasCSVToList(string csv) { list l = []; list s = []; string m = ""; do { string a = llGetSubString(csv, 0, 0); csv = llDeleteSubString(csv, 0, 0); if(a == ",") { if(llList2String(s, -1) != "\"") { l += m; m = ""; jump continue; } m += a; jump continue; } if(a == "\"" && llGetSubString(csv, 0, 0) == a) { m += a; csv = llDeleteSubString(csv, 0, 0); jump continue; } if(a == "\"") { if(llList2String(s, -1) != a) { s += a; jump continue; } s = llDeleteSubList(s, -1, -1); jump continue; } m += a; @continue; } while(csv != ""); // postcondition: length(s) = 0 return l + m; } // corrade data string CORRADE = ""; string GROUP = ""; string PASSWORD = ""; // holds the name of the item to rez string ITEM = ""; // for holding the callback URL string callback = ""; // for notecard reading integer line = 0; // key-value data will be read into this list list tuples = []; // holds the location of hunt items list POI = []; list poi = []; default { state_entry() { llSetText("", <1, 1, 1>, 1.0); if(llGetInventoryType("configuration") != INVENTORY_NOTECARD) { llOwnerSay("Sorry, could not find a configuration inventory notecard."); return; } // DEBUG llOwnerSay("Reading configuration file..."); llGetNotecardLine("configuration", line); } dataserver(key id, string data) { if(data == EOF) { // invariant, length(tuples) % 2 == 0 if(llGetListLength(tuples) % 2 != 0) { llOwnerSay("Error in configuration notecard."); return; } CORRADE = llList2String( tuples, llListFindList( tuples, [ "corrade" ] ) +1 ); if(CORRADE == "") { llOwnerSay("Error in configuration notecard: corrade"); return; } GROUP = llList2String( tuples, llListFindList( tuples, [ "group" ] ) +1 ); if(GROUP == "") { llOwnerSay("Error in configuration notecard: group"); return; } PASSWORD = llList2String( tuples, llListFindList( tuples, [ "password" ] ) +1 ); if(PASSWORD == "") { llOwnerSay("Error in configuration notecard: password"); return; } ITEM = llList2String( tuples, llListFindList( tuples, [ "item" ] ) +1 ); if(ITEM == "") { llOwnerSay("Error in configuration notecard: item"); return; } // BEGIN POI integer i = llGetListLength(tuples)-1; do { string n = llList2String(tuples, i); if(llSubStringIndex(n, "POI_") != -1) { list l = llParseString2List(n, ["_"], []); if(llList2String(l, 0) == "POI") { integer x = llList2Integer( l, 1 )-1; // extend the polygon to the number of points while(llGetListLength(POI) < x) POI += ""; // and insert the point at the location POI = llListReplaceList( POI, (list)( (vector)( "<" + llList2CSV( llParseString2List( llList2String( tuples, llListFindList( tuples, (list)n ) +1 ), ["<", ",", ">"], [] ) ) + ">") ), x, x ); } } } while(--i>-1); // now clean up any empty slots i = llGetListLength(POI)-1; do { if(llList2String(POI, i) == "") POI = llDeleteSubList(POI, i, i); } while(--i > -1); // END POI // DEBUG llOwnerSay("Read configuration notecard..."); state url; } if(data == "") jump continue; integer i = llSubStringIndex(data, "#"); if(i != -1) data = llDeleteSubString(data, i, -1); list o = llParseString2List(data, ["="], []); // get rid of starting and ending quotes string k = llDumpList2String( llParseString2List( llStringTrim( llList2String( o, 0 ), STRING_TRIM), ["\""], [] ), "\""); string v = llDumpList2String( llParseString2List( llStringTrim( llList2String( o, 1 ), STRING_TRIM), ["\""], [] ), "\""); if(k == "" || v == "") jump continue; tuples += k; tuples += v; @continue; llGetNotecardLine("configuration", ++line); } on_rez(integer num) { llResetScript(); } changed(integer change) { if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) { llResetScript(); } } } state url { state_entry() { // DEBUG llOwnerSay("Requesting URL..."); llRequestURL(); } http_request(key id, string method, string body) { if(method != URL_REQUEST_GRANTED) return; callback = body; // DEBUG llOwnerSay("Got URL..."); state detect; } on_rez(integer num) { llResetScript(); } changed(integer change) { if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) { llResetScript(); } } } state detect { state_entry() { // DEBUG llOwnerSay("Detecting if Corrade is online..."); llSetTimerEvent(1); } timer() { llRequestAgentData((key)CORRADE, DATA_ONLINE); } dataserver(key id, string data) { if(data != "1") { // DEBUG llOwnerSay("Corrade is not online, sleeping..."); llSetTimerEvent(5); return; } state permission; } on_rez(integer num) { llResetScript(); } changed(integer change) { if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) { llResetScript(); } } state_exit() { llSetTimerEvent(0); } } state permission { state_entry() { // DEBUG llOwnerSay("Binding to the permission notification..."); llInstantMessage(CORRADE, wasKeyValueEncode( [ "command", "notify", "group", wasURLEscape(GROUP), "password", wasURLEscape(PASSWORD), "action", "set", "type", "permission", "URL", wasURLEscape(callback), "callback", wasURLEscape(callback) ] ) ); llSetTimerEvent(60); } http_request(key id, string method, string body) { llHTTPResponse(id, 200, "OK"); if(wasKeyValueGet("command", body) != "notify" || wasKeyValueGet("success", body) != "True") { // DEBUG llOwnerSay("Failed to bind to the permission notification: " + wasURLUnescape( wasKeyValueGet( "error", body ) ) ); return; } // DEBUG llOwnerSay("Permission notification installed..."); state menu; } timer() { // alarm hit, permission notification not installed llResetScript(); } on_rez(integer num) { llResetScript(); } changed(integer change) { if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) { llResetScript(); } } state_exit() { llSetTimerEvent(0); } } state menu { state_entry() { llSetText("Touch me for menu!", <0, 1, 0>, 1.0); } touch_start(integer num) { if(llDetectedKey(0) != llGetOwner()) return; integer comChannel = (integer)("0x8" + llGetSubString(llGetKey(), 0, 6)); llListen(comChannel, "", llGetOwner(), ""); llDialog(llGetOwner(), "The menu will allow you to cast and dispel the hunt items.", [ "Cast", "Dispel" ], comChannel); } listen(integer channel, string name, key id, string message) { /* Copy the POI list to recurse over. */ poi = POI; /* Process the dialog messages. */ if(message == "Cast") state rez; if(message == "Dispel") state derez; } changed(integer change) { if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) { llResetScript(); } } state_exit() { llSetTimerEvent(0); } } /* * In order to rez all the items we permute the "poi" list by recursing over states. * The "rez_trampoline" provides a trampoline for the "rez" state re-entry. */ state rez_trampoline { state_entry() { llSetTimerEvent(1); } timer() { state rez; } state_exit() { llSetTimerEvent(0); } } /* * Rez the hunt item from inventory, grant debit permission and trampoline for the * next item in the POI list. */ state rez { state_entry() { // If we have rezzed all the objects, then stop rezzing. if(llGetListLength(poi) == 0) state menu; llSetText("Hunt items left to set-up: " + (string) llGetListLength( poi ), <0, 1, 1>, 1.0 ); // Permute POIs string head = llList2String(poi, 0); poi = llDeleteSubList(poi, 0, 0); // DEBUG llOwnerSay("Rezzing @ " + head); llInstantMessage(CORRADE, wasKeyValueEncode( [ "command", "rez", "group", wasURLEscape(GROUP), "password", wasURLEscape(PASSWORD), "position", wasURLEscape(head), "item", wasURLEscape(ITEM), "callback", wasURLEscape(callback) ] ) ); } http_request(key id, string method, string body) { llHTTPResponse(id, 200, "OK"); // Get the result of rezzing the object. if(wasKeyValueGet("command", body) == "rez") { if(wasKeyValueGet("success", body) != "True") { // DEBUG llOwnerSay("Failed to rez the object: " + wasURLUnescape( wasKeyValueGet( "error", body ) ) ); return; } llOwnerSay("Item rezzed..."); return; } // Grant debit permissions to the rezzed object. if(wasKeyValueGet("type", body) == "permission" && wasKeyValueGet("permissions", body) == "Debit") { llInstantMessage(CORRADE, wasKeyValueEncode( [ "command", "replytoscriptpermissionrequest", "group", wasURLEscape(GROUP), "password", wasURLEscape(PASSWORD), "task", wasKeyValueGet("task", body), "item", wasKeyValueGet("item", body), "region", wasKeyValueGet("region", body), "action", "reply", "permissions", "Debit", "callback", wasURLEscape(callback) ] ) ); // DEBUG llOwnerSay("Replying to permission request..."); return; } // Get the result of granting script permissions. if(wasKeyValueGet("command", body) == "replytoscriptpermissionrequest") { if(wasKeyValueGet("success", body) != "True") { // DEBUG llOwnerSay("Failed to grant permissions to the object: " + wasURLUnescape( wasKeyValueGet( "error", body ) ) ); return; } llOwnerSay("Permissions granted..."); // Go for the next item in the POI list. state rez_trampoline; } } on_rez(integer num) { llResetScript(); } changed(integer change) { if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) { llResetScript(); } } } /* * In order to de-rez the hunt items we first teleport Corrade in the vicinity * of the POI and then issue a "derez" command to Corrade. * Symmetrically to "rez", the "derez_trampoline" state provides a trampoline * for the "derez" state re-entry. */ state derez_trampoline { state_entry() { llSetTimerEvent(1); } timer() { state derez; } state_exit() { llSetTimerEvent(0); } } state derez { state_entry() { // If we have derezzed all the objects, then stop rezzing. if(llGetListLength(poi) == 0) state menu; llSetText("Hunt items left to remove: " + (string) llGetListLength( poi ), <0, 1, 1>, 1.0 ); // Permute POIs string head = llList2String(poi, 0); poi = llDeleteSubList(poi, 0, 0); // DEBUG llOwnerSay("Teleporting to: " + (string)head); llInstantMessage((key)CORRADE, wasKeyValueEncode( [ "command", "teleport", "group", wasURLEscape(GROUP), "password", wasURLEscape(PASSWORD), "region", wasURLEscape(llGetRegionName()), "position", wasURLEscape(head), "entity", "region", "fly", "True", "callback", wasURLEscape(callback) ] ) ); } http_request(key id, string method, string body) { llHTTPResponse(id, 200, "OK"); // Get the result of teleporting to the POI. if(wasKeyValueGet("command", body) == "teleport") { // If the teleport did not succeed and the error was not that the destination // was too close, then print the error and stop; otherwise, continue. if(wasKeyValueGet("success", body) != "True" && wasKeyValueGet("status", body) != "37559") { // DEBUG llOwnerSay("Failed to teleport: " + wasURLUnescape( wasKeyValueGet( "error", body ) ) ); return; } // DEBUG llOwnerSay("Teleport succeeded..."); // If the teleport succeeded, request to derez the item. llInstantMessage((key)CORRADE, wasKeyValueEncode( [ "command", "derez", "group", wasURLEscape(GROUP), "password", wasURLEscape(PASSWORD), "item", wasURLEscape(ITEM), "range", 5, "callback", wasURLEscape(callback) ] ) ); return; } // Get the result of the derez request. if(wasKeyValueGet("command", body) == "derez") { // If removing the item because the item was not found, then it was // probably consumed during the hunt so carry on to the next destination. if(wasKeyValueGet("success", body) != "True" && wasKeyValueGet("status", body) != "22693") { // DEBUG llOwnerSay("Failed to derez: " + wasURLUnescape( wasKeyValueGet( "error", body ) ) ); return; } // DEBUG llOwnerSay("Derez succeeded..."); state derez_trampoline; } } on_rez(integer num) { llResetScript(); } changed(integer change) { if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) { llResetScript(); } } }