Generic user

    This object determines the behavior of user.

    Informational public methods:

	doing()				Get doing string for who list
	connected_at()			Return connection time
	last_command_at()		Return time of last input
	check_password(str)		True if str crypts to password
	match_environment(s)		Return nearby object matching s
	local_to_environment(obj)	True if obj is nearby

    Public methods to perform actions:

	tell(s)				Write string to user
	invalidate_verb_cache()		Remove verb cache

    Owner methods:

	set_name(s)			Set name
	set_doing(s)			Set doing string
	set_password(str)		Set password to crypt(str)
	parse_command(str)		Execute input line str

    Protected methods:

	did_move(obj, place)		Notification of completed move

    Private methods:

	validate_verb_cache()		Build verb cache if necessary

    System object methods:

	login()				Perform login actions

    Server methods:

	disconnect()			Indicates disconnection
	parse(s)			Indicates input line

    Commands:

	who_cmd("@who")			Display a who listing
	doing_cmd("@doing", s)		Set doing message
	quit_cmd("@quit")		Log out
	help_on_cmd("help", topic, "on", obj)
					Get help on a topic on an object
	help_cmd("help", str)		Get help

parent person
parent commands
object user

var user password 0
var user verb_cache 0
var user doing 0
var user connected_at 0
var user last_command_at 0
var user editor 0
var user editor_method 0
var user connections 0

method init
    arg ancestors;

    (> pass(ancestors) <);
    if (definer() in ancestors) {
	password = "*";
	verb_cache = 0;
	doing = "";
	connected_at = 0;
	last_command_at = 0;
	editor = 0;
	editor_method = 0;
        connections = [];
	$sys.register_new_user_name();
    }
.

method set_name
    arg s;
    var old_name;

    old_name = .name();
    if (s == old_name)
	return;
    if (" " in s)
	throw(~space, "Space in new name.");
    if ((| $sys.find_user(s) |))
	throw(~duplicate, "Duplicate user name.");
    (> pass(s) <);
    $sys.user_changed_name(old_name);
.

eval
    var err;

    .initialize();
    .set_name("JoeAverage");
    .add_command("@who", 'template, 'who_cmd);
    .add_command("@doing *", 'template, 'doing_cmd);
    .add_command("@quit", 'template, 'quit_cmd);
    .add_command("i?nventory", 'template, 'inventory_cmd);
    .add_command("p?age * with *", 'template, 'page_cmd);
    .add_command("sample_edit *", 'template, 'sample_edit_cmd);
.

method doing
    return doing;
.

