nakedmudv3.0/
nakedmudv3.0/lib/
nakedmudv3.0/lib/logs/
nakedmudv3.0/lib/misc/
nakedmudv3.0/lib/players/
nakedmudv3.0/lib/pymodules/
nakedmudv3.0/lib/txt/
nakedmudv3.0/lib/world/
nakedmudv3.0/lib/world/examples/
nakedmudv3.0/lib/world/examples/mproto/
nakedmudv3.0/lib/world/examples/oproto/
nakedmudv3.0/lib/world/examples/reset/
nakedmudv3.0/lib/world/examples/rproto/
nakedmudv3.0/lib/world/examples/trigger/
nakedmudv3.0/lib/world/limbo/
nakedmudv3.0/lib/world/limbo/rproto/
nakedmudv3.0/src/alias/
nakedmudv3.0/src/char_vars/
nakedmudv3.0/src/editor/
nakedmudv3.0/src/example_module/
nakedmudv3.0/src/help/
nakedmudv3.0/src/set_val/
nakedmudv3.0/src/socials/
nakedmudv3.0/src/time/
//*****************************************************************************
//
// 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");
}