/source/automated-hunt-system/automated-hunt-system.lsl |
@@ -0,0 +1,755 @@ |
/////////////////////////////////////////////////////////////////////////// |
// Copyright (C) Wizardry and Steamworks 2014 - License: GNU GPLv3 // |
/////////////////////////////////////////////////////////////////////////// |
// |
// 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 GNU/GPLv3 license at: |
// http://www.gnu.org/licenses/gpl.html |
// |
/////////////////////////////////////////////////////////////////////////// |
|
/////////////////////////////////////////////////////////////////////////// |
// Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3 // |
/////////////////////////////////////////////////////////////////////////// |
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: GNU GPLv3 // |
/////////////////////////////////////////////////////////////////////////// |
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: GNU GPLv3 // |
/////////////////////////////////////////////////////////////////////////// |
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: GNU GPLv3 // |
/////////////////////////////////////////////////////////////////////////// |
// 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: GNU GPLv3 // |
/////////////////////////////////////////////////////////////////////////// |
// 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: GNU GPLv3 // |
/////////////////////////////////////////////////////////////////////////// |
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: GNU GPLv3 // |
/////////////////////////////////////////////////////////////////////////// |
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(); |
} |
} |
} |