method set_doing
    arg s;

    if (!.is_owned_by(sender()))
	throw(~perm, "Sender not an owner.");
    if (type(s) != 'string)
	throw(~type, "Argument not a string.");
    doing = s;
.

method connected_at
    return connected_at;
.

method last_command_at
    return last_command_at;
.

method verb_changed_inside
    .invalidate_verb_cache();
.

method tell
    arg what;
    var line, conn;

    if (type(what) == 'list) {
        for line in (what)
            .tell(line);
    } else {
        for conn in (connections)
            conn.tell(what);
    }
.

method set_password
    arg str;

    if (!.is_owned_by(sender()))
	throw(~perm, "Sender not an owner.");
    password = crypt(str);
.

method check_password
    arg str;

    return crypt(str, substr(password, 1, 2)) == password;
.

method validate_verb_cache
    var loc, objects, all_objects, obj, anc, templates;

    if (sender() != this() || caller() != definer())
	throw(~perm, "Sender not this.");
    if (verb_cache != 0)
	return;

    // Get a list of objects to look for verbs on.
    loc = .location();
    objects = [this()] + [loc] + .contents();
    objects = objects + setremove(loc.contents(), this());

    // Now expand that list to include ancestors.
    all_objects = [];
    for obj in (objects) {
	all_objects = setadd(all_objects, obj);
	all_objects = union(all_objects, obj.ancestors());
    }

    // Now, for each object in that list, add all its templates to the verb
    // cache.
    verb_cache = [];
    for obj in (all_objects) {
	templates = (| obj.verb_templates() |);
	if (templates)
	    verb_cache = union(verb_cache, templates);
    }
.

method invalidate_verb_cache
    verb_cache = 0;
.

method did_move
    arg [args];
    var loc;

    (> pass(@args) <);
    .invalidate_verb_cache();
    loc = .location();
    (| loc.look_cmd("look") |);
.

method parse
    arg s;

    if (!caller().is_agent('connection))
	throw(~perm, "Sender is not an agent of connection protocol.");

    // Don't let regular users see stack traces.
    catch any {
	last_command_at = time();
	while (s && s[1] == " ")
	    s = substr(s, 2);
	if (!s)
	    return;
	return .parse_command(s);
    } with handler {
	.tell("Internal error processing command: " + s);
    }
.

method parse_command
    arg str;
    var cmd, loc, result, i, j, template, word, fields, obj, verb_info;

    if (!.is_owned_by(sender()))
	throw(~perm, "Sender not an owner.");

    // Are we editing?
    if (editor) {
	if (substr(str, 1, 1) == "<") {
	    str = substr(str, 2);
	    if (!str)
		return;
	} else {
	    result = editor.handle_editor_command(str);
	    switch (result[1]) {
		case 'not_done:
		    editor = result[2];
		case 'abort:
		    editor = 0;
		case 'done:
		    .(editor_method)(result[2]);
	    }
	    return;
	}
    }

    // Check commands on this.
    cmd = .match_command(str);
    if (cmd)
	return .(cmd[1])(@cmd[2]);

    // Check commands on location.
    loc = .location();
    cmd = loc.match_command(str);
    if (cmd) {
	loc.(cmd[1])(@cmd[2]);
	return;
    }

    // Resort to verb cache.
    .validate_verb_cache();
    catch ~objnf, ~verbnf {
	for template in (union(verb_cache, $sys.remote_verb_templates())) {
	    fields = match_template(strsub(template, "%this", "*"), str);
	    if (type(fields) != 'list)
		continue;
	    j = 1;
	    for word in (explode(template)) {
		if (word == "%this") {
		    obj = sender().match_environment(fields[j]);
		    verb_info = obj.verb_info(template);
		    if (verb_info[2] != 'remote && !.local_to_environment(obj))
			.tell("You cannot do that from here.");
		    else
			obj.(verb_info[1])(@fields);
		    return;
		} else if (word == "*=*") {
		    j = j + 2;
		} else {
		    j = j + 1;
		}
	    }
	}
    } with handler {
	switch (error()) {
	    case ~objnf:
		.tell("I don't see \"" + error_arg() + "\" here.");
	    case ~verbnf:
		.tell("You can't do that to that object.");
	}
	return;
    }

    // Try exit names.
    for obj in (loc.exits()) {
	if (str == obj.name()) {
	    obj.go_through();
	    return;
	}
    }

    // No luck.
    .tell("I don't understand that.");
.

method connection_logged_in
    arg addr, port;

    if (!caller().is_agent('connection))
        throw(~perm, "Caller is not an agent of connection protocol.");
    connections = connections + [sender()];
    if (listlen(connections) == 1)
        .login(sender());
    else
        .login_again(sender());
.

method connection_gone
    arg addr, port;

    if (!caller().is_agent('connection))
        throw(~perm, "Caller is not an agent of connection protocol.");
    connections = setremove(connections, sender());
    if (!connections)
        .logout(sender());
    connections = setremove(connections, sender());
.

method login
    arg connection;
    var loc;

    if (sender() != this() || definer() != caller())
	throw(~perm, "Invalid access to private method.");
    $sys.user_logged_in();
    .tell("* * * Connected * * *");
    connected_at = time();
    last_command_at = time();
    loc = .location();
    (| loc.did_connect() |);
    (| loc.look_cmd("look") |);
.

method login_again
    arg connection;

    if (sender() != this() || definer() != caller())
	throw(~perm, "Invalid access to private method.");
    last_command_at = time();
    connection.tell("* * * Already connected * * *");
.

method logout
    arg connection;

    if (sender() != this() || definer() != caller())
	throw(~perm, "Invalid access to private method.");
    $sys.user_logged_out();
    .invalidate_verb_cache();
    (| loc.did_disconnect() |);
.

method match_environment
    arg s;
    var loc, objects, obj;

    loc = .location();

    // Handle special cases.
    if (s == "me")
	return this();
    if (s == "here")
	return loc;
    if (s && s[1] == "$") {
	obj = todbref(substr(s, 2));
	if (!valid(obj))
	    throw(~objnf, "No such object " + s, s);
	return obj;
    }

    objects = [this()] + [loc] + .contents();
    objects = objects + setremove(loc.contents(), this());
    objects = objects + loc.exits();

    // Look first for exact matches.
    for obj in (objects) {
	if (obj.name() == s)
	    return obj;
    }

    // Now look for partial matches.
    for obj in (objects) {
	if (match_begin(obj.name(), s))
	    return obj;
    }

    throw(~objnf, "No object " + s + " in environment.", s);
.

method local_to_environment
    arg obj;
    var loc;

    if (obj == this() || obj in .contents())
	return 1;
    loc = .location();
    if (obj == loc || obj in loc.contents() || obj in loc.exits())
	return 1;
    return 0;
.

method who_cmd
    arg dummy;
    var user, seconds, namestr, constr, idlestr, n, doing;

    if (sender() != this())
	throw(~perm, "Sender not this.");
    .tell("User Name          On For Idle  " + $sys.doing_poll());
    for user in ($sys.connected_users()) {
	namestr = pad(user.name(), 14) + "  ";

	// Put together the string for connect time.
	seconds = time() - user.connected_at();
	if (seconds > 86400) {
	    constr = tostr(seconds / 86400);
	    constr = constr + "d";
	} else {
	    constr = "";
	}
	constr = pad(constr, -3);
	seconds = seconds % 86400;
	constr = constr + " " + pad(tostr(seconds / 3600), -2, "0");
	constr = constr + ":" + pad(tostr((seconds % 3600) / 60), -2, "0");

	// Put together the string for idle time.
	seconds = time() - user.last_command_at();
	if (seconds > 86400)
	    idlestr = tostr(seconds / 86400) + "d";
	else if (seconds > 3600)
	    idlestr = tostr(seconds / 3600) + "h";
	else if (seconds > 60)
	    idlestr = tostr(seconds / 60) + "m";
	else
	    idlestr = tostr(seconds) + "s";
	idlestr = pad(idlestr, -3) + "  ";

	// Get doing and truncate if neccessary.
	doing = user.doing();
	if (strlen(doing) > 46)
	    doing = substr(doing, 1, 46);

	// Display the resulting line.
	.tell(namestr + constr + "  " + idlestr + doing);
    }
    n = listlen($sys.connected_users());
    if (n == 1)
       .tell("One user logged in.");
    else
       .tell(tostr(n) + " users logged in.");
.

method doing_cmd
    arg dummy, s;

    if (sender() != this())
	throw(~perm, "Sender not this.");
    .set_doing(s);
    .tell("Set.");
.

method quit_cmd
    arg dummy;

    if (sender() != this())
        throw(~perm, "Sender not this.");
    return 'disconnect;
.

method inventory_cmd
    arg dummy;
    var i;

    if (sender() != this())
	throw(~perm, "Sender not this.");
    .tell("Carrying:");
    for i in (.contents())
	.tell(" " + i.name());
.

method page_cmd
    arg dummy1, recipient, dummy2, message;
    var user;

    if (sender() != this())
	throw(~perm, "Sender not this.");
    catch ~usernf {
	user = $sys.find_user(recipient);
	if (!(user in $sys.connected_users())) {
	    .tell(user.name() + " is not connected.");
	    return;
	}
	user.tell(.name() + " pages: " + message);
	.tell("You page \"" + message + "\" to " + user.name() + ".");
    } with handler {
	.tell(recipient + " is not the name of a user.");
    }
.

method sample_edit_cmd
    arg dummy, str;

    editor = $editor_class.new([str]);
    editor_method = 'sample_edit_done;
.

method sample_edit_done
    arg text;

    .tell("Sample edit finished:");
    .tell(text);
.