/* $Header: /belch_a/users/rearl/tinymuck/src/RCS/game.c,v 1.15 90/09/28 12:21:03 rearl Exp $ */ /* * $Log: game.c,v $ * Revision 1.15 90/09/28 12:21:03 rearl * Added logging of interactive player commands. * * Revision 1.14 90/09/16 04:42:11 rearl * Preparation code added for disk-based MUCK. * * Revision 1.13 90/09/15 22:20:04 rearl * Fixed non-GOD_PRIV problem. * * Revision 1.12 90/09/13 06:22:31 rearl * Added optional argument to @dump for changing the dumpfile in emergencies. * * Revision 1.11 90/09/10 02:19:54 rearl * Fixed a possible bug in database checkpointing, minimized * command-line space-smashing. * * Revision 1.10 90/09/01 05:57:55 rearl * Fixed bugs in macro dumpfile. * * Revision 1.9 90/08/27 03:24:52 rearl * Disk-based MUF source code, added environment support code. * * Revision 1.8 90/08/11 04:00:20 rearl * *** empty log message *** * * Revision 1.7 90/08/05 14:43:34 rearl * One more silly bug fixed in the command handler. * * Revision 1.6 90/08/02 18:55:04 rearl * Fixed some calls to logging functions. * * Revision 1.5 90/07/30 00:11:07 rearl * Added @owned command. * * Revision 1.4 90/07/29 17:34:05 rearl * Fixed bug in = once and for all, also some bugs hiding in the command * processing switch. * * Revision 1.3 90/07/21 16:32:23 casie * more log calls added * * Revision 1.2 90/07/21 01:43:51 casie * added support for logging. * * Revision 1.1 90/07/19 23:03:33 casie * Initial revision * * */ #include "copyright.h" #include "config.h" #include <stdio.h> #include <ctype.h> #include <signal.h> #include <sys/wait.h> #include "db.h" #include "params.h" #include "interface.h" #include "match.h" #include "externs.h" #include "strings.h" #include "patchlevel.h" /* declarations */ static char *dumpfile = 0; static int epoch = 0; static int alarm_triggered = 0; static int alarm_block = 0; static void fork_and_dump(void); void dump_database(void); #include <sys/time.h> void do_dump(dbref player, const char *newfile) { char buf[BUFFER_LEN]; if(Wizard(player)) { alarm_triggered = 1; if (*newfile #ifdef GOD_PRIV && God(player) #endif /* GOD_PRIV */ ) { strcpy(dumpfile, newfile); sprintf(buf, "Dumping to file %s...", dumpfile); } else { sprintf(buf, "Dumping..."); } notify(player, buf); } else { notify(player, "Permission denied."); } } void do_shutdown(dbref player) { if(Wizard(player)) { log_status("SHUTDOWN: by %s\n", unparse_object(player, player)); shutdown_flag = 1; } else { notify(player, "Your delusions of grandeur have been duly noted."); log_status("ILLEGAL SHUTDOWN: tried by %s\n", unparse_object(player, player)); } } /* should be void, but it's defined as int */ static void alarm_handler(int x) { x = x; /* just so it dont complain */ alarm_triggered = 1; if(!alarm_block) { fork_and_dump(); } } static void dump_database_internal(void) { char tmpfile[2048]; FILE *f; sprintf(tmpfile, "%s.#%d#", dumpfile, epoch - 1); (void) unlink(tmpfile); /* nuke our predecessor */ sprintf(tmpfile, "%s.#%d#", dumpfile, epoch); if((f = fopen(tmpfile, "w")) != NULL) { db_write(f); fclose(f); if(rename(tmpfile, dumpfile) < 0) perror(tmpfile); } else { perror(tmpfile); } /* Write out the macros */ sprintf(tmpfile, "%s.#%d#", MACRO_FILE, epoch - 1); (void) unlink(tmpfile); sprintf(tmpfile, "%s.#%d#", MACRO_FILE, epoch); if((f = fopen(tmpfile, "w")) != NULL) { macrodump(macrotop, f); fclose(f); if (rename(tmpfile, MACRO_FILE) < 0) perror(tmpfile); } else { perror(tmpfile); } } void panic(const char *message) { char panicfile[2048]; FILE *f; int i; log_status("PANIC: %s\n", message); fprintf(stderr, "PANIC: %s\n", message); /* turn off signals */ for(i = 0; i < NSIG; i++) { signal(i, SIG_IGN); } /* shut down interface */ /* emergency_shutdown();*/ /* emergency_warn();*/ /* who cares... lets not tell'm. >:) */ /* dump panic file */ sprintf(panicfile, "%s.PANIC", dumpfile); if((f = fopen(panicfile, "w")) == NULL) { perror("CANNOT OPEN PANIC FILE, YOU LOSE"); #ifdef NOCOREDUMP #else /* !NOCOREDUMP */ signal(SIGIOT, SIG_DFL); #endif /* NOCOREDUMP */ } else { log_status("DUMPING: %s\n", panicfile); fprintf(stderr, "DUMPING: %s\n", panicfile); db_write(f); fclose(f); log_status("DUMPING: %s (done)\n", panicfile); fprintf(stderr, "DUMPING: %s (done)\n", panicfile); #ifdef NOCOREDUMP #else /* !NOCOREDUMP */ signal(SIGIOT, SIG_DFL); #endif /* NOCOREDUMP */ } execl("./restart", "restart", NULL, NULL); /* try and restart it... */ #ifdef NOCOREDUMP _exit(136); #else signal(SIGIOT,SIG_DFL); abort(); #endif } void dump_database(void) { epoch++; log_status("DUMPING: %s.#%d#\n", dumpfile, epoch); dump_database_internal(); log_status("DUMPING: %s.#%d# (done)\n", dumpfile, epoch); } static void fork_and_dump(void) { int child; epoch++; log_status("CHECKPOINTING: %s.#%d#\n", dumpfile, epoch); #ifdef USE_VFORK child = vfork(); #else /* USE_VFORK */ child = fork(); #endif /* USE_VFORK */ if(child == 0) { /* in the child */ close(0); /* get that file descriptor back */ dump_database_internal(); _exit(0); /* !!! */ } else if(child < 0) { perror("fork_and_dump: fork()"); } /* in the parent */ /* reset alarm */ alarm_triggered = 0; alarm(DUMP_INTERVAL); } dbref global_trigger; static void reaper(int x) { union wait stat; x = x; /* so it dont complain */ while(wait3(&stat, WNOHANG, 0) > 0); } time_t startup_time; int init_game(const char *infile, const char *outfile) { FILE *f; if ((f = fopen(MACRO_FILE, "r")) == NULL) log_status("INIT: Macro storage file %s is tweaked.\n", MACRO_FILE); else { macroload(f); fclose(f); } if((f = fopen((char *)infile, "r")) == NULL) return -1; /* ok, read it in */ log_status("LOADING: %s\n", infile); fprintf(stderr, "LOADING: %s\n", infile); if(db_read(f) < 0) return -1; log_status("LOADING: %s (done)\n", infile); fprintf(stderr, "LOADING: %s (done)\n", infile); /* everything ok */ fclose(f); /* initialize random number generator */ srandom(getpid()); /* set up dumper */ if(dumpfile) free((void *) dumpfile); dumpfile = alloc_string(outfile); signal(SIGALRM, (void *) alarm_handler); signal(SIGHUP, (void *) alarm_handler); signal(SIGCHLD, (void *) reaper); alarm_triggered = 0; alarm(DUMP_INTERVAL); /* set up uptime */ return 0; } /* use this only in process_command */ #define Matched(string) { if(!string_prefix((string), command)) goto bad; } void process_command(dbref player, const char *commie, dbref cause, int no_prog) { char *arg1; char *arg2; char *full_command; char *p; /* utility */ char *command; char commbuff[BUFFER_LEN]; char pbuf[BUFSIZ]; char xbuf[BUFSIZ]; { int a; for(a = 0; a < 10; a++) wptr[a] = NULL; } if (commie == 0) abort(); strcpy(commbuff, commie); command = commbuff; global_trigger = player; global_cause = cause; /* robustify player */ if(player < 0 || player >= db_top || ((Typeof(player) != TYPE_PLAYER)&&(Typeof(player)!=TYPE_THING))) { log_status("process_command: bad player %d\n", player); return; } if(Typeof(player) == TYPE_EXIT || Typeof(player) == TYPE_ROOM) player = OWNER(player); #ifdef LOG_COMMANDS if (!(FLAGS(player) & INTERACTIVE) || DBFETCH(player)->run) log_command("%s(%d) in %s(%d):%s %s\n", NAME(player), (int) player, NAME(DBFETCH(player)->location), (int) DBFETCH(player)->location, (FLAGS(player) & INTERACTIVE) ? " [interactive]" : " ", command); else log_command("%s(%d) in %s(%d):[editor] %s\n",NAME(player),(int) player, NAME(DBFETCH(player)->curr_prog), (int) DBFETCH(player)->curr_prog, command); #endif /* LOG_COMMANDS */ if(FLAGS(player)&PUPPET && Typeof(player) != TYPE_PLAYER) { sprintf(pbuf, "%s>> %s", NAME(player), command); notify_internal(DBFETCH(player)->owner, pbuf); } /* block dump to prevent db inconsistencies from showing up */ alarm_block = 1; if (FLAGS(player) & INTERACTIVE && !no_prog) { interactive(player, command); return; } /* eat leading whitespace */ while(*command && isspace(*command)) command++; /* check for single-character commands */ if(*command == SAY_TOKEN) { sprintf(pbuf,"say %s", command+1); command = &pbuf[0]; } else if((*command == POSE_TOKEN) || *command == ';') { sprintf(pbuf, "pose %s", command+1); command = &pbuf[0]; } else if(*command == '#') { sprintf(pbuf,"@force %s", command); command = &pbuf[0]; } if(can_move(player, command)) { /* command is an exact match for an exit */ do_move(player, command); *match_args = 0; } else { full_command = strcpy(xbuf, command); for (; *full_command && !isspace(*full_command); full_command++); if (*full_command) full_command++; /* find arg1 -- move over command word */ for(arg1 = command; *arg1 && !isspace(*arg1); arg1++); /* truncate command */ if(*arg1) *arg1++ = '\0'; /* move over spaces */ while(*arg1 && isspace(*arg1)) arg1++; /* find end of arg1, start of arg2 */ for(arg2 = arg1; *arg2 && *arg2 != ARG_DELIMITER; arg2++); /* truncate arg1 */ for(p = arg2 - 1; p >= arg1 && isspace(*p); p--) *p = '\0'; /* go past delimiter if present */ if(*arg2) *arg2++ = '\0'; while(*arg2 && isspace(*arg2)) arg2++; arg1 = check_arg(arg1, player, cause); arg2 = check_arg(arg2, player, cause); switch(command[0]) { case '@': switch(command[1]) { case '@': Matched("@@"); /* does nothing, useful for commenting */ break; case 'a': case 'A': /* @action, @attach */ switch(command[2]) { case 'a': case 'A': Matched("@aahear"); do_attr(player,"Aahear",arg1,arg2); break; case 'c': case 'C': switch(command[3]) { case 'o': case 'O': Matched("@aconnect"); do_attr(player, "Aconnect", arg1, arg2); break; case 't': case 'T': Matched("@action"); do_action(player, arg1, arg2); break; default: goto bad; } break; case 'd': case 'D': switch(command[3]) { case 'e': case 'E': Matched("@adesc"); do_attr(player, "Adesc", arg1, arg2); break; case 'r': case 'R': Matched("@adrop"); do_attr(player,"Adrop", arg1, arg2); break; default: goto bad; } break; case 'E': case 'e': Matched("@aenter"); do_attr(player, "Aenter", arg1, arg2); break; case 'f': case 'F': Matched("@afail"); do_attr(player, "Afail", arg1, arg2); break; case 'h': case 'H': Matched("@ahear"); do_attr(player, "Ahear", arg1, arg2); break; case 'k': case 'K': Matched("@akill"); do_attr(player, "Akill", arg1, arg2); break; case 'l': case 'L': Matched("@aleave"); do_attr(player, "Aleave", arg1, arg2); break; case 'm': case 'M': Matched("@amhear"); do_attr(player, "Amhear", arg1, arg2); break; case 'p': case 'P': Matched("@apay"); do_attr(player, "Apay", arg1, arg2); break; case 's': case 'S': Matched("@asucc"); do_attr(player, "Asucc", arg1, arg2); break; case 't': case 'T': Matched("@attach"); do_attach(player, arg1, arg2); break; default: goto bad; } break; case 'b': case 'B': Matched("@boot"); do_boot(player, arg1); break; case 'c': case 'C': /* chown, create */ switch(command[2]) { case 'h': case 'H': Matched("@chown"); do_chown(player, arg1, arg2); break; case 'o': case 'O': Matched("@cost"); do_attr(player, "Cost", arg1, arg2); break; case 'r': case 'R': Matched("@create"); do_create(player, arg1, atoi(arg2)); break; default: goto bad; } break; case 'd': case 'D': /* describe, dig, or dump */ switch(command[2]) { case 'b': case 'B': do_dboot(player, arg1); break; case 'e': case 'E': Matched("@describe"); do_attr(player, "Desc", arg1, arg2); break; case 'i': case 'I': Matched("@dig"); do_dig(player, arg1, arg2); break; case 'r': case 'R': Matched("@drop"); do_attr(player, "Drop", arg1, arg2); break; case 'u': case 'U': Matched("@dump"); do_dump(player, full_command); break; default: goto bad; } break; case 'e': case 'E': switch(command[2]) { case 'd': case 'D': Matched("@edit"); do_edit(player, arg1); break; case 'n': case 'N': Matched("@enter"); do_attr(player, "Enter", arg1, arg2); break; default: goto bad; } break; case 'f': case 'F': /* fail, field, find, or force */ switch(command[2]) { case 'a': case 'A': Matched("@fail"); do_attr(player, "Fail", arg1, arg2); break; case 'i': case 'I': switch(command[3]) { case 'e': case 'E': Matched("@field"); do_field(player, arg1, arg2, cause); break; case 'n': case 'N': Matched("@find"); do_find(player, arg1, arg2); break; default: goto bad; } break; case 'o': case 'O': Matched("@force"); do_force(player, arg1, arg2); break; default: goto bad; } break; case 'h': case 'H': Matched("@halt"); do_halt(player, arg1); break; case 'i': case 'I': Matched("@idesc"); do_attr(player, "Idesc", arg1, arg2); break; case 'k': case 'K': Matched("@kill"); do_attr(player, "Kill", arg1, arg2); break; case 'l': case 'L': /* lock or link */ switch(command[2]) { case 'e': case 'E': Matched("@leave"); do_attr(player, "Leave", arg1, arg2); break; case 'i': case 'I': switch(command[3]) { case 'n': case 'N': Matched("@link"); do_link(player, arg1, arg2); break; case 's': case 'S': if(!string_compare(command,"@list")) { Matched("@list"); match_and_list(player, arg1, arg2); } else { Matched("@listen"); do_attr(player, "Listen", arg1, arg2); } break; default: goto bad; } break; case 'o': case 'O': Matched("@lock"); do_lock(player, arg1, arg2); break; default: goto bad; } break; case 'n': case 'N': /* @name or @newpassword */ switch(command[2]) { case 'a': case 'A': Matched("@name"); do_name(player, arg1, arg2); break; case 'e': case 'E': if(strcmp(command, "@newpassword")) goto bad; do_newpassword(player, arg1, arg2); break; default: goto bad; } break; case 'o': case 'O': switch(command[2]) { case 'c': case 'C': Matched("@oconnect"); do_attr(player, "Oconnect", arg1, arg2); break; case 'd': case 'D': switch(command[3]) { case 'e': case 'E': Matched("@odesc"); do_attr(player, "Odesc", arg1, arg2); break; case 'i': case 'I': Matched("@odisconnect"); do_attr(player, "Odisconnect", arg1, arg2); break; case 'r': case 'R': Matched("@odrop"); do_attr(player, "Odrop", arg1, arg2); break; default: goto bad; } break; case 'e': case 'E': Matched("@oenter"); do_attr(player, "Oenter", arg1, arg2); break; case 'f': case 'F': Matched("@ofail"); do_attr(player, "Ofail", arg1, arg2); break; case 'k': case 'K': Matched("@okill"); do_attr(player, "Okill", arg1, arg2); break; case 'l': case 'L': Matched("@oleave"); do_attr(player, "Oleave", arg1, arg2); break; case 'p': case 'P': switch(command[3]) { case 'e': case 'E': Matched("@open"); do_open(player, arg1, arg2); break; case 'a': case 'A': Matched("@opay"); do_attr(player, "Opay", arg1, arg2); break; default: goto bad; } break; case 's': case 'S': Matched("@osuccess"); do_attr(player, "Osucc", arg1, arg2); break; case 'w': case 'W': Matched("@owned"); do_owned(player, arg1, arg2); break; default: goto bad; } break; case 'p': case 'P': switch(command[2]) { case 'a': case 'A': switch(command[3]) { case 's': case 'S': Matched("@password"); do_password(player, arg1, arg2); break; case 'y': Matched("@pay"); do_attr(player, "Pay", arg1, arg2); break; default: goto bad; } break; case 'c': case 'C': Matched("@pcreate"); do_pcreate(player, arg1, arg2); break; case 'r': case 'R': Matched("@prog"); do_prog(player, arg1); break; case 's': case 'S': Matched("@ps"); do_ps(player, arg1); break; default: goto bad; } break; case 'r': case 'R': switch(command[2]) { #ifdef RECYCLE case 'e': case 'E': Matched("@recycle"); do_recycle(player, arg1); break; #endif /* RECYCLE */ case 's': case 'S': if(strcmp(command, "@rst")) goto bad; do_restart(player); break; default: goto bad; } break; case 's': case 'S': /* set, shutdown, success */ switch(command[2]) { case 'e': case 'E': switch(command[3]) { case 't': case 'T': Matched("@set"); do_set(player, arg1, arg2); break; case 'x': case 'X': Matched("@sex"); do_attr(player, "Sex", arg1, arg2); break; default: goto bad; } break; case 'h': case 'H': if(strcmp(command, "@shutdown")) goto bad; do_shutdown(player); break; case 't': case 'T': switch(command[3]) { case 'a': case 'A': switch(command[4]) { case 't': case 'T': Matched("@stats"); do_stats(player, arg1); break; case 'r': case 'R': Matched("@startup"); do_attr(player, "Startup", arg1, arg2); break; default: goto bad; } break; default: goto bad; } break; case 'u': case 'U': Matched("@success"); do_attr(player, "Succ", arg1, arg2); break; case 'w': case 'W': switch(command[3]) { case 'a': case 'A': Matched("@swap"); do_swap(player, arg1, arg2); break; case 'i': case 'I': Matched("@switch"); do_switch(player, arg1, arg2, cause); break; default: goto bad; } break; default: goto bad; } break; case 't': case 'T': switch(command[2]) { case 'e': case 'E': Matched("@teleport"); do_teleport(player, arg1, arg2); break; case 'o': case 'O': if(strcmp(command, "@toad")) goto bad; do_toad(player, arg1, arg2); break; case 'r': case 'R': switch(command[3]) { case 'a': case 'A': Matched("@trace"); do_trace(player, arg1, atoi(arg2)); break; case 'i': case 'I': Matched("@trigger"); do_trigger(player, arg1, arg2); break; default: goto bad; } break; default: goto bad; } break; case 'u': case 'U': switch(command[2]) { case 'n': case 'N': if(string_prefix(command, "@unli")) { do_unlink(player, arg1); } else if(string_prefix(command, "@unlo")) { do_unlock(player, arg1); } else { goto bad; } break; case 'p': case 'P': Matched("@uptime"); do_uptime(player); break; default: goto bad; } break; case 'v': case 'V': /* v? fields */ if(command[3]) { Matched("@version"); do_version(player); } else { char buf[3]; if(tolower(command[2])<'a' || tolower(command[2])>'z') goto bad; strcpy(buf,"V?"); buf[1] = tolower(command[2]); do_attr(player, buf, arg1, arg2); } break; case 'w': case 'W': if(!strcmp(command, "@wall")) do_wall(player, full_command); else { Matched("@wait"); do_wait(player,arg1,arg2); } break; default: goto bad; } break; case 'd': case 'D': Matched("drop"); do_drop(player, arg1); break; case 'e': case 'E': switch(command[1]) { case 'x': case 'X': Matched("examine"); do_examine(player, arg1); break; case 'n': case 'N': Matched("enter"); do_enter(player,arg1); break; default: goto bad; } break; case 'g': case 'G': /* get, give, go, or gripe */ switch(command[1]) { case 'e': case 'E': Matched("get"); do_get(player, arg1); break; case 'i': case 'I': Matched("give"); do_give(player, arg1, atoi(arg2)); break; case 'o': case 'O': Matched("goto"); do_move(player, arg1); break; case 'r': case 'R': Matched("gripe"); do_gripe(player, full_command); break; default: goto bad; } break; case 'h': case 'H': Matched("help"); do_help(player); break; case 'i': case 'I': Matched("inventory"); do_inventory(player); break; case 'k': case 'K': Matched("kill"); do_kill(player, arg1, atoi(arg2)); break; case 'l': case 'L': switch(command[1]) { case 'o': case 'O': case '\0': /* so just 'l' is look */ Matched("look"); do_look_at(player, arg1); break; case 'e': case 'E': Matched("leave"); do_leave(player); break; default: goto bad; } break; case 'm': case 'M': if (string_prefix(command, "move")) { do_move(player, arg1); break; } else { if (string_compare(command, "man")) goto bad; do_man(player); } break; case 'n': case 'N': /* news */ if(string_compare(command, "news")) goto bad; do_news(player); break; case 'p': case 'P': switch(command[1]) { case 'a': case 'A': Matched("page"); do_page(player, arg1, arg2); break; case 'o': case 'O': Matched("pose"); do_pose(player, full_command); break; case 'u': case 'U': Matched("put"); do_drop(player, arg1); break; default: goto bad; } break; case 'r': case 'R': Matched("read"); /* undocumented alias for look at */ do_look_at(player, arg1); break; case 's': case 'S': /* say, "score" */ switch(command[1]) { case 'a': case 'A': Matched("say"); do_say(player, full_command); break; case 'c': case 'C': Matched("score"); do_score(player); break; default: goto bad; } break; case 't': case 'T': switch(command[1]) { case 'a': case 'A': Matched("take"); do_get(player, arg1); break; case 'h': case 'H': Matched("throw"); do_drop(player, arg1); break; default: goto bad; } break; case 'w': case 'W': Matched("whisper"); do_whisper(player, arg1, arg2); break; default: bad: { char bebuf[BUFFER_LEN]; if(*full_command) sprintf(bebuf, "%s %s", command, full_command); else sprintf(bebuf, "%s", command); if(!check_mushact(player, bebuf)) { notify(player, "Huh? (Type \"help\" for help.)"); #ifdef LOG_FAILED_COMMANDS if(!controls(player, DBFETCH(player)->location)) { log_status("HUH from %s(%d) in %s(%d)[%s]: %s %s\n", NAME(player), player, NAME(DBFETCH(player)->location), DBFETCH(player)->location, NAME(OWNER(DBFETCH(player)->location)), command, full_command); } #endif /* LOG_FAILED_COMMANDS */ } } break; } if(arg1) free(arg1); if(arg2) free(arg2); /* check_arg */ } /* unblock alarms */ alarm_block = 0; if(alarm_triggered) { fork_and_dump(); } } #undef Matched int do_act(dbref what, const char *command, dbref player) { int res = 0, loog; const char *y; static char x[4]= "v_\0"; if(Halted(what)) return 0; for(x[1] = 'a'; x[1] <= 'z' && !res; x[1]++) { if(y = get_attr(what, x)) { /* We can try this. */ if(*y == '$' && index(y, ':')) { /* yay. we really can try this. */ static char yy[BUFFER_LEN]; strcpy(yy, y + 1); *index(yy, ':') ='\0'; if(wild_match(yy, command)) { res++; trigobj(what, 1 + index(yy, '\0'), player); } } } } for(loog = 0; loog < 10; loog++) wptr[loog] = NULL; return res; } int check_mushact(dbref player, const char *command) { dbref first; int res = 0; puts("player contents"); for(first = DBFETCH(player)->contents; first != NOTHING; first = DBFETCH(first)->next) if(Typeof(first) == TYPE_THING) res += do_act(first, command, player); puts("location contents"); if(DBFETCH(player)->location != NOTHING) for(first = DBFETCH(DBFETCH(player)->location)->contents; first != NOTHING; first = DBFETCH(first)->next) if(Typeof(first) == TYPE_THING) res += do_act(first, command, player); puts("Done."); return res; } dbref global_cause; int do_restart(dbref who) { if(!Wizard(who)) { notify(who, "Permission denied."); return 0; } kill(getpid(), 15); /* cause automatic restart. */ return 0; } int do_uptime(dbref who) { int x = time(NULL) - startup_time; sprintf(buf, "up %d days, %02d:%02d:%02d", x/(60*60*24), (x/(60*60))%24, (x/60)%60,x%60); notify(who, buf); return 0; } int do_version(dbref who) { sprintf(buf, "Version: %s", VERSION_STRING); notify(who, buf); return 0; }