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
	connections()			List of connection objects

    Public methods to perform actions:

        tell(s)                         Write string to user

    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

    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 proto_user
object user

var root name 'user
var user password 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_user
    if (caller() != $root)
        throw(~perm, "Caller is not $root.");
    password = "*";
    doing = "";
    connected_at = 0;
    last_command_at = 0;
    editor = 0;
    editor_method = 0;
    connections = [];
    $user_db.user_created();
.

method uninit_user
    var conn;

    if (caller() != $root)
        throw(~perm, "Caller is not $root.");
    for conn in (connections)
        (| conn.user_going_away() |);
    password = 0;
    doing = 0;
    editor = 0;
    editor_method = 0;
    connections = 0;
    $user_db.user_going_away();
.

method set_vr_name
    arg new_name;
    var old_name;

    if (!.is_owned_by(sender()))
        throw(~perm, "Sender is not an owner.");
    if (" " in new_name)
        throw(~spaces, "Space in new name.");
    if ((| $user_db.find_user(new_name) |))
        throw(~duplicate, "Duplicate user name.");
    old_name = .vr_name();
    (> pass(new_name) <);
    $user_db.user_changed_name(old_name);
.

eval
    var err;

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

method spawn
    arg [args];

    if (!$sys.is_admin(sender()))
        throw(~perm, "Sender is not an admin.");
    return (> pass(@args) <);
.

method will_move
    arg mover, place;

    (> pass(mover, place) <);
    if (!place.has_ancestor($room))
        throw(~perm, "Players can only move into rooms.");
.

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 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 did_move
    arg [args];
    var loc;

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

method parse_line
    arg s;

    if (caller() != $connection)
        throw(~perm, "Caller is not $connection.");

    // 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, templates, 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;
    }

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

    // Resort to verb cache.
    templates = $verb_cache.templates();
    catch ~objnf, ~verbnf {
        for template in (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;
    }

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

method connection_logged_in
    arg addr, port;

    if (caller() != $connection)
        throw(~perm, "Caller is not $connection.");
    connections = connections + [sender()];
    if (listlen(connections) == 1)
        .login(sender());
    else
        .login_again(sender());
.

method connection_gone
    arg addr, port;

    if (caller() != $connection)
        throw(~perm, "Caller is not $connection.");
    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.");
    $user_db.user_connected();
    .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.");
    $user_db.user_disconnected();
    (| .location().did_disconnect() |);
.

method connections
    return connections;
.

method connected
    disallow_overrides;

    return connections ? 1 | 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 ($user_db.connected_users()) {
        namestr = pad(user.vr_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($user_db.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.vr_name());
.

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

    if (sender() != this())
        throw(~perm, "Sender not this.");
    catch ~usernf {
        user = $user_db.find_user(recipient);
        if (!(user in $user_db.connected_users())) {
            .tell(user.vr_name() + " is not connected.");
            return;
        }
        user.tell(.vr_name() + " pages: " + message);
        .tell("You page \"" + message + "\" to " + user.vr_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);
.