28b36c20-b296-2f40-1159-8a8e49169079_script.lsl 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. //
  2. // The line above should be left blank to avoid script errors in OpenSim.
  3. // Sloodle quiz chair
  4. // Allows SL users to take Moodle quizzes in-world
  5. // Part of the Sloodle project (www.sloodle.org)
  6. //
  7. // Copyright (c) 2006-9 Sloodle (various contributors)
  8. // Released under the GNU GPL
  9. //
  10. // Contributors:
  11. // Edmund Edgar
  12. // Paul Preibisch
  13. // Peter R. Bloomfield
  14. // Once configured in the usual way, this script waits for a request to ask a question in the form of a linked message with num SLOODLE_CHANNEL_QUIZ_ASK_QUESTION.
  15. // When the student answers the question, it sends out a linked message with num SLOODLE_CHANNEL_QUESTION_ANSWERED_AVATAR.
  16. // Note that it doesn't handle timeouts in case the user doesn't respond - all that should be done by the calling script.
  17. integer SLOODLE_CHANNEL_QUIZ_STOP_FOR_AVATAR = -1639271119; //Tells us to STOP a quiz for the avatar
  18. integer SLOODLE_CHANNEL_ERROR_TRANSLATION_REQUEST=-1828374651;
  19. integer doRepeat = 0; // whether we should run through the questions again when we're done
  20. integer doDialog = 1; // whether we should ask the questions using dialog rather than chat
  21. integer menu_channel;
  22. string sloodlehttpvars;
  23. integer answerDialogListenHandler;
  24. integer answerChatListenHandler;
  25. integer answerChatListenHandlerNonPublic;
  26. integer SLOODLE_CHANNEL_QUESTION_ASKED_AVATAR = -1639271105; //Sent by main quiz script to tell UI scripts that question has been asked to avatar with key. String contains question ID + "|" + question text
  27. integer SLOODLE_CHANNEL_QUESTION_ANSWERED_AVATAR = -1639271106; //Sent by main quiz script to tell UI scripts that question has been answered by avatar with key. String contains selected option ID + "|" + option text + "|"
  28. integer SLOODLE_CHANNEL_QUIZ_LOADING_QUESTION = -1639271107;
  29. integer SLOODLE_CHANNEL_ANSWER_SCORE_FOR_AVATAR = -1639271113; // Tells anyone who might be interested that we scored the answer. Score in string, avatar in key.
  30. integer SLOODLE_CHANNEL_QUIZ_STATE_ENTRY_QUIZZING = -1639271117; //mod quiz script is in state quizzing
  31. integer SLOODLE_CHANNEL_QUIZ_ERROR_INVALID_QUESION = -1639271121; //
  32. integer SLOODLE_CHANNEL_QUIZ_ERROR_NO_ATTEMPTS_LEFT= -1639271123; //
  33. integer SLOODLE_CHANNEL_QUIZ_ERROR_NO_QUESTIONS= -1639271124; //
  34. integer SLOODLE_CHANNEL_QUIZ_ASK_QUESTION_CHAT = -1639271125; // Tells the question handler scripts to ask the question with the ID in str to the avatar with key VIA CHAT.
  35. integer SLOODLE_CHANNEL_QUIZ_ASK_QUESTION_DIALOG = -1639271126; // Tells the question handler scripts to ask the question with the ID in str to the avatar with key VIA DIALOG.
  36. ///// TRANSLATION /////
  37. // Link message channels
  38. integer SLOODLE_CHANNEL_TRANSLATION_REQUEST = -1928374651;
  39. // Translation output methods
  40. string SLOODLE_TRANSLATE_WHISPER = "whisper"; // 1 output parameter: chat channel number
  41. string SLOODLE_TRANSLATE_SAY = "say"; // 1 output parameter: chat channel number
  42. string SLOODLE_TRANSLATE_OWNER_SAY = "ownersay"; // No output parameters
  43. string SLOODLE_TRANSLATE_DIALOG = "dialog"; // Recipient avatar should be identified in link message keyval. At least 2 output parameters: first the channel number for the dialog, and then 1 to 12 button label strings.
  44. string SLOODLE_TRANSLATE_LOAD_URL = "loadurl"; // Recipient avatar should be identified in link message keyval. 1 output parameter giving URL to load.
  45. string SLOODLE_TRANSLATE_IM = "instantmessage"; // Recipient avatar should be identified in link message keyval. No output parameters.
  46. string sloodle_full_quiz_url;
  47. key httpfetchquestionquery = NULL_KEY;
  48. key feedbackreq = NULL_KEY;
  49. float request_timeout = 20.0;
  50. integer question_id = -1;
  51. // Text and type of the current and next question
  52. string qtext = "";
  53. string qtype = "";
  54. // Lists of option information for the current question
  55. list opids = []; // IDs
  56. list optext = []; // Texts
  57. list opgrade = []; // Grades
  58. list opfeedback = []; // Feedback if this option is selected
  59. // Avatar currently using this cahir
  60. key sitter = NULL_KEY;
  61. ///// FUNCTIONS /////
  62. /******************************************************************************************************************************
  63. * sloodle_error_code -
  64. * Author: Paul Preibisch
  65. * Description - This function sends a linked message on the SLOODLE_CHANNEL_ERROR_TRANSLATION_REQUEST channel
  66. * The error_messages script hears this, translates the status code and sends an instant message to the avuuid
  67. * Params: method - SLOODLE_TRANSLATE_SAY, SLOODLE_TRANSLATE_IM etc
  68. * Params: avuuid - this is the avatar UUID to that an instant message with the translated error code will be sent to
  69. * Params: status code - the status code of the error as on our wiki: http://slisweb.sjsu.edu/sl/index.php/Sloodle_status_codes
  70. *******************************************************************************************************************************/
  71. sloodle_error_code(string method, key avuuid,integer statuscode){
  72. llMessageLinked(LINK_SET, SLOODLE_CHANNEL_ERROR_TRANSLATION_REQUEST, method+"|"+(string)avuuid+"|"+(string)statuscode, NULL_KEY);
  73. } sloodle_debug(string msg)
  74. {
  75. llMessageLinked(LINK_THIS, DEBUG_CHANNEL, msg, NULL_KEY);
  76. }
  77. integer random_integer( integer min, integer max )
  78. {
  79. return min + (integer)( llFrand( max - min + 1 ) );
  80. }
  81. // Query the server for the identified question (request by global question ID)
  82. key request_question( integer qid )
  83. {
  84. llMessageLinked(LINK_SET, SLOODLE_CHANNEL_QUIZ_LOADING_QUESTION, (string)qid, sitter);
  85. // Request the identified question from Moodle
  86. string body = sloodlehttpvars+"&ltq=" + (string)qid;
  87. key newhttp = llHTTPRequest(sloodle_full_quiz_url, [HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/x-www-form-urlencoded"], body);
  88. llSetTimerEvent(0.0);
  89. llSetTimerEvent(request_timeout);
  90. return newhttp;
  91. }
  92. // Query the server for the feedback for a particular choice.
  93. // This is only called if the server has told us that the feedback is too long to go in the regular request
  94. // It does this by substituting the feedback [[[LONG]]]
  95. key request_feedback( integer qid, string fid ) {
  96. // Request the identified question from Moodle
  97. string body = sloodlehttpvars;
  98. body += "&ltq=" + (string)qid;
  99. body += "&fid=" + (string)fid;
  100. key reqid = llHTTPRequest(sloodle_full_quiz_url, [HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/x-www-form-urlencoded"], body);
  101. llSleep(3.0); // Hopefully the message will come back before the next question is asked. But if it comes back out of order, we won't insist.
  102. return reqid;
  103. }
  104. // Notify the server of a response
  105. notify_server(string qtype, integer questioncode, string responsecode, float scorechange)
  106. {
  107. string body =sloodlehttpvars;
  108. body += "&resp" + (string)questioncode + "_=" + responsecode;
  109. body += "&resp" + (string)questioncode + "_submit=1";
  110. body += "&questionids=" + (string)questioncode;
  111. body += "&action=notify";
  112. body += "&scorechange="+(string)scorechange;
  113. llHTTPRequest(sloodle_full_quiz_url, [HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/x-www-form-urlencoded"], body);
  114. }
  115. // Ask the current question
  116. ask_question()
  117. {
  118. // Are we using dialogs?
  119. if (doDialog == 1) {
  120. // We want to create a dialog with the option texts embedded into the main text,
  121. // and numbers on the buttons
  122. integer qi;
  123. list qdialogoptions = [];
  124. string qdialogtext = qtext + "\n";
  125. // Go through each option
  126. integer num_options = llGetListLength(optext);
  127. if ((qtype == "numerical")|| (qtype == "shortanswer")) {
  128. // Ask the question via IM
  129. llInstantMessage(sitter, qtext);
  130. } else {
  131. for (qi = 1; qi <= num_options; qi++) {
  132. // Append this option to the main dialog (remebering buttons are 1-based, but lists 0-based)
  133. qdialogtext += (string)qi + ": " + llList2String(optext,qi-1) + "\n";
  134. // Add a button for this option
  135. qdialogoptions = qdialogoptions + [(string)qi];
  136. }
  137. // Present the dialog to the user
  138. answerDialogListenHandler = llListen(menu_channel, "", sitter, "");
  139. llDialog(sitter, qdialogtext, qdialogoptions, menu_channel);
  140. }
  141. } else {
  142. // Ask the question via IM
  143. llListenRemove(answerChatListenHandler); // cancel any existing listens before creating a new one
  144. answerChatListenHandler = llListen(0, "", sitter, "");
  145. // Listen on channel 111 to allow people to give answers without everyone else hearing them.
  146. llListenRemove(answerChatListenHandlerNonPublic); // cancel any existing listens before creating a new one
  147. answerChatListenHandlerNonPublic = llListen(111, "", sitter, "");
  148. llInstantMessage(sitter, qtext);
  149. // Offer the options via IM
  150. integer x = 0;
  151. integer num_options = llGetListLength(optext);
  152. for (x = 0; x < num_options; x++) {
  153. llInstantMessage(sitter, (string)(x + 1) + ". " + llList2String(optext, x));
  154. }
  155. }
  156. llMessageLinked(LINK_SET, SLOODLE_CHANNEL_QUESTION_ASKED_AVATAR, (string)question_id+"|"+qtext, sitter);
  157. }
  158. // Send a translation request link message
  159. sloodle_translation_request(string output_method, list output_params, string string_name, list string_params, key keyval, string batch){
  160. llMessageLinked(LINK_THIS, SLOODLE_CHANNEL_TRANSLATION_REQUEST, output_method + "|" + llList2CSV(output_params) + "|" + string_name + "|" + llList2CSV(string_params) + "|" + batch, keyval);
  161. }
  162. ///// ----------- /////
  163. ///// STATES /////
  164. default{
  165. on_rez(integer param)
  166. {
  167. llResetScript();
  168. }
  169. link_message(integer sender_num, integer num, string str, key id){
  170. if (num ==SLOODLE_CHANNEL_QUIZ_STOP_FOR_AVATAR){
  171. llListenRemove(answerDialogListenHandler);
  172. llListenRemove(answerChatListenHandler);
  173. llListenRemove(answerChatListenHandlerNonPublic);
  174. }
  175. else
  176. if (num == SLOODLE_CHANNEL_QUIZ_STATE_ENTRY_QUIZZING){
  177. sitter=id;
  178. llListenRemove(answerDialogListenHandler); // Cancel any existing listens
  179. menu_channel = random_integer(-90000,-900000);
  180. }
  181. else
  182. if (num == SLOODLE_CHANNEL_QUIZ_ASK_QUESTION_DIALOG||num == SLOODLE_CHANNEL_QUIZ_ASK_QUESTION_CHAT) {//todo add to dia
  183. list data = llParseString2List(str, ["|"], []);
  184. question_id = llList2Integer(data, 0);
  185. sloodle_full_quiz_url = llList2String(data, 1);//todo add to dia
  186. integer offset = llStringLength(llList2String(data,0)+"|"+sloodle_full_quiz_url+"|");
  187. sloodlehttpvars = llGetSubString(str, offset, -1);//-1 is the end of the list
  188. sitter = id;
  189. if (num == SLOODLE_CHANNEL_QUIZ_ASK_QUESTION_DIALOG) {//todo add to dia
  190. doDialog=TRUE; //todo add to dia
  191. }else{
  192. doDialog=FALSE;//todo add to dia
  193. }
  194. httpfetchquestionquery = request_question((integer)str);
  195. }
  196. }
  197. state_exit()
  198. {
  199. llSetTimerEvent(0.0);
  200. }
  201. listen(integer channel, string name, key id, string message)
  202. {
  203. // If using dialogs, then only listen to the dialog channel
  204. if (doDialog && ((qtype == "multichoice") || (qtype == "truefalse"))) {
  205. if (channel != menu_channel) {
  206. sloodle_translation_request(SLOODLE_TRANSLATE_IM, [0], "usedialogs", [llKey2Name(sitter)], sitter, "quiz");
  207. return;
  208. }
  209. } else {
  210. if (channel != 0) return;
  211. }
  212. string opid; // used when the feedback is too long, and we have to fetch it off the server
  213. // Only listen to the sitter
  214. if (id == sitter) {
  215. // Handle the answer...
  216. float scorechange = 0;
  217. string feedback = "";
  218. string answeroptext = "";
  219. // Check the type of question this was
  220. if ((qtype == "multichoice") || (qtype == "truefalse")) {
  221. // Multiple choice - the response should be a number from the dialog box (1-based)
  222. integer answer_num = (integer)message;
  223. // Make sure it's valid
  224. if ((answer_num > 0) && (answer_num <= llGetListLength(opids))) {
  225. // Correct to 0-based
  226. answer_num -= 1;
  227. feedback = llList2String(opfeedback, answer_num);
  228. scorechange = llList2Float(opgrade, answer_num);
  229. opid = llList2String(opids, answer_num);
  230. answeroptext = llList2String(optext, answer_num);
  231. // Notify the server of the response
  232. notify_server(qtype, question_id, llList2String(opids, answer_num),scorechange);
  233. } else {
  234. sloodle_translation_request(SLOODLE_TRANSLATE_SAY, [0], "invalidchoice", [llKey2Name(sitter)], NULL_KEY, "quiz");
  235. ask_question();
  236. }
  237. } else if (qtype == "shortanswer") {
  238. // Notify the server of the response
  239. integer x = 0;
  240. integer num_options = llGetListLength(optext);
  241. for (x = 0; x < num_options; x++) {
  242. if (llToLower(message) == llToLower(llList2String(optext, x))) {
  243. feedback = llList2String(opfeedback, x);
  244. scorechange = llList2Float(opgrade, x);
  245. opid = llList2String(opids, x);
  246. answeroptext = llList2String(optext, x);
  247. }
  248. notify_server(qtype, question_id, message, scorechange);
  249. }
  250. } else if (qtype == "numerical") {
  251. // Notify the server of the response
  252. float number = (float)message;
  253. integer x = 0;
  254. integer num_options = llGetListLength(optext);
  255. for (x = 0; x < num_options; x++) {
  256. if (number == (float)llList2String(optext, x)) {
  257. feedback = llList2String(opfeedback, x);
  258. scorechange = llList2Float(opgrade, x);
  259. opid = llList2String(opids, x);
  260. answeroptext = llList2String(optext, x);
  261. }
  262. notify_server(qtype, question_id, message, scorechange);
  263. }
  264. }
  265. else {
  266. sloodle_translation_request(SLOODLE_TRANSLATE_SAY, [0], "invalidtype", [qtype], NULL_KEY, "quiz");
  267. }
  268. llMessageLinked(LINK_SET, SLOODLE_CHANNEL_QUESTION_ANSWERED_AVATAR, opid+"|"+answeroptext, sitter);
  269. if (feedback == "[[LONG]]") // special long feedback placeholder for when there is too much feedback to give to the script
  270. feedbackreq = request_feedback( question_id, opid );
  271. else if (feedback != "") llInstantMessage(sitter, feedback); // Text feedback
  272. else if (scorechange > 0.0) {
  273. sloodle_translation_request(SLOODLE_TRANSLATE_IM, [0], "correct", [llKey2Name(sitter)], sitter, "quiz");
  274. //num_correct += 1; SAL commented out this
  275. } else {
  276. sloodle_translation_request(SLOODLE_TRANSLATE_IM, [0], "incorrect", [llKey2Name(sitter)], sitter, "quiz");
  277. }
  278. llSleep(1.); //wait to finish the sloodle_translation_request before next question.
  279. // Clear out our current data (a feeble attempt to save memory!)
  280. qtext = "";
  281. qtype = "";
  282. opids = [];
  283. optext = [];
  284. opgrade = [];
  285. opfeedback = [];
  286. llMessageLinked(LINK_SET, SLOODLE_CHANNEL_ANSWER_SCORE_FOR_AVATAR, (string)scorechange, sitter);
  287. }
  288. }
  289. timer()
  290. {
  291. // There has been a timeout of the HTTP request
  292. sloodle_translation_request(SLOODLE_TRANSLATE_SAY, [0], "httptimeout", [], NULL_KEY, "");
  293. llSetTimerEvent(0.0);
  294. if (question_id > -1) {
  295. httpfetchquestionquery = request_question(question_id);
  296. }
  297. }
  298. http_response(key request_id, integer status, list metadata, string body)
  299. {
  300. // This response will always contain question data.
  301. // If the current question is being loaded, then ask it right away, and load the next.
  302. // If the next question is being loaded, then just store it.
  303. // It will be made current and asked whenever the current one gets answered.
  304. // If the user ever gets ahead of our loading, then they will be waiting on the 'current' question.
  305. // As soon as that is loaded, it will get asked.
  306. // Is this the response we are expecting?
  307. if (request_id == httpfetchquestionquery) {
  308. httpfetchquestionquery = NULL_KEY;
  309. llSetTimerEvent(0.0);
  310. } else if (request_id != feedbackreq) {
  311. return;
  312. }
  313. // Make sure the response was OK
  314. if (status != 200) {
  315. sloodle_error_code(SLOODLE_TRANSLATE_SAY, NULL_KEY,status); //send message to error_message.lsl
  316. if (request_id!=feedbackreq){
  317. request_question( question_id );//TODO update DIA
  318. }
  319. }
  320. // Split the response into several lines
  321. list lines = llParseStringKeepNulls(body, ["\n"], []);
  322. integer numlines = llGetListLength(lines);
  323. body = "";
  324. list statusfields = llParseStringKeepNulls(llList2String(lines,0), ["|"], []);
  325. integer statuscode = llList2Integer(statusfields, 0);
  326. // Was it an error code?
  327. if (statuscode == -10301) {
  328. sloodle_translation_request(SLOODLE_TRANSLATE_SAY, [0], "noattemptsleft", [llKey2Name(sitter)], NULL_KEY, "");
  329. llMessageLinked(LINK_SET, SLOODLE_CHANNEL_QUIZ_ERROR_NO_ATTEMPTS_LEFT, (string)question_id, sitter);//todo add to dia
  330. return;
  331. } else if (statuscode == -10302) {
  332. sloodle_translation_request(SLOODLE_TRANSLATE_SAY, [0], "noquestions", [], NULL_KEY, "");
  333. llMessageLinked(LINK_SET, SLOODLE_CHANNEL_QUIZ_ERROR_NO_QUESTIONS, (string)question_id, sitter);//todo add to dia
  334. return;
  335. } else if (statuscode <= 0) {
  336. //sloodle_translation_request(SLOODLE_TRANSLATE_SAY, [0], "servererror", [statuscode], NULL_KEY, "");
  337. sloodle_error_code(SLOODLE_TRANSLATE_IM, sitter,statuscode); //send message to error_message.lsl
  338. // Check if an error message was reported
  339. if (numlines > 1) sloodle_debug(llList2String(lines, 1));
  340. return;
  341. }
  342. if (request_id == feedbackreq) {
  343. llInstantMessage( sitter, llList2String(lines, 1) );
  344. return;
  345. }
  346. // Save a tiny bit of memory!
  347. statusfields = [];
  348. // Go through each line of the response
  349. integer i = 0;
  350. for (i = 1; i < numlines; i++) {
  351. // Extract and parse the current line
  352. list thisline = llParseStringKeepNulls(llList2String(lines, i),["|"],[]);
  353. string rowtype = llList2String( thisline, 0 );
  354. // Check what type of line this is
  355. if ( rowtype == "question" ) {
  356. // Grab the question information and reset the options
  357. qtext = llList2String(thisline, 4);
  358. qtype = llList2String(thisline, 7);
  359. opids = [];
  360. optext = [];
  361. opgrade = [];
  362. opfeedback = [];
  363. // Make sure it's a valid question type
  364. if ((qtype != "multichoice") && (qtype != "truefalse") && (qtype != "numerical") && (qtype != "shortanswer")) {
  365. sloodle_translation_request(SLOODLE_TRANSLATE_SAY, [0], "invalidtype", [qtype], NULL_KEY, "quiz");
  366. sloodle_translation_request(SLOODLE_TRANSLATE_SAY, [0], "resetting", [], NULL_KEY, "");
  367. llMessageLinked(LINK_SET, SLOODLE_CHANNEL_QUIZ_ERROR_INVALID_QUESION, (string)question_id, sitter);//todo add to dia
  368. return;
  369. }
  370. } else if ( rowtype == "questionoption" ) {
  371. // Add this option to the appropriate place
  372. opids += [(integer)llList2String(thisline, 2)];
  373. optext += [llList2String(thisline, 4)];
  374. opgrade += [(float)llList2String(thisline, 5)];
  375. opfeedback += [llList2String(thisline, 6)];
  376. }
  377. }
  378. // Automatically ask this question
  379. ask_question();
  380. }
  381. }
  382. // Please leave the following line intact to show where the script lives in Git:
  383. // SLOODLE LSL Script Git Location: mod/quiz-1.0/objects/default/assets/sloodle_quiz_question_handler.lslp