//***************************************************************************** // // scripts.c // // NakedMud makes extensive use of scripting. It uses scripting to generate // objects, mobiles, and rooms when they are loaded into the game. There are // also scripting hooks for these things (commonly referred to as triggers), // which allow them to be a bit more dynamic and flavorful in the game. For // instance, greetings when someone enters a room, repsonses to questions, // actions when items are received.. you know... that sort of stuff. // //***************************************************************************** #include "../mud.h" #include "../utils.h" #include "../world.h" #include "../character.h" #include "../room.h" #include "../object.h" #include "../socket.h" #include "../auxiliary.h" #include "../storage.h" #include "../handler.h" #include "../hooks.h" #include "scripts.h" #include "pyplugs.h" #include "pychar.h" #include "pyroom.h" #include "pyexit.h" #include "pyobj.h" #include "pymud.h" #include "pyevent.h" #include "pystorage.h" #include "pyauxiliary.h" #include "trighooks.h" // online editor stuff #include "../editor/editor.h" #include "script_editor.h" //***************************************************************************** // auxiliary data //***************************************************************************** typedef struct { LIST *triggers; } TRIGGER_AUX_DATA; TRIGGER_AUX_DATA *newTriggerAuxData(void) { TRIGGER_AUX_DATA *data = malloc(sizeof(TRIGGER_AUX_DATA)); data->triggers = newList(); return data; } void deleteTriggerAuxData(TRIGGER_AUX_DATA *data) { deleteListWith(data->triggers, free); free(data); } void triggerAuxDataCopyTo(TRIGGER_AUX_DATA *from, TRIGGER_AUX_DATA *to) { deleteListWith(to->triggers, free); to->triggers = listCopyWith(from->triggers, strdup); } TRIGGER_AUX_DATA *triggerAuxDataCopy(TRIGGER_AUX_DATA *data) { TRIGGER_AUX_DATA *newdata = malloc(sizeof(TRIGGER_AUX_DATA)); newdata->triggers = listCopyWith(data->triggers, strdup); return newdata; } char *read_one_trigger(STORAGE_SET *set) { return strdup(read_string(set, "trigger")); } TRIGGER_AUX_DATA *triggerAuxDataRead(STORAGE_SET *set) { TRIGGER_AUX_DATA *data = malloc(sizeof(TRIGGER_AUX_DATA)); data->triggers = gen_read_list(read_list(set, "triggers"), read_one_trigger); return data; } STORAGE_SET *store_one_trigger(char *key) { STORAGE_SET *set = new_storage_set(); store_string(set, "trigger", key); return set; } STORAGE_SET *triggerAuxDataStore(TRIGGER_AUX_DATA *data) { STORAGE_SET *set = new_storage_set(); store_list(set, "triggers", gen_store_list(data->triggers,store_one_trigger)); return set; } //***************************************************************************** // local datastructures, functions, and defines //***************************************************************************** // // a stack that keeps track of the locale scripts are running in LIST *locale_stack = NULL; // // Many thanks to Xanthros who pointed out that, if two scripts trigger // eachother, we could get tossed into an infinite loop. He suggested a // check for loop depth before each script is run. If we are deeper than // some maximum depth, do not run the script. #define MAX_LOOP_DEPTH 30 int script_loop_depth = 0; // a local variable used for storing whether or not the last script ran fine bool script_ok = TRUE; // // looks for dynamic descs and expands them out as needed. Dynamic descs are // bits of code that are embedded within descriptions, and surrounded by [ and // ]. They can be anything that returns a (string or numeric) value. Code must // be a single statement. To perform conditional output, it is common to use // the ite() (if, then, else) function, which takes 2 arguments and an optional // third.Here would be some valid dynamic descriptions (assuming that the // variables I made up exist in your mud): // You see [me.getvar("flowers")] flowers blooming here. // [ite(ch.perception >10, "There is a large bird's nest on the east cliff.")] // [ite(ch.name=="Bob", "You are in your house.", "You are in Bob's house.")] // You are in [ite(ch.name == "Bob", "your", ch.name + "'s")] house. void expand_dynamic_descs(BUFFER *desc, PyObject *me, CHAR_DATA *ch) { // make a new temp buffer to hold all of the expanded data BUFFER *new_desc = newBuffer(bufferLength(desc)*2); char code[SMALL_BUFFER]; PyObject *dict = NULL; int start, end, i, size = bufferLength(desc); for(i = 0; i < size; i++) { // figure out when our next dynamic desc is. start = next_letter_in(bufferString(desc) + i, '['); // no more if(start == -1) { // copy the rest and skip to the end of the buffer bprintf(new_desc, "%s", bufferString(desc) + i); i = size - 1; } // we have another desc else { // copy everything up to start while(start > 0) { bprintf(new_desc, "%c", *(bufferString(desc) + i)); start--; i++; } // skip the start marker i++; // find our end end = next_letter_in(bufferString(desc) + i, ']'); // make sure we have it if(end == -1) break; // copy everything between start and end strncpy(code, bufferString(desc) + i, end); code[end] = '\0'; // skip i up to the end i = i + end; // if we haven't already created a dict, do it now if(dict == NULL) { PyObject *pych = newPyChar(ch); dict = restricted_script_dict(); PyDict_SetItemString(dict, "me", me); PyDict_SetItemString(dict, "ch", pych); Py_DECREF(pych); } // evaluate the code PyObject *retval = PyRun_String(code, Py_eval_input, dict, dict); // did we encounter an error? if(retval == NULL) { char *tb = getPythonTraceback(); log_string("Dynamic desc terminated with an error:\r\n%s\r\n" "\r\nTraceback is:\r\n%s\r\n", code, tb); free(tb); break; } // append the output else if(PyString_Check(retval)) bprintf(new_desc, "%s", PyString_AsString(retval)); else if(PyInt_Check(retval)) bprintf(new_desc, "%ld", PyInt_AsLong(retval)); else if(PyFloat_Check(retval)) bprintf(new_desc, "%lf", PyFloat_AsDouble(retval)); // invalid return type... else if(retval != Py_None) log_string("dynamic desc had invalid evaluation: %s", code); Py_XDECREF(retval); } } // copy over the changes, and free our buffer bufferCopyTo(new_desc, desc); deleteBuffer(new_desc); // free up our dictionary Py_XDECREF(dict); } void expand_char_dynamic_descs(BUFFER *desc, CHAR_DATA *me, CHAR_DATA *ch) { PyObject *pyme = newPyChar(me); expand_dynamic_descs(desc, pyme, ch); Py_DECREF(pyme); } void expand_obj_dynamic_descs(BUFFER *desc, OBJ_DATA *me, CHAR_DATA *ch) { PyObject *pyme = newPyObj(me); expand_dynamic_descs(desc, pyme, ch); Py_DECREF(pyme); } void expand_room_dynamic_descs(BUFFER *desc, ROOM_DATA *me, CHAR_DATA *ch) { PyObject *pyme = newPyRoom(me); expand_dynamic_descs(desc, pyme, ch); Py_DECREF(pyme); } void finalize_scripts(void *none1, void *none2, void *none3) { Py_Finalize(); } //***************************************************************************** // player commands //***************************************************************************** // // displays info on a script to a person COMMAND(cmd_tstat) { if(!charGetSocket(ch)) return; else { TRIGGER_DATA *trig = worldGetType(gameworld, "trigger", get_fullkey_relative(arg, get_key_locale(roomGetClass(charGetRoom(ch))))); if(trig == NULL) send_to_char(ch, "No trigger exists with that key.\r\n"); else { send_to_socket(charGetSocket(ch), "--------------------------------------------------------------------------------\r\n" "Name : %s\r\n" "Trigger type : %s\r\n" "--------------------------------------------------------------------------------\r\n", triggerGetName(trig), triggerGetType(trig)); script_display(charGetSocket(ch), triggerGetCode(trig), FALSE); } } } // // attach a new trigger to the given instanced object/mobile/room COMMAND(cmd_attach) { TRIGGER_DATA *trig = NULL; char *key = NULL; void *tgt = NULL; int found_type = PARSE_NONE; if(!parse_args(ch, TRUE, cmd, arg, "word [to] { ch.room obj.room.inv.eq room }", &key, &tgt, &found_type)) return; // check to make sure our key is OK if((trig = worldGetType(gameworld, "trigger", get_fullkey_relative(key, get_key_locale(roomGetClass(charGetRoom(ch)))))) == NULL) send_to_char(ch, "No trigger exists with the key, %s.\r\n", key); else { // what are we trying to attach it to? if(found_type == PARSE_CHAR) { send_to_char(ch, "Trigger %s attached to %s.\r\n", key, charGetName(tgt)); triggerListAdd(charGetTriggers(tgt), triggerGetKey(trig)); } else if(found_type == PARSE_ROOM) { send_to_char(ch, "Trigger %s attached to %s.\r\n", key, roomGetName(tgt)); triggerListAdd(roomGetTriggers(tgt), triggerGetKey(trig)); } else { send_to_char(ch, "Trigger %s attached to %s.\r\n", key, objGetName(tgt)); triggerListAdd(objGetTriggers(tgt), triggerGetKey(trig)); } } } // // detach a trigger from to the given instanced object/mobile/room COMMAND(cmd_detach) { TRIGGER_DATA *trig = NULL; char *key = NULL; void *tgt = NULL; int found_type = PARSE_NONE; if(!parse_args(ch, TRUE, cmd, arg, "word [from] { ch.room obj.room.inv.eq room }", &key, &tgt, &found_type)) return; // check to make sure our key is OK if((trig = worldGetType(gameworld, "trigger", get_fullkey_relative(key, get_key_locale(roomGetClass(charGetRoom(ch)))))) == NULL) send_to_char(ch, "which trigger did you want to detach?\r\n"); else { // what are we trying to detach the trigger from? if(found_type == PARSE_CHAR) { send_to_char(ch, "Trigger %s detached from %s.\r\n", key, charGetName(tgt)); triggerListRemove(charGetTriggers(tgt), triggerGetKey(trig)); } else if(found_type == PARSE_ROOM) { send_to_char(ch, "Trigger %s detached from %s.\r\n", key, roomGetName(tgt)); triggerListRemove(roomGetTriggers(tgt), triggerGetKey(trig)); } else { send_to_char(ch, "Trigger %s detached to %s.\r\n", key, objGetName(tgt)); triggerListRemove(objGetTriggers(tgt), triggerGetKey(trig)); } } } const char *triggerGetListType(TRIGGER_DATA *trigger) { static char buf[SMALL_BUFFER]; sprintf(buf, "%-40s %13s", triggerGetName(trigger), triggerGetType(trigger)); return buf; } // this is used for the header when printing out zone trigger info #define TRIGGER_LIST_HEADER \ "Name Type" COMMAND(cmd_tlist) { do_list(ch, (arg&&*arg?arg:get_key_locale(roomGetClass(charGetRoom(ch)))), "trigger", TRIGGER_LIST_HEADER, triggerGetListType); } COMMAND(cmd_tdelete) { do_delete(ch, "trigger", deleteTrigger, arg); } COMMAND(cmd_trename) { char from[SMALL_BUFFER]; arg = one_arg(arg, from); do_rename(ch, "trigger", from, arg); } //***************************************************************************** // implementation of scripts.h - triggers portion in triggers.c //***************************************************************************** void init_scripts(void) { // create our locale stack locale_stack = newList(); // initialize python Py_Initialize(); // initialize all of our modules written in C init_PyAuxiliary(); init_PyEvent(); init_PyStorage(); init_PyChar(); init_PyRoom(); init_PyExit(); init_PyObj(); init_PyMud(); // initialize all of our modules written in Python init_pyplugs(); // initialize the other parts to this module init_script_editor(); init_trighooks(); // so triggers can be saved to/loaded from disk worldAddType(gameworld, "trigger", triggerRead, triggerStore, deleteTrigger, triggerSetKey); // deal with auxiliary data auxiliariesInstall("trigger_data", newAuxiliaryFuncs(AUXILIARY_TYPE_CHAR | AUXILIARY_TYPE_OBJ| AUXILIARY_TYPE_ROOM, newTriggerAuxData, deleteTriggerAuxData, triggerAuxDataCopyTo, triggerAuxDataCopy, triggerAuxDataStore,triggerAuxDataRead)); // add in some hooks for preprocessing scripts embedded in descs hookAdd("preprocess_room_desc", expand_room_dynamic_descs); hookAdd("preprocess_char_desc", expand_char_dynamic_descs); hookAdd("preprocess_obj_desc", expand_obj_dynamic_descs); hookAdd("shutdown", finalize_scripts); /* // add new player commands add_cmd("trun", NULL, cmd_scrun, POS_UNCONCIOUS, POS_FLYING, "builder", FALSE, FALSE); */ extern COMMAND(cmd_tedit); // define the command add_cmd("attach", NULL, cmd_attach, POS_UNCONCIOUS, POS_FLYING, "scripter", FALSE, FALSE); add_cmd("detach", NULL, cmd_detach, POS_UNCONCIOUS, POS_FLYING, "scripter", FALSE, FALSE); add_cmd("tedit", NULL, cmd_tedit, POS_UNCONCIOUS, POS_FLYING, "scripter", FALSE, TRUE); add_cmd("tstat", NULL, cmd_tstat, POS_UNCONCIOUS, POS_FLYING, "scripter", FALSE, FALSE); add_cmd("tlist", NULL, cmd_tlist, POS_UNCONCIOUS, POS_FLYING, "scripter", FALSE, FALSE); add_cmd("tdelete", NULL, cmd_tdelete, POS_UNCONCIOUS, POS_FLYING, "scripter",FALSE, FALSE); add_cmd("trename", NULL, cmd_trename, POS_UNCONCIOUS, POS_FLYING, "scripter", FALSE, FALSE); } // // makes a dictionary with all of the neccessary stuff in it, but without // a __builtin__ module set PyObject *mud_script_dict(void) { PyObject* dict = PyDict_New(); // add the exit() function so people can terminate scripts PyObject *sys = PyImport_ImportModule("sys"); if(sys != NULL) { PyObject *exit = PyDict_GetItemString(PyModule_GetDict(sys), "exit"); if(exit != NULL) PyDict_SetItemString(dict, "exit", exit); Py_DECREF(sys); } // merge all of the mud module contents with our current dict PyObject *mudmod = PyImport_ImportModule("mud"); PyDict_Update(dict, PyModule_GetDict(mudmod)); Py_DECREF(mudmod); mudmod = PyImport_ImportModule("char"); PyDict_Update(dict, PyModule_GetDict(mudmod)); Py_DECREF(mudmod); mudmod = PyImport_ImportModule("room"); PyDict_Update(dict, PyModule_GetDict(mudmod)); Py_DECREF(mudmod); mudmod = PyImport_ImportModule("obj"); PyDict_Update(dict, PyModule_GetDict(mudmod)); Py_DECREF(mudmod); mudmod = PyImport_ImportModule("event"); PyDict_Update(dict, PyModule_GetDict(mudmod)); Py_DECREF(mudmod); mudmod = PyImport_ImportModule("random"); PyDict_SetItemString(dict, "random", mudmod); Py_DECREF(mudmod); return dict; } PyObject *restricted_script_dict(void) { // build up our basic dictionary PyObject *dict = mud_script_dict(); // add restricted builtin modules PyObject *builtins = PyImport_ImportModule("__restricted_builtin__"); if(builtins != NULL) { PyDict_SetItemString(dict, "__builtins__", builtins); Py_DECREF(builtins); } return dict; } PyObject *unrestricted_script_dict(void) { PyObject *dict = mud_script_dict(); // add builtins PyObject *builtins = PyImport_ImportModule("__builtin__"); if(builtins != NULL) { PyDict_SetItemString(dict, "__builtins__", builtins); Py_DECREF(builtins); } return dict; } void run_script(PyObject *dict, const char *script, const char *locale) { if(script_loop_depth >= MAX_LOOP_DEPTH) script_ok = FALSE; else { listPush(locale_stack, strdupsafe(locale)); script_loop_depth++; PyObject* compileRetval = PyRun_String(script, Py_file_input, dict, dict); script_loop_depth--; script_ok = TRUE; // we threw an error and it wasn't an intentional // system exit error. Now print the backtrace if(compileRetval == NULL && PyErr_Occurred() != PyExc_SystemExit) { char *tb = getPythonTraceback(); log_string("Script terminated with an error:\r\n%s\r\n" "\r\nTraceback is:\r\n%s\r\n", script, tb); free(tb); script_ok = FALSE; } Py_XDECREF(compileRetval); free(listPop(locale_stack)); } } const char *get_script_locale(void) { return listHead(locale_stack); } bool last_script_ok(void) { return script_ok; } void format_script_buffer(BUFFER *script) { bufferReplace(script, "\r", "", TRUE); } LIST *charGetTriggers(CHAR_DATA *ch) { TRIGGER_AUX_DATA *data = charGetAuxiliaryData(ch, "trigger_data"); return data->triggers; } LIST *objGetTriggers (OBJ_DATA *obj) { TRIGGER_AUX_DATA *data = objGetAuxiliaryData(obj, "trigger_data"); return data->triggers; } LIST *roomGetTriggers(ROOM_DATA *room) { TRIGGER_AUX_DATA *data = roomGetAuxiliaryData(room, "trigger_data"); return data->triggers; } void triggerListAdd(LIST *list, const char *trigger) { if(!listGetWith(list, trigger, strcasecmp)) listPut(list, strdup(trigger)); } void triggerListRemove(LIST *list, const char *trigger) { char *val = listRemoveWith(list, trigger, strcasecmp); if(val) free(val); } // // statements we need to highlight when showing a script const char *control_table[] = { "import", "return", "except", "while", "from", "elif", "else", "pass", "try", "def", "for", "if", "in", "is", "and", "or", "not", NULL }; // // returns which control string we found. returns -1 if none were found int check_for_control(const char *ptr, int i) { int syn_i; for(syn_i = 0; control_table[syn_i] != NULL; syn_i++) { int len = strlen(control_table[syn_i]); // not enough characters for it to exist if(i - len + 1 < 0) continue; // we found it might have found it. Check to make // sure that we are surrounded by spaces or colons if(!strncasecmp(ptr+i-len+1, control_table[syn_i], len)) { // check the left side first if(!(i - len < 0 || isspace(ptr[i-len]) || ptr[i-len] == ':')) continue; // and now the right side if(!(ptr+i+1 == '\0' || isspace(ptr[i+1]) || ptr[i+1] == ':')) continue; return syn_i; } } // didn't find any return -1; } void script_display(SOCKET_DATA *sock, const char *script, bool show_line_nums){ const char *ptr = script;//buffer_string(sock->text_editor); char line[SMALL_BUFFER] = "\0"; int line_num = 1; int line_i = 0, i = 0; bool in_line_comment = FALSE; // are we displaying a comment? bool in_digit = FALSE; // are we displaying a digit? bool in_string = FALSE; // how about a string? char string_type = '"'; // what kinda string marker is it? ' or " ? int syn_to_color = -1; // if we're coloring flow control, which one? for(i = 0; ptr[i] != '\0'; i++) { // take off the color for digits if(in_digit && !isdigit(ptr[i])) { sprintf(line+line_i, "{g"); line_i += 2; in_digit = FALSE; } // NO ELSE ... we might need to color something else // transfer over the character line[line_i] = ptr[i]; // if the character is a #, color the comment red if(ptr[i] == '#') { sprintf(line+line_i, "{r#"); line_i += 3; in_line_comment = TRUE; } // we've found a digit that we have to color in else if(isdigit(ptr[i]) && !in_digit && !in_line_comment && !in_string) { sprintf(line+line_i, "{y%c", ptr[i]); line_i += 3; in_digit = TRUE; } // if we've found a string marker, color/uncolor it else if((ptr[i] == '"' || ptr[i] == '\'') && !in_line_comment && // if we're already coloring a string and the marker // types don't match up, then don't worry about it !(in_string && string_type != ptr[i])) { if(in_string && ptr[i] == string_type) sprintf(line+line_i, "\%c{g", string_type); else sprintf(line+line_i, "{w%c", ptr[i]); line_i += 3; in_string = (in_string + 1) % 2; string_type = ptr[i]; } // we've hit a new line else if(ptr[i] == '\n') { // do we need to show line numbers char line_num_info[20]; if(show_line_nums) sprintf(line_num_info, "{c%2d] ", line_num); else *line_num_info = '\0'; line[line_i] = '\0'; send_to_socket(sock, "%s{g%s{n\r\n", line_num_info, line); *line = '\0'; line_i = 0; line_num++; in_line_comment = in_string = FALSE; // reset on newline } // checking while, for, if, else, elif, etc... // this is kinda tricky. We have to backtrack and check some stuff else if(!(in_line_comment || in_digit || in_string) && (syn_to_color = check_for_control(ptr, i)) != -1) { sprintf(line+line_i-strlen(control_table[syn_to_color])+1, "{p%s{g", control_table[syn_to_color]); line_i += 5; // the two markers for the color, and one for new character } // didn't find anything of interest else line_i++; } line[line_i] = '\0'; // send the last line if(*line) send_to_socket(sock, "{c%2d]{g %s{n\r\n", line_num, line); // and kill any color that is leaking // send_to_socket(sock, "{n"); // we don't end in a newline, but we have code if(line_num != 1 && ptr[strlen(ptr)-1] != '\n') send_to_socket(sock, "{RBuffer does not end in newline!{n\r\n"); }