# Copyright (C) 2011-2021 A S Lewis
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU
# General Public License as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this program. If not,
# see <http://www.gnu.org/licenses/>.
#
#
# Games::Axmud::Cmd::XXX
# All standard Axmud command packages (except ';test', which can be found in cmds_test.pm)

# Debug commands

{ package Games::Axmud::Cmd::Test;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('test', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['test'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'General debugging command';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $string,
            @list,
            %hash,
        );

        # (No improper arguments to check)

        # Add any code you like here, in order to test something
        # Use the client command ';test' to run the code

        # ...

        #   Don't interfere with this line; when there is no code above (or it's all been commented
        #       out), it provides a default response to the user's ';test' command
        return $self->complete($session, $standardCmd, '\'test\' command complete');
    }
}

{ package Games::Axmud::Cmd::HelpTest;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('helptest', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['hpt', 'testhelp', 'helptest'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Tests ' . $axmud::SCRIPT . ' help files';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $cmdCount, $keywordCount, $funcCount, $taskCount, $errorCount, $limit, $lastLine,
            $scriptObj, $file, $fileHandle,
            @sortedList, @aboutList, @quickList, @peekList,
            %cmdHash, %fileHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Help files lines should not be longer than this value
        $limit = $axmud::CLIENT->constHelpCharLimit;
        $cmdCount = 0;
        $keywordCount = 0;
        $funcCount = 0;
        $taskCount = 0;
        $errorCount = 0;

        $session->writeText('Starting ' . $axmud::SCRIPT . ' help test');

        # Test client command help files
        $session->writeText('Testing client commands...');

        # Check for missing help files, and help files that exist, but shouldn't
        foreach my $cmd (sort {lc($a) cmp lc($b)} ($axmud::CLIENT->clientCmdList)) {

            if (! $self->helpExists($cmd)) {

                $session->writeText('   Missing help file for \'' . $cmd . '\' command');
                $errorCount++;

            } else {

                $cmdHash{$cmd} = undef;
            }
        }

        foreach my $cmd (sort {lc($a) cmp lc($b)} ($self->listHelpFiles())) {

            if (exists $fileHash{$cmd}) {

                $session->writeText('   Multiple help files for \'' . $cmd . '\' command');
                $errorCount++;

            } else {

                $fileHash{$cmd} = undef;

                if (! exists $cmdHash{$cmd}) {

                    $session->writeText(
                        '   Command \'' . $cmd . '\' doesn\'t exist, but a help file for it does',
                    );

                    $errorCount++;

                } else {

                    $fileHash{$cmd} = undef;
                }
            }
        }

        OUTER: foreach my $cmd (sort {lc($a) cmp lc($b)} (keys %fileHash)) {

            my (
                $cmdObj,
                @list, @checkList, @endList,
            );

            $cmdObj = $axmud::CLIENT->ivShow('clientCmdHash', $cmd);
            $cmdCount++;

            if (! $cmdObj) {

                $session->writeText('   Unrecognised command \'' . $cmd . '\' found');
                $errorCount++;
                next OUTER;
            }

            # Read the help file. The TRUE argument tells $self->help not to discard any lines from
            #   the beginning and end of the file (so that we can test them)
            @list = $cmdObj->help($session, TRUE);
            if (! @list) {

                $session->writeText('   Empty help file for \'' . $cmd . '\' command');
                $errorCount++;
                next OUTER;

            } elsif (scalar @list < 7) {

                # Shortest complete help file is 7 lines long
                $session->writeText('   Incomplete help file for \'' . $cmd . '\' command');
                $errorCount++;
                next OUTER;
            }

            # Test the length of this command object's user commands
            foreach my $userCmd ($cmdObj->userCmdList) {

                if (length $userCmd > 32) {

                    $session->writeText(
                        '   User command \'' . $userCmd . '\' (redirects to standard command '
                        . '\') is too long',
                    )
                    ;
                    $errorCount++;
                }
            }

            # Check every line for tabs
            for (my $index = 0; $index < scalar @list; $index++) {

                my $line = $list[$index];

                if ($line =~ m/\t/) {

                    $session->writeText(
                        '   Command \'' . $cmd . '\', undesirable tab(s) on line #' . ($index + 2),
                    );

                    $session->writeText('   > ' . $line);
                    $errorCount++;
                }
            }

            # Check that the last line isn't empty
            $lastLine = $list[-1];
            if (! ($lastLine =~ m/\S/)) {

                $session->writeText('   Command \'' . $cmd . '\', empty last line');
                $errorCount++;
            }

            # The first three lines should match the text generated by ->getHelpStart
            @checkList = $cmdObj->getHelpStart();
            INNER: for (my $index = 0; $index < 3; $index++) {

                my ($string, $checkString);

                $string = $list[$index];
                $checkString = $checkList[$index];
                # Trailing whitespace is irrelevant, so remove it
                $string =~ s/\s+$//;
                $checkString =~ s/\s+$//;

                if ($string ne $checkString) {

                    $session->writeText(
                        '   Command \'' . $cmd . '\', invalid initial line #' . ($index + 2) . ':',
                    );

                    $session->writeText('   > ' . $string);
                    $session->writeText('   < ' . $checkString);
                    $errorCount++;
                    next INNER;
                }
            }

            # The final three lines should match the text generated by ->getHelpEnd
            @checkList = reverse ($cmdObj->getHelpEnd());
            @endList = reverse @list;
            INNER: for (my $index = 0; $index < 3; $index++) {

                my ($string, $checkString, $number);

                $string = $endList[$index];
                $checkString = $checkList[$index];
                # Trailing whitespace is irrelevant, so remove it
                $string =~ s/\s+$//;
                $checkString =~ s/\s+$//;

                if ($string ne $checkString) {

                    $number = scalar @list - 2 + $index;
                    $session->writeText(
                        '   Command \'' . $cmd . '\', invalid final line #'
                        . ((scalar @list) + 1) . ':',
                    );

                    $session->writeText('   > ' . $string);
                    $session->writeText('   < ' . $checkString);
                    $errorCount++;
                    next INNER;
                }
            }

            # Check the length of each line
            INNER: for (my $index = 0; $index < scalar @list; $index++) {

                my $line = $list[$index];

                # Trailing whitespace is irrelevant, so remove it
                $line =~ s/\s+$//;

                if (length ($line) > $limit) {

                    $session->writeText(
                        '   Command \'' . $cmd . '\' line #' . ($index + 2) . ', length '
                        . length ($line) . ':',
                    );

                    $session->writeText('   > ' . $line);
                    $errorCount++;
                    next INNER;
                }
            }
        }

        # Test Axbasic. Create a dummy Axbasic script so we can access its IVs
        $scriptObj = Language::Axbasic::Script->new($session);

        # Test Axbasic keyword help files
        $session->writeText('Testing ' . $axmud::BASIC_NAME . ' keywords...');
        @sortedList = sort {lc($a) cmp lc($b)} ($scriptObj->keywordList);

        OUTER: foreach my $keyword (@sortedList) {

            my @list;

            $keywordCount++;

            # 'Weak' keywords don't have a help file
            if ($scriptObj->ivExists('weakKeywordHash', $keyword)) {

                next OUTER;
            }

            # Read the help file
            @list = $self->abHelp($session, $keyword, 'keyword');
            if (! @list) {

                $session->writeText('   Missing help file for \'' . $keyword . '\' keyword');
                $errorCount++;
                next OUTER;

            } elsif (scalar @list < 4) {

                # Shortest complete help file is 3 lines long
                $session->writeText('   Incomplete help file for \'' . $keyword . '\' keyword');
                $errorCount++;
                next OUTER;
            }

            # Check every line for tabs
            for (my $index = 0; $index < scalar @list; $index++) {

                my $line = $list[$index];

                if ($line =~ m/\t/) {

                    $session->writeText(
                        '   ' . $axmud::BASIC_NAME . ' keyword \'' . $keyword
                        . '\', undesirable tab(s) on line #' . ($index + 2),
                    );

                    $session->writeText('   > ' . $line);
                    $errorCount++;
                }
            }

            # Check that the last line isn't empty
            $lastLine = $list[-1];
            if (! ($lastLine =~ m/\S/)) {

                $session->writeText(
                    '   ' . $axmud::BASIC_NAME . ' keyword \'' . $keyword . '\', empty last line',
                );

                $errorCount++;
            }

            # Check the length of each line
            INNER: for (my $index = 0; $index < scalar @list; $index++) {

                my $line = $list[$index];

                # Trailing whitespace is irrelevant, so remove it
                $line =~ s/\s+$//;

                if (length ($line) > $limit) {

                    $session->writeText(
                        '   ' . $axmud::BASIC_NAME . ' keyword \'' . $keyword . '\' line #'
                        . ($index + 2) . ', length ' . length ($line) . ':',
                    );

                    $session->writeText('   > ' . $line);
                    $errorCount++;
                    next INNER;
                }
            }
        }

        # Test Axbasic function help files
        $session->writeText('Testing ' . $axmud::BASIC_NAME . ' intrinsic functions...');
        @sortedList = sort {lc($a) cmp lc($b)} ($scriptObj->ivKeys('funcArgHash'));

        OUTER: foreach my $func (@sortedList) {

            my @list;

            $funcCount++;

            # Read the help file
            @list = $self->abHelp($session, $func, 'func');
            if (! @list) {

                $session->writeText('   Missing help file for \'' . $func . '\' function');
                $errorCount++;
                next OUTER;

            } elsif (scalar @list < 4) {

                # Shortest complete help file is 4 lines long
                $session->writeText('   Incomplete help file for \'' . $func . '\' function');
                $errorCount++;
                next OUTER;
            }

            # Check every line for tabs
            for (my $index = 0; $index < scalar @list; $index++) {

                my $line = $list[$index];

                if ($line =~ m/\t/) {

                    $session->writeText(
                        '   ' . $axmud::BASIC_NAME . ' function \'' . $func . '\', undesirable'
                        . ' tab(s) on line #' . ($index + 2),
                    );

                    $session->writeText('   > ' . $line);
                    $errorCount++;
                }
            }

            # Check that the last line isn't empty
            $lastLine = $list[-1];
            if (! ($lastLine =~ m/\S/)) {

                $session->writeText(
                    '   ' . $axmud::BASIC_NAME . ' function \'' . $func . '\', empty last line',
                );

                $errorCount++;
            }

            # Check the length of each line
            INNER: for (my $index = 0; $index < scalar @list; $index++) {

                my $line = $list[$index];

                # Trailing whitespace is irrelevant, so remove it
                $line =~ s/\s+$//;

                if (length ($line) > $limit) {

                    $session->writeText(
                        '   ' . $axmud::BASIC_NAME . ' functon \'' . $func . '\' line #'
                        . ($index + 2) . ', length ' . length ($line) . ':',
                    );

                    $session->writeText('   > ' . $line);
                    $errorCount++;
                    next INNER;
                }
            }
        }

        # Test task help files
        $session->writeText('Testing ' . $axmud::SCRIPT . ' tasks...');
        @sortedList = sort {lc($a) cmp lc($b)} ($axmud::CLIENT->ivValues('taskPackageHash'));

        OUTER: foreach my $packageName (@sortedList) {

            my (
                $prettyName,
                @list,
            );

            $taskCount++;

            # Remove the Games::Axmud::Task:: bit to get the task's 'pretty' name
            $prettyName = $packageName;
            $prettyName =~ s/^Games\:\:Axmud\:\:Task\:\://;

            # Read the help file
            @list = $self->taskHelp($session, $prettyName);
            if (! @list) {

                $session->writeText('   Missing help file for \'' . $prettyName . '\' task');
                $errorCount++;
                next OUTER;
            }

            # Check every line for tabs
            for (my $index = 0; $index < scalar @list; $index++) {

                my $line = $list[$index];

                if ($line =~ m/\t/) {

                    $session->writeText(
                        '   ' . $axmud::SCRIPT . ' task \'' . $prettyName . '\', undesirable tab(s)'
                        . ' on line #' . ($index + 2),
                    );

                    $session->writeText('   > ' . $line);
                    $errorCount++;
                }
            }

            # Check that the last line isn't empty
            $lastLine = $list[-1];
            if (! ($lastLine =~ m/\S/)) {

                $session->writeText(
                    '   ' . $axmud::SCRIPT . ' task \'' . $prettyName . '\', empty last line',
                );

                $errorCount++;
            }

            # Check the length of each line
            INNER: for (my $index = 0; $index < scalar @list; $index++) {

                my $line = $list[$index];

                # Trailing whitespace is irrelevant, so remove it
                $line =~ s/\s+$//;

                if (length ($line) > $limit) {

                    $session->writeText(
                        '   ' . $axmud::SCRIPT . ' task \'' . $prettyName . '\' line #'
                        . ($index + 2) . ', length ' . length ($line) . ':',
                    );

                    $session->writeText('   > ' . $line);
                    $errorCount++;
                    next INNER;
                }
            }
        }

        # Test output of the ';about' command
        $session->writeText('Testing \';about\' command output...');

        @aboutList = $self->getAboutText();
        OUTER: for (my $index = 0; $index < scalar @aboutList; $index++) {

            my $line = $aboutList[$index];

            if (length ($line) > $limit) {

                $session->writeText(
                    '   \';about\' command output, line #' . ($index + 1) . ', length '
                    . length ($line) . ':',
                );

                $session->writeText('   > ' . $line);
                $errorCount++;
                next INNER;
            }
        }

        # Test Axmud quick help
        $session->writeText('Testing quick help...');

        # Load the quick help file
        $file = $axmud::SHARE_DIR . '/help/misc/quickhelp';
        if (! (-e $file)) {

            $session->writeText('   Missing quick help file');
            $errorCount++;

        } else {

            if (! open($fileHandle, $file)) {

                $session->writeText('   Unable to read quick help file');
                $errorCount++;

            } else {

                @quickList = <$fileHandle>;
                close($fileHandle);

                OUTER: for (my $index = 0; $index < scalar @quickList; $index++) {

                    my $line = $quickList[$index];

                    chomp $line;
                    if (length ($line) > $limit) {

                        $session->writeText(
                            '   Quick help file, line #' . ($index + 1) . ', length '
                            . length ($line) . ':',
                        );

                        $session->writeText('   > ' . $line);
                        $errorCount++;
                        last OUTER;
                    }
                }
            }
        }

        # Test Axmud peek/poke help
        $session->writeText('Testing peek/poke help...');

        # Load the peek/poke help file
        $file = $axmud::SHARE_DIR . '/help/misc/peekpoke';
        if (! (-e $file)) {

            $session->writeText('   Missing peek/poke help file');
            $errorCount++;

        } else {

            if (! open($fileHandle, $file)) {

                $session->writeText('   Unable to read peek/poke help file');
                $errorCount++;

            } else {

                @peekList = <$fileHandle>;
                close($fileHandle);

                OUTER: for (my $index = 0; $index < scalar @peekList; $index++) {

                    my $line = $peekList[$index];

                    chomp $line;
                    if (length ($line) > $limit) {

                        $session->writeText(
                            '   Peek/poke help file, line #' . ($index + 1) . ', length '
                            . length ($line) . ':',
                        );

                        $session->writeText('   > ' . $line);
                        $errorCount++;
                        last OUTER;
                    }
                }
            }
        }

        # Show confirmation
        return $self->complete(
            $session, $standardCmd,
            'End of list, errors: ' . $errorCount . ' (client commands: ' . $cmdCount . ', '
            . $axmud::BASIC_NAME . ' keywords: ' . $keywordCount . ', ' . $axmud::BASIC_NAME
            . ' intrinsic functions: ' . $funcCount . ', ' . $axmud::SCRIPT . ' tasks: '
            . $taskCount . ')',
        );
    }
}

{ package Games::Axmud::Cmd::DumpAscii;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('dumpascii', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['da', 'dumpascii'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Tests display of ASCII characters';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $charSet, $title, $stop,
            %asciiHash,
        );

        # Check for improper arguments
        if ((defined $switch && $switch ne '-e') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the session's character set (for speed)
        $charSet = $session->sessionCharSet;

        # Names for some special ASCII characters
        %asciiHash = (
            0   => 'null',
            1   => 'start of heading',
            2   => 'start of text',
            3   => 'end of text',
            4   => 'end of transmission',
            5   => 'enquiry',
            6   => 'acknowledge',
            7   => 'bell',
            8   => 'backspace',
            9   => 'horizontal tab',
            10  => 'NL line feed, new line',
            11  => 'vertical tab',
            12  => 'NP form feed, new page',
            13  => 'carriage return',
            14  => 'shift out',
            15  => 'shift in',
            16  => 'data link escape',
            17  => 'device control 1',
            18  => 'device control 2',
            19  => 'device control 3',
            20  => 'device control 4',
            21  => 'negative acknowledge',
            22  => 'synchronous idle',
            23  => 'end of trans. block',
            24  => 'cancel',
            25  => 'end of medium',
            26  => 'substitute',
            27  => 'esc',               # Escape
            28  => 'file separator',
            29  => 'group separator',
            30  => 'record separator',
            31  => 'unit separator',
            32  => 'space',
            127 => 'delete',
        );

        # Display header
        $title = 'ASCII character dump, using character set \'' . $charSet;
        if ($switch) {

            $session->writeText($title . '\' (characters 0-255)');
            $stop = 256;

        } else {

            $session->writeText($title . '\' (characters 0-127)');
            $stop = 128;
        }

        $session->writeText('     #   Char   Name (in standard ASCII)');

        # Display list
        for (my $num = 0; $num < $stop; $num++) {

            my ($chr, $text);

            if ($num == 0 || $num == 9 || $num == 10 || $num == 13) {

                # Characters that mess up our line formatting: 0 is 'null', 9 is horizontal tab,
                #   10 is line feed, 13 is carriage return
                $chr = '';

            } else {

                $chr = Encode::decode($charSet, chr($num));
            }

            Encode::decode($session->sessionCharSet, $text);

            if (exists $asciiHash{$num}) {
                $text = $asciiHash{$num};
            } else {
                $text = '';
            }

            $session->writeText(sprintf('   %3d   ', $num) . "$chr\t$text");
        }

        # Display footer
        return $self->complete($session, $standardCmd, 'End of list');
    }
}

{ package Games::Axmud::Cmd::TestColour;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('testcolour', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tco', 'testcolor', 'testcolour'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Tests display of ' . $axmud::SCRIPT . ' colour tags';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $word, $flag, $textViewObj,
            @colourList, @boldList, @textColourList, @ulList, @charList, @rgbList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Display a word in many different text and underlay colour combinations
        $word = 'test ';

        # Import the ordered lists of normal/bold colours
        @colourList = $axmud::CLIENT->constColourTagList;
        @boldList = $axmud::CLIENT->constBoldColourTagList;

        foreach my $tag (@colourList) {

            push (@ulList, 'ul_' . $tag);
        }

        foreach my $tag (@boldList) {

            push (@ulList, 'UL_' . $tag);
        }

        # Get combined lists of foreground colours and underlay colours
        @textColourList = (@colourList, @boldList);

        # Create a list containing a small sample of the 16.7 million possible RGB colour tags
        @charList = qw(0 1 2 3 4 5 6 7 8 9 A B C D E F);
        foreach my $char (@charList) {

            push (@rgbList, 'u#' . ($char x 2) . 'ABCD');
        }

        # Import the session's default textview object (for convenience)
        $textViewObj = $session->defaultTabObj->textViewObj;

        # Display colours
        $session->writeText($axmud::SCRIPT . ' standard colour tag test');

        foreach my $text (@textColourList) {

            foreach my $ul (@ulList) {

                $textViewObj->insertText($word, $text, $ul, 'echo');
            }

            $textViewObj->insertText('', 'after');
        }

        $textViewObj->insertText('', 'after');

        # Display colours with italics
        $session->writeText($axmud::SCRIPT . ' standard colour tags with italics');

        foreach my $text (@textColourList) {

            foreach my $ul (@ulList) {

                $textViewObj->insertText($word, $text, $ul, 'italics', 'echo');
            }

            $textViewObj->insertText('', 'after');
        }

        $textViewObj->insertText('', 'after');

        # Display colours with strike-through and underline
        $session->writeText(
            $axmud::SCRIPT . ' standard colour tags with strike-through / underline',
        );

        foreach my $text (@textColourList) {

            foreach my $ul (@ulList) {

                $textViewObj->insertText(
                    $word,
                    $text,
                    $ul,
                    'strike',
                    'underline',
                    'echo',
                );
            }

            $textViewObj->insertText('', 'after');
        }

        $textViewObj->insertText('', 'after');

        # Display a small selection of RGB colour tags
        $session->writeText(
            $axmud::SCRIPT . ' RGB colour tags (small selection of 16.7 million possible colours)',
        );

        foreach my $rgb (@rgbList) {

            my $text;

            if ($rgb eq 'u#000000') {
                $text = '#FFFFFF';
            } else {
                $text = '#000000';
            }

            $textViewObj->insertText(
                $word,
                $text,
                $rgb,       # An underlay colour, e.g. 'u#AAAAAA'
                'echo',
            );
        }

        $textViewObj->insertText('', 'after');

        return $self->complete($session, $standardCmd, 'Standard colour tag test complete');
    }
}

{ package Games::Axmud::Cmd::TestXTerm;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('testxterm', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['txt', 'testxterm'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Tests display of ' . $axmud::SCRIPT . ' xterm-256 colour tags';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $textViewObj, $colourCube,
            %colourHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the session's default textview object (for convenience)
        $textViewObj = $session->defaultTabObj->textViewObj;

        # Import the hash of xterm colours
        if (! $switch) {

            $colourCube = $axmud::CLIENT->currentColourCube;
            %colourHash = $axmud::CLIENT->xTermColourHash;

        } elsif ($switch eq '-x') {

            $colourCube = 'xterm';
            %colourHash = $axmud::CLIENT->constXTermColourHash;

        } elsif ($switch eq '-n') {

            $colourCube = 'netscape';
            %colourHash = $axmud::CLIENT->constNetscapeColourHash;

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid switch (try \'-x\' for the xterm cube, or \'-n\' for the netscape cube)',
            );
        }

        # Display header
        $session->writeText(
            $axmud::SCRIPT . ' xterm colour tag test (colour cube: ' . $colourCube . ')',
        );

        # Display list. Show columns of 4
        for (my $count = 0; $count < 255; $count += 4) {

            for (my $c = 0; $c < 4; $c++) {

                my ($index, $rgb, $text, $underlay, $echo);

                $index = $count + $c;       # Will cycle through range 0-255
                $rgb = $colourHash{'x' . $index};
                $underlay = 'ux' . $index;

                # Show either white or black text
                if (
                    ($index >= 0 && $index <= 7)    # (15 is white)
                    || ($index >= 16 && $index <= 23)
                    || ($index >= 232 && $index <= 243)
                ) {
                    $text = 'x15';      # White #FFFFFF
                } else {
                    $text = 'x16';      # Black #000000
                }

                if ($c == 3) {
                    $echo = 'after';
                } else {
                    $echo = 'echo'
                }

                $textViewObj->insertText(
                    sprintf(" %3i: ", $index) . " $rgb ",
                    $text,          # 'x15' or 'x15'
                    $underlay,      # e.g. 'ux255'
                    'echo',
                );

                $textViewObj->insertText(
                    ' ',
                    $echo,          # 'echo' or 'after' after every 4 colours
                );
            }
        }

        # Display footer
        return $self->complete($session, $standardCmd, 'XTerm colour tag test complete');
    }
}

{ package Games::Axmud::Cmd::TestFile;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('testfile', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tf', 'testfile'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Tests a data file';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $filePath, $scriptName, $fileType,
            %fileHash,
        );

        # Check for improper arguments
        if (! defined $switch || ($switch ne '-i' && $switch ne '-h' && $switch ne '-c')) {

            return $self->improper($session, $inputString);
        }

        # Prompt the user to select a file to test
        $filePath = $session->mainWin->showFileChooser(
            'Test file',
            'open',
            $axmud::DATA_DIR . '/data',
        );

        if (! $filePath) {

            return $self->complete($session, $standardCmd, 'File not tested');
        }

        # ;tf -i
        # ;tf -h
        if ($switch ne '-c') {

            # Get the file's header information (metadata) as a hash
            %fileHash = $axmud::CLIENT->configFileObj->examineDataFile($filePath, 'return_header');
            if (! %fileHash) {

                return $self->error(
                    $session, $inputString,
                    'File is not a valid ' . $axmud::SCRIPT . ' data file',
                );
            }

            # ;tf -i
            if ($switch eq '-i') {

                # Check the file was produced by this Axmud client
                $scriptName = $fileHash{'script_name'};
                $fileType = $fileHash{'file_type'};

                if (! $scriptName || ! $fileType) {

                    return $self->error(
                        $session, $inputString,
                        'File is not a valid ' . $axmud::SCRIPT . ' data file',
                    );

                } elsif (! $axmud::CLIENT->configFileObj->checkCompatibility($scriptName)) {

                    return $self->error(
                        $session, $inputString,
                        '\'' . $fileType . ' data file was created by \'' . $scriptName
                        . '\', which is not ' . $axmud::NAME_ARTICLE . '-compatible client',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'File type is \'' . $fileHash{'file_type'} . '\'',
                    );
                }

            # ;tf -h
            } else {

                # Show header (of this list, not the file header)
                $self->showHeader($session, $filePath, %fileHash);

                return $self->complete($session, $standardCmd, 'End of header');
            }

        # ;tf -c
        } else {

            # Get the entire contents of the file as a hash
            %fileHash = $axmud::CLIENT->configFileObj->examineDataFile($filePath, 'return_hash');
            if (! %fileHash) {

                return $self->error(
                    $session, $inputString,
                    'File is not a valid ' . $axmud::SCRIPT . ' data file',
                );
            }

            # Show header (of this list, not the file header)
            $self->showHeader($session, $filePath, %fileHash);
            # Delete the metadata from $fileHash, leaving the file contents
            delete $fileHash{'file_type'};
            delete $fileHash{'script_name'};
            delete $fileHash{'script_version'};
            delete $fileHash{'save_date'};
            delete $fileHash{'save_time'};
            delete $fileHash{'assoc_world_prof'};

            # Show the file contents
            $session->writeText('File contents for ' . $filePath);

            foreach my $key (keys %fileHash) {

                my $value = $fileHash{$key};

                if (defined $value) {
                    $session->writeText(sprintf('   %-32.32s %-48.48s', $key, $value));
                } else {
                    $session->writeText(sprintf('   %-32.32s <undef>', $key));
                }
            }

            return $self->complete($session, $standardCmd, 'End of file test');
        }
    }

    sub showHeader {

        # Called by $self->do for the switches -h and -c
        # Shows information about the specified file's header
        #
        # Expected arguments
        #   $session    - The calling function's GA::Session
        #   $filePath   - Path of the file being displayed
        #   %fileHash   - Contents of the file, stored in a hash (for -h, will only contain the
        #                   header information)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $session, $filePath, %fileHash) = @_;

        # Local variables
        my $scriptName;

        # Check for improper arguments
        if (! defined $session || ! defined $filePath || ! %fileHash) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->showHeader', @_);
        }

        # Show header (of this list, not the file header)
        $session->writeText('File header (metadata) for ' . $filePath);

        # Show list
        $session->writeText('   File type        : ' . $fileHash{'file_type'});

        $scriptName = $fileHash{'script_name'};
        $session->writeText('   Script name      : ' . $scriptName);
        $session->writeText('   Script version   : ' . $fileHash{'script_version'});
        $session->writeText('   Save date        : ' . $fileHash{'save_date'});
        $session->writeText('   Save time        : ' . $fileHash{'save_time'});

        if (defined $fileHash{'assoc_world_prof'}) {
            $session->writeText('   Associated world : ' . $fileHash{'assoc_world_prof'});
        } else {
            $session->writeText('   Associated world : <none>');
        }

        return 1;
    }
}

{ package Games::Axmud::Cmd::TestModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('testmodel', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tmd', 'testmodel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Performs an integrity check on the world model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $errorCount, $fixCount,
            @outputList,
        );

        # Check for improper arguments
        if ((defined $switch && $switch ne '-f') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # It might be a long wait, so make sure the message is visible right away
        $session->writeText('Testing \'' . $session->currentWorld->name . '\' world model...');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->do');

        # Perform the test
        if ($switch) {

            ($errorCount, $fixCount, @outputList) = $session->worldModelObj->testModel(
                $session,
                TRUE,           # Fix problems
                TRUE,           # Use a list of return values
            );

        } else {

            ($errorCount, $fixCount, @outputList) = $session->worldModelObj->testModel(
                $session,
                FALSE,          # Don't problems
                TRUE,           # Use a list of return values
            );
        }

        # That's the end of the test
        # Display any output. If a very large number of error messages have been generated, don't
        #   show all of them
        if ((scalar @outputList) > 100) {

            $session->writeText((scalar @outputList) . ' error messages; curtailing output');

            for (my $index = 0; $index < 100; $index++) {

                $session->writeText($outputList[$index]);
            }

        } else {

            foreach my $msg (@outputList) {

                $session->writeText($msg);
            }
        }

        return $self->complete(
            $session, $standardCmd,
            'World model integrity check complete (errors: ' . $errorCount . ', fixes '
            . $fixCount . ')',
        );
    }

}

{ package Games::Axmud::Cmd::TestPattern;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('testpattern', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tpt', 'testregex', 'patterntest', 'testpattern'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens the Pattern Test window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Open an 'other' window
        $session->mainWin->quickFreeWin('Games::Axmud::OtherWin::PatternTest', $session);

        return $self->complete(
            $session, $standardCmd,
            'Opened Pattern Test window',
        );
    }
}

{ package Games::Axmud::Cmd::QuickInput;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('quickinput', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['inp', 'qinput', 'quickinput'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens the Quick Input window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Open an 'other' window
        $session->mainWin->quickFreeWin('Games::Axmud::OtherWin::QuickInput', $session);

        return $self->complete(
            $session, $standardCmd,
            'Opened Quick Input window',
        );
    }
}

{ package Games::Axmud::Cmd::SimulateWorld;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('simulateworld', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sim', 'simworld', 'simulateworld'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Simulates text received from the world';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my $text;

        # (No improper arguments to check)

        # ;sim
        if (! @args) {

            # Open an 'other' window
            $session->mainWin->quickFreeWin(
                'Games::Axmud::OtherWin::Simulate',
                $session,
                # Config hash
                'type' => 'world',
            );

            return $self->complete(
                $session, $standardCmd,
                'Opened world simulation window',
            );

        # ;sim <text>
        } else {

            # Combine the arguments into a single line
            $text = join(' ', @args);

            # Convert any newline characters to real newline characters
            $text =~ s/\\n/\n/g;

            # Make sure the text ends in a newline character
            chomp $text;
            $text .= "\n";

            # Call the GA::Session's function for processing incoming data, as if it had been called
            #   by GA::Session->incomingDataLoop. The TRUE argument means that the 'main' window's
            #   blinker shouldn't be turned on.
            $session->processIncomingData($text, TRUE);

            # Display confirmation
            return $self->complete(
                $session, $standardCmd,
                'World simulation complete (received text length: ' . (length $text)
                . ' characters)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SimulatePrompt;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('simulateprompt', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['spr', 'simpr', 'simprompt', 'simulateprompt'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Simulates a prompt received from the world';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my $text;

        # (No improper arguments to check)

        if (! @args) {

            # Open an 'other' window
            $session->mainWin->quickFreeWin(
                'Games::Axmud::OtherWin::Simulate',
                $session,
                # Config hash
                'type' => 'prompt',
            );

            return $self->complete(
                $session, $standardCmd,
                'Opened prompt simulation window',
            );

        } else {

            # Combine the arguments into a single line
            $text = join(' ', @args);

            # Convert any newline characters to real newline characters
            $text =~ s/\\n/\n/g;

            # Remove the final newline character(s) to make this a prompt
            chomp $text;

            # Call the GA::Session's function for processing incoming data, as if it had been called
            #   by GA::Session->incomingDataLoop. The TRUE argument means that the 'main' window's
            #   blinker shouldn't be turned on.
            $session->processIncomingData($text, TRUE);

            # Display confirmation
            return $self->complete(
                $session, $standardCmd,
                'Prompt simulation complete (received text length: ' . (length $text)
                . ' characters)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SimulateCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('simulatecommand', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['scm', 'simcmd', 'simulatecmd', 'simulatecommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Simulates a world command';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $cmd,
            $check,
        ) = @_;

        # Local variables
        my $limit;

        # Check for improper arguments
        if (! defined $cmd || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Can't simulate world commands while there are excess commands, waiting to be sent...
        if ($session->excessCmdList) {

            return $self->error(
                $session, $inputString,
                'Can\'t simulate world commands while there are excess world commands still waiting'
                . ' to be sent (try again in a few moments)',
            );

        # ...or if there is a ';simulatecommand' already in operation
        } elsif ($session->disableWorldCmdFlag) {

            return $self->error(
                $session, $inputString,
                'There is already a \';simulatecommand\' operation in progress (try again in a few'
                . ' moments)',
            );
        }

        # Temporarily override the world profile's ->excessCmdLimit IV, allowing any number of world
        #   commands to be processed instantaneously
        $limit = $session->currentWorld->excessCmdLimit;
        $session->currentWorld->ivPoke('excessCmdLimit', 0);

        # Temporarily disable world commands from actually being sent to the world
        $session->set_disableWorldCmdFlag(TRUE);

        # Simulate the world command (this client command can't be used to simulate echo commands,
        #   Perl commands, etc, so we don't call ->doInstruct)
        $session->worldCmd($cmd);

        # Restore IVs
        $session->set_disableWorldCmdFlag(FALSE);
        $session->currentWorld->ivPoke('excessCmdLimit', $limit);

        return $self->complete(
            $session, $standardCmd,
            'Simulated the world command \'' . $cmd . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::SimulateHook;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('simulatehook', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['shk', 'simhook', 'simulatehook'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Simulates ' . $axmud::NAME_ARTICLE . ' hook event';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $hookVar, $hookVal,
            $check,
        ) = @_;

        # Local variables
        my $event;

        # Check for improper arguments
        if (! defined $switch || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Do hook events that don't use $hookVar/$hookVal first
        if ($switch eq 'connect' || $switch eq '-c') {

            # Convert the switch -c to the hook event 'connect'
            $event = 'connect';

        } elsif ($switch eq 'disconnect' || $switch eq '-d') {

            $event = 'disconnect';

        } elsif ($switch eq 'login' || $switch eq '-l') {

            $event = 'login';

        } elsif ($switch eq 'current_session' || $switch eq '-u') {

            $event = 'current_session';

        } elsif ($switch eq 'visible_session' || $switch eq '-v') {

            $event = 'visible_session';

        } elsif ($switch eq 'get_focus' || $switch eq '-g') {

            $event = 'get_focus';

        } elsif ($switch eq 'lose_focus' || $switch eq '-f') {

            $event = 'lose_focus';

        } elsif ($switch eq 'close_disconnect' || $switch eq '-x') {

            $event = 'close_disconnect';

        } elsif ($switch eq 'map_rescue_merge' || $switch eq '-mm') {

            $event = 'map_rescue_merge';

        } elsif ($switch eq 'map_rescue_off' || $switch eq '-mf') {

            $event = 'map_rescue_off';
        }

        if ($event) {

            if ($hookVar) {

                return $self->error(
                    $session, $inputString,
                    'Hook data isn\'t used with the \'' . $event . '\' hook event',
                );

            } else {

                $session->checkHooks($event);

                return $self->complete(
                    $session, $standardCmd,
                    'Simulated the \'' . $event . '\' hook event',
                );
            }
        }

        # Now do hook events that use $hookVar, but not $hookVal
        if ($switch eq 'prompt' || $switch eq '-p') {

            $event = 'prompt';

        } elsif ($switch eq 'receive_text' || $switch eq '-r') {

            $event = 'receive_text';

        } elsif ($switch eq 'sending_cmd' || $switch eq '-i') {

            $event = 'sending_cmd';

        } elsif ($switch eq 'send_cmd' || $switch eq '-s') {

            $event = 'send_cmd';

        } elsif ($switch eq 'system_text' || $switch eq '-st') {

            $event = 'system_text';

        } elsif ($switch eq 'system_error' || $switch eq '-se') {

            $event = 'system_error';

        } elsif ($switch eq 'system_warning' || $switch eq '-sw') {

            $event = 'system_warning';

        } elsif ($switch eq 'system_debug' || $switch eq '-sd') {

            $event = 'system_debug';

        } elsif ($switch eq 'system_improper' || $switch eq '-si') {

            $event = 'system_improper';

        } elsif ($switch eq 'system_all' || $switch eq '-sa') {

            $event = 'system_all';

        } elsif ($switch eq 'system_all_error' || $switch eq '-sl') {

            $event = 'system_all_error';

        } elsif ($switch eq 'not_current' || $switch eq '-o') {

            $event = 'not_current';

        } elsif ($switch eq 'change_current' || $switch eq '-h') {

            $event = 'change_current';

        } elsif ($switch eq 'not_visible' || $switch eq '-j') {

            $event = 'not_visible';

        } elsif ($switch eq 'change_visible' || $switch eq '-k') {

            $event = 'change_visible';

        } elsif ($switch eq 'textview_resize' || $switch eq '-tv') {

            $event = 'textview_resize';

        } elsif ($switch eq 'user_idle' || $switch eq '-e') {

            $event = 'user_idle';

        } elsif ($switch eq 'world_idle' || $switch eq '-w') {

            $event = 'world_idle';

        } elsif ($switch eq 'aard102' || $switch eq '-q') {

            $event = 'aard102';

        } elsif ($switch eq 'zmp' || $switch eq '-z') {

            $event = 'zmp';

        } elsif ($switch eq 'atcp' || $switch eq '-a') {

            $event = 'atcp';

        } elsif ($switch eq 'gmcp' || $switch eq '-y') {

            $event = 'gmcp';

        } elsif ($switch eq 'mcp' || $switch eq '-b') {

            $event = 'mcp';

        } elsif ($switch eq 'map_room' || $switch eq '-mr') {

            $event = 'map_room';

        } elsif ($switch eq 'map_no_room' || $switch eq '-mn') {

            $event = 'map_no_room';

        } elsif ($switch eq 'map_lost' || $switch eq '-ml') {

            $event = 'map_lost';

        } elsif ($switch eq 'map_rescue_on' || $switch eq '-mo') {

            $event = 'map_rescue_on';
        }

        if ($event) {

            if (! $hookVar || $hookVal) {

                return $self->error(
                    $session, $inputString,
                    'One item of hook data must be used with the \'' . $event . '\' hook event',
                );

            } else {

                $session->checkHooks($event, $hookVar);

                return $self->complete(
                    $session, $standardCmd,
                    'Simulated the \'' . $event . '\' hook event',
                );
            }
        }

        # Now do hook events that use both $hookVar and $hookVal
        if ($switch eq 'msdp' || $switch eq '-m') {

            $event = 'msdp';

        } elsif ($switch eq 'mssp' || $switch eq '-n') {

            $event = 'mssp';
        }

        if ($event) {

            if (! $hookVar || ! $hookVal) {

                return $self->error(
                    $session, $inputString,
                    'Two items of hook data must be used with the \'' . $event . '\' hook event',
                );

            } else {

                $session->checkHooks($event, $hookVar, $hookVal);

                return $self->complete(
                    $session, $standardCmd,
                    'Simulated the \'' . $event . '\' hook event',
                );
            }
        }

        # Now do custom hook events. There is no central register of custom hook events, so fire any
        #   custom hook event whose name is in the right format (starts with an underscore,
        #   followed by a numeric character)
        if ($switch =~ m/^\_\w/) {

            $session->checkHooks($switch);

            return $self->complete(
                $session, $standardCmd,
                'Simulated the custom hook event \'' . $switch . '\'',
            );

        } else {


            return $self->error(
                $session, $inputString,
                'Unrecognised switch/hook event \'' . $switch . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DebugToggle;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('debugtoggle', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dbt', 'debugtoggle'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles ' . $axmud::SCRIPT . ' debugging flags';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $iv, $descrip, $string,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;dbt
        if (! $switch) {

            # Display header
            $session->writeText('Debug flag list');

            # Display list
            @list = (
                'debugLineNumsFlag'     => '\'Main\' window shows explicit line numbers     ',
                'debugLineTagsFlag'     => '\'Main\' window shows explict colour/style tags ',
                'debugLocatorFlag'      => 'Locator task shows debug messages             ',
                'debugMaxLocatorFlag'   => 'Locator task shows extensive debug messages   ',
                'debugExitFlag'         => 'Illegal exit directions show debug messages   ',
                'debugMoveListFlag'     => 'Locator task shows expected rooms             ',
                'debugParseObjFlag'     => 'Object parsing shows debug messages           ',
                'debugCompareObjFlag'   => 'Object comparison shows debug messages        ',
                'debugExplainPluginFlag'
                                        => 'Plugin load failure shows debug messages      ',
                'debugCheckIVFlag'      => 'Show error if code acccesses bad property     ',
                'debugTableFitFlag'     => 'Show resize errors for table objects          ',
                'debugTrapErrorFlag'    => 'Show Perl errors/warnings in \'main\' window    ',
            );

            do {

                $iv = shift @list;
                $descrip = shift @list;

                if ($axmud::CLIENT->$iv) {
                    $string = 'on';
                } else {
                    $string = 'off';
                }

                $session->writeText('   ' . $descrip . ' : ' . $string);

            } until (! @list);

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of list (' . ((scalar @list) / 2) . ' debug flags found)',
            );

        # ;dbt <switch>
        } else {

            if ($switch eq '-n') {
                $iv = 'debugLineNumsFlag';
            } elsif ($switch eq '-e') {
                $iv = 'debugLineTagsFlag';
            } elsif ($switch eq '-l') {
                $iv = 'debugLocatorFlag';
            } elsif ($switch eq '-x') {
                $iv = 'debugMaxLocatorFlag';
            } elsif ($switch eq '-d') {
                $iv = 'debugExitFlag';
            } elsif ($switch eq '-m') {
                $iv = 'debugMoveListFlag';
            } elsif ($switch eq '-p') {
                $iv = 'debugParseObjFlag';
            } elsif ($switch eq '-c') {
                $iv = 'debugCompareObjFlag';
            } elsif ($switch eq '-f') {
                $iv = 'debugExplainPluginFlag';
            } elsif ($switch eq '-i') {
                $iv = 'debugCheckIVFlag';
            } elsif ($switch eq '-r') {
                $iv = 'debugTableFitFlag';
            } elsif ($switch eq '-t') {
                $iv = 'debugTrapErrorFlag';
            } else {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised switch \'' . $switch . '\'',
                );
            }

            $axmud::CLIENT->toggle_debugFlag($iv);

            if (
                $iv eq 'debugLocatorFlag'
                && ! $axmud::CLIENT->debugLocatorFlag
                && $axmud::CLIENT->debugMaxLocatorFlag
            ) {
                # Turning off ->debugLocatorFlag also turns off ->debugMaxLocatorFlag
                $axmud::CLIENT->toggle_debugFlag('debugMaxLocatorFlag');
            }

            if ($axmud::CLIENT->$iv) {
                $string = 'on';
            } else {
                $string = 'off';
            }

            return $self->complete(
                $session, $standardCmd,
                '\'' . $iv . '\' flag set to ' . $string,
            );
        }
    }
}

{ package Games::Axmud::Cmd::DebugConnection;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('debugconnection', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dco', 'debugconnect', 'debugconnection'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles connection debugging flags';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $iv, $descrip, $string,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;dco
        if (! $switch) {

            # Display header
            $session->writeText('Connection debug flag list');

            # Display list
            @list = (
                'debugEscSequenceFlag'  => 'Show invalid escape sequences                      ',
                'debugTelnetFlag'       => 'Show telnet option negotiations messages           ',
                'debugTelnetMiniFlag'   => 'Show short option negotiation messages             ',
                'debugTelnetLogFlag'    => 'Telnet library writes its own logfile, telopt.log  ',
                'debugMsdpFlag'         => 'Show messages for MSDP data sent to Status/Locator ',
                'debugMxpFlag'          => 'Show messages for MXP errors                       ',
                'debugMxpCommentFlag'   => 'Display MXP comments                               ',
                'debugPuebloFlag'       => 'Show messages for Pueblo errors                    ',
                'debugPuebloCommentFlag'
                                        => 'Display Pueblo comments                            ',
                'debugZmpFlag'          => 'Display incoming ZMP data                          ',
                'debugAtcpFlag'         => 'Display incoming ATCP data                         ',
                'debugGmcpFlag'         => 'Display incoming GMCP data                         ',
                'debugMcpFlag'          => 'Show messages for MCP errors                       ',
            );

            do {

                $iv = shift @list;
                $descrip = shift @list;

                if ($axmud::CLIENT->$iv) {
                    $string = 'on';
                } else {
                    $string = 'off';
                }

                $session->writeText('   ' . $descrip . ' : ' . $string);

            } until (! @list);

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of list (' . ((scalar @list) / 2) . ' debug flags found)',
            );

        # ;dco <switch>
        } else {

            if ($switch eq '-q') {
                $iv = 'debugEscSequenceFlag';
            } elsif ($switch eq '-t') {
                $iv = 'debugTelnetFlag';
            } elsif ($switch eq '-s') {
                $iv = 'debugTelnetMiniFlag';
            } elsif ($switch eq '-l') {
                $iv = 'debugTelnetLogFlag';
            } elsif ($switch eq '-d') {
                $iv = 'debugMsdpFlag';
            } elsif ($switch eq '-x') {
                $iv = 'debugMxpFlag';
            } elsif ($switch eq '-c') {
                $iv = 'debugMxpCommentFlag';
            } elsif ($switch eq '-p') {
                $iv = 'debugPuebloFlag';
            } elsif ($switch eq '-u') {
                $iv = 'debugPuebloCommentFlag';
            } elsif ($switch eq '-z') {
                $iv = 'debugZmpFlag';
            } elsif ($switch eq '-a') {
                $iv = 'debugAtcpFlag';
            } elsif ($switch eq '-g') {
                $iv = 'debugGmcpFlag';
            } elsif ($switch eq '-m') {
                $iv = 'debugMcpFlag';
            } else {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised switch \'' . $switch . '\'',
                );
            }

            $axmud::CLIENT->toggle_debugFlag($iv);

            if ($axmud::CLIENT->$iv) {
                $string = 'on';
            } else {
                $string = 'off';
            }

            return $self->complete(
                $session, $standardCmd,
                '\'' . $iv . '\' flag set to ' . $string,
            );
        }
    }
}

{ package Games::Axmud::Cmd::Restart;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('restart', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['res', 'restart'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Restarts suspended ' . $axmud::SCRIPT . ' internal processes';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $count, $errorCount,
            @sessionList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (! $axmud::CLIENT->suspendSessionLoopFlag) {

            return $self->complete(
                $session, $standardCmd,
                $axmud::SCRIPT . ' internal processes are not currently suspended',
            );

        } else {

            $axmud::CLIENT->restoreSessionLoops();

            # GA::Client->haltSessionLoops displayed a message in every session, so this command
            #   must display a message in every session too
            foreach my $otherSession ($axmud::CLIENT->ivValues('sessionHash')) {

                if ($otherSession ne $session) {

                    $otherSession->writeText($axmud::SCRIPT . ' internal processes restarted');
                }
            }

            return $self->complete(
                $session, $standardCmd,
                $axmud::SCRIPT . ' internal processes restarted',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Peek;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('peek', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['peek'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Reads ' . $axmud::SCRIPT . ' internal data';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $string,
            $check,
        ) = @_;

        # Local variables
        my ($successFlag, $blessed, $ivName, $var, $objFlag, $privFlag, $text, $otherFlag);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If $string was not specified, use the last string specified in a ;poke command
        if (! defined $string) {

            if (defined $session->prevPokeString) {

                $string = $session->prevPokeString;

            } else {

                return $self->error(
                    $session, $inputString,
                    'Cannot peek at internal variables - no string specified',
                );
            }
        }

        # Parse the supplied $string into its components. If the parsing succeeds, the func returns
        #   an array with six elements
        ($successFlag, $blessed, $ivName, $var, $objFlag, $privFlag)
            = $session->parsePeekPoke($string);

        if (! $successFlag) {

            # On failure, $blessed contains an error message
            return $self->error($session, $inputString, 'Peek failure: ' . $blessed);

        } else {

            # Display the interal variables
            $session->writeText('Peek \'' . $string . '\'');

            if ($objFlag) {

                $text = 'Perl object';

            } elsif (defined $blessed && defined $ivName) {

                $text = 'Perl object and IV (instance variable, or object property)';

            } else {

                $otherFlag = TRUE;
                $text = 'Value extracted from an IV (instance variable, or object property)';
            }

            $session->writeText('   Type     : ' . $text);

            if (! $privFlag) {
                $text = 'Read/write';
            } else {
                $text = 'Read only';
            }

            $session->writeText('   Privacy  : ' . $text);

            if (defined $blessed) {

                $session->writeText('   Object   : ' . $blessed);
                $session->writeText('   Class    : ' . $blessed->_objClass);
            }

            if (defined $ivName) {

                if (! $otherFlag) {
                    $session->writeText('   IV       : ' . $ivName);
                } else {
                    $session->writeText('   From IV  : ' . $ivName);
                }
            }

            if (! $objFlag) {

                if (! defined $var) {

                    $session->writeText('   Value    : <undef>');

                } else {

                    if ( ref($var) eq 'ARRAY') {

                        if (! @$var) {

                            $session->writeText('   List     : <empty>');

                        } else {

                            $session->writeText('   List     :');
                            foreach my $item (@$var) {

                                if (defined $item) {
                                    $session->writeText('      ' . $item);
                                } else {
                                    $session->writeText('      <undef>');
                                }
                            }
                        }

                    } elsif ( ref($var) eq 'HASH') {

                        if (! %$var) {

                            $session->writeText('   Hash     : <empty>');

                        } else {

                            $session->writeText('   Hash     :');
                            $session->writeText('      KEY                            VALUE');
                            foreach my $key (sort {lc($a) cmp lc($b)} (keys %$var)) {

                                my $value = $$var{$key};

                                if (! defined $value) {

                                    $value = '<undef>';
                                }

                                $session->writeText(
                                    '      ' . sprintf('%-30.30s %-50.50s', $key, $value),
                                );
                            }
                        }

                    } else {

                        if (defined $var) {
                            $session->writeText('   Value    : ' . $var);
                        } else {
                            $session->writeText('   Value    : <undef>');
                        }
                    }
                }
            }

            # Remember the value, in case ;poke string1 is used, followed by ;peek string2 - string2
            #   is the one we want to remember
            $session->set_prevPokeString($string);

            return $self->complete($session, $standardCmd, 'Peek complete');
        }
    }
}

{ package Games::Axmud::Cmd::Poke;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('poke', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['poke'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies ' . $axmud::SCRIPT . ' internal data';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $string,
            @args,
        ) = @_;

        # Local variables
        my ($successFlag, $blessed, $ivName, $var, $objFlag, $privFlag, $msg);

        # Check for improper arguments
        if (! defined $string) {

            return $self->improper($session, $inputString);
        }

        # Parse the supplied $string into its components. If the parsing succeeds, the func returns
        #   an array with six elements
        ($successFlag, $blessed, $ivName, $var, $objFlag, $privFlag)
            = $session->parsePeekPoke($string);

        if (! $successFlag) {

            # On failure, $blessed contains an error message
            return $self->error($session, $inputString, 'Peek failure: ' . $blessed);

        } elsif ($objFlag) {

            return $self->error(
                $session, $inputString,
                'Cannot poke - \'' . $string . '\' is a Perl object',
            );

        } elsif ($privFlag) {

            return $self->error(
                $session, $inputString,
                'Cannot poke - \'' . $string . '\' refers to read-only data',
            );


        } elsif (! defined $blessed || ! defined $ivName) {

            return $self->error(
                $session, $inputString,
                'Cannot poke - \'' . $string . '\' does not refer to a simple object-property'
                . ' relationship',
            );

        } elsif (@args > 1 && ref($var) ne 'ARRAY' && ref($var) ne 'HASH') {

            return $self->error(
                $session, $inputString,
                'Can\'t poke a list or hash to a scalar property',
            );

        } else {

            # Set the IV's value
            $blessed->ivPoke($ivName, @args);

            # Operation complete
            if (ref($var) eq 'ARRAY') {
                $msg = 'list data';
            } elsif (ref($var) eq 'HASH') {
                $msg = 'hash data';
            } else {
                $msg = 'scalar value';
            }

            return $self->complete(
                $session, $standardCmd,
                'Wrote ' . $msg . ' to \'' . $string . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::PeekHelp;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('peekhelp', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['pkh', 'pokehelp', 'peekhelp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows strings for \';peek\' and \';poke\' operations';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $file, $fileHandle,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Load the quick help file
        $file = $axmud::SHARE_DIR . '/help/misc/peekpoke';
        if (! (-e $file)) {

            return $self->error(
                $session, $inputString,
                'Peek/poke help file not available',
            );

        } else {

            if (! open($fileHandle, $file)) {

                return $self->error(
                    $session, $inputString,
                    'Unable to read peek/poke help file',
                );

            } else {

                @list = <$fileHandle>;
                close($fileHandle);
            }
        }

        # Display the help
        foreach my $string (@list) {

            chomp $string;
            $session->writeText($string);
        }

        return $self->complete(
            $session, $standardCmd,
            'Peek/poke help displayed',
        );
    }
}

# Client commands

{ package Games::Axmud::Cmd::Help;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('help', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['h', 'help'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows help for client commands';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $cmd,
            $check
        ) = @_;

        # Local variables
        my (
            $disconnectFlag, $obj, $count, $string, $limit,
            @list,
            %hash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;h -d
        if ($cmd && $cmd eq '-d') {

            # Show asterisks in the long list to show which commands are available after
            #   disconnection
            $disconnectFlag = TRUE;
            # Apart from that, behave like an ordinary ';help' command without switches
            $cmd = undef;
        }

        # ;h
        if (! defined $cmd) {

            if ($axmud::BLIND_MODE_FLAG) {

                $session->writeText('List of client commands');

            } elsif ($session->status eq 'disconnected') {

                $disconnectFlag = TRUE;
                $session->writeText(
                    'List of client commands (* - available after a disconnection)',
                );

                $session->writeText('Type \';help <command>\' for more help');

            } else {

                $session->writeText(
                    'List of client commands (type \';help <command>\' for more help',
                );
            }

            # Display each line in the ordered list of commands. Lines that begin with the '@'
            #   character are group headings; all other lines are commands
            $count = 0;
            foreach my $line ($axmud::CLIENT->clientCmdPrettyList) {

                if (index ($line, '@') == 0) {

                    # Display a heading
                    $line =~ s/\@/   /;
                    $session->writeText($line);

                } else {

                    $count++;

                    if ($axmud::BLIND_MODE_FLAG) {

                        # In blind mode, display a simpler format
                        $session->writeText($self->composeBlindHelpLine($session, lc($line)));

                    } else {

                        # Display the command and its description (the TRUE arguments means we
                        #   should show whether the command is available offline, or not)
                        $session->writeText(
                            $self->composeHelpLine($session, lc($line), $disconnectFlag),
                        );
                    }
                }
            }

            if ($count == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (1 command displayed)',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (' . $count . ' commands displayed)',
                );
            }

        # ;h -s
        } elsif ($cmd eq '-s') {

            $session->writeText(
                'List of client commands (type \';help <command>\' for more help)',
            );

            # Import the command object hash and the maximum length for help files
            %hash = $axmud::CLIENT->clientCmdHash;
            $limit = $axmud::CLIENT->constHelpCharLimit - 3;

            # In the ordered list of commands, '@' are group headings; everything else is a client
            #   command
            $count = 0;
            foreach my $line ($axmud::CLIENT->clientCmdPrettyList) {

                my ($cmdObj, $newString);

                if (index ($line, '@') == 0) {

                    # Display the previous batch of commands (if any)
                    if ($string) {

                        $session->writeText('   ' . $string);
                        $string = '';
                    }

                    # Display a heading
                    $line =~ s/\@//;
                    $session->writeText($line);

                } else {

                    $count++;

                    # Get the command object
                    $cmdObj = $hash{lc($line)};

                    # Get the shortest/standard form of the client command
                    if ($axmud::BLIND_MODE_FLAG) {
                        $newString = lc($line);
                    } else {
                        $newString = $cmdObj->findShortestCmd . '/' . lc($line);
                    }

                    if ($string && (length($string) + length($newString)) > $limit) {

                        # Display the previous batch of commands - no room to add the new command
                        #   to it
                        $session->writeText('   ' . $string);
                        $string = '';
                    }

                    # Add the command to the string, to be displayed when the next header is found
                    if ($string) {
                        $string .= ', ' . $newString;
                    } else {
                        $string = $newString;
                    }
                }
            }

            if ($string) {

                # Display the last batch of commands
                $session->writeText($string);
            }

            if ($count == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (1 command displayed)',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (' . $count . ' commands displayed)',
                );
            }

        # ;h <command>
        } else {

            # Check <command> is a recognised user command
            if (! $axmud::CLIENT->ivExists('userCmdHash', $cmd)) {

                return $self->error(
                    $session, $inputString,
                    'The ' . $axmud::SCRIPT . ' command \'' . $cmd . '\' isn\'t recognised',
                );
            }

            # Translate <command> from a user command to a standard command (e.g. from 'ab' to
            #   'about')
            $cmd = $axmud::CLIENT->ivShow('userCmdHash', $cmd);
            # Get the blessed reference of the command object for this command
            $obj = $axmud::CLIENT->ivShow('clientCmdHash', $cmd);

            # Get the first three lines of the help text
            push (@list, $obj->getHelpStart());

            # Call the help function for the command to fetch the command-specific text
            if (index ((ref $obj), 'Games::Axmud::Cmd::Plugin::') == 0) {

                # Look in plugin help directories for the help file too
                push (@list, $obj->help($session));

            } else {

                # The TRUE argument means 'only look in the standard help directory'
                push (@list, $obj->help($session, undef, TRUE));
            }

            # Fetch the final three lines of the help text; add an extra blank line
            push (@list, $obj->getHelpEnd(), ' ');

            # Display the help
            foreach my $string (@list) {

                $session->writeText($string);
            }

            return $self->complete(
                $session, $standardCmd,
                'Help for \'' . $cmd . '\' command displayed',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Hint;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('hint', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['hint'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows the current world\'s hint text again';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $hint;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (! $session->currentWorld->worldHint) {

            return $self->complete(
                $session, $standardCmd,
                'There is no hint text for the current world \'' . $session->currentWorld->name
                . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                $session->currentWorld->worldHint,
            );
        }
    }
}

{ package Games::Axmud::Cmd::QuickHelp;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('quickhelp', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['qh', 'quickhelp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows short ' . $axmud::SCRIPT . ' help document';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $file, $fileHandle,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Load the quick help file
        $file = $axmud::SHARE_DIR . '/help/misc/quickhelp';
        if (! (-e $file)) {

            return $self->error(
                $session, $inputString,
                'Quick help file not available',
            );

        } else {

            if (! open($fileHandle, $file)) {

                return $self->error(
                    $session, $inputString,
                    'Unable to read quick help file',
                );

            } else {

                @list = <$fileHandle>;
                close($fileHandle);
            }
        }

        # Display the help
        foreach my $string (@list) {

            chomp $string;
            $session->writeText($string);
        }

        return $self->complete(
            $session, $standardCmd,
            'Quick help displayed',
        );
    }
}

{ package Games::Axmud::Cmd::SearchHelp;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('searchhelp', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sh', 'shelp', 'searchhelp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Searches ' . $axmud::SCRIPT . ' help';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $fileFlag, $basicFlag, $taskFlag, $flagCount, $header, $scriptObj, $title,
            $count,
            @cmdList, @objList, @keywordList, @funcList, @showList, @sortedList, @taskList,
            %matchHash,
        );

        # Extract arguments
        $flagCount = 0;

        ($switch, @args) = $self->extract('-c', 0, @args);
        if (defined $switch) {

            $fileFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-b', 0, @args);
        if (defined $switch) {

            $basicFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-t', 0, @args);
        if (defined $switch) {

            $taskFlag = TRUE;
            $flagCount++;
        }

        if ($flagCount > 1) {

            return $self->error(
                $session, $inputString,
                'The switches -f, -j and -t can\'t be combined',
            );
        }

        # Anything left in @args are the search terms
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Import an ordered list of command objects
        @cmdList = $axmud::CLIENT->clientCmdList;
        foreach my $cmd (@cmdList) {

            push (@objList, $axmud::CLIENT->ivShow('clientCmdHash', $cmd));
        }

        # Create a dummy LA::Script and get an ordered list of keywords and intrinsic
        #   functions
        $scriptObj = Language::Axbasic::Script->new($session);
        if (! defined $scriptObj && $basicFlag) {

            return $self->error(
                $session, $inputString,
                'General error fetching ' . $axmud::BASIC_NAME . ' help',
            );
        }

        # ;sh
        if (! $flagCount) {

            $header = $axmud::SCRIPT . ' help search (summaries only)';

            OUTER: foreach my $obj (@objList) {

                MIDDLE: foreach my $regex (@args) {

                    INNER: foreach my $string ($obj->userCmdList, $obj->descrip) {

                        if ($string =~ m/$regex/i) {

                             # Use this command
                            $matchHash{$obj->standardCmd} = $obj;
                            next OUTER;
                        }
                    }
                }
            }

        # ;sh -c
        } elsif ($fileFlag) {

            $header = $axmud::SCRIPT . ' help search (comprehensive search)';

            OUTER: foreach my $obj (@objList) {

                # Search the help file
                MIDDLE: foreach my $line ($obj->help($session)) {

                    INNER: foreach my $regex (@args) {

                        if ($line =~ m/$regex/i) {

                            # Use this command
                            $matchHash{$obj->standardCmd} = $obj;
                            next OUTER;
                        }
                    }
                }
            }
        }

        # ;sh
        # ;sh -c
        if ($header) {

            # If no matches found, just display an error
            if (! %matchHash) {

                return $self->error(
                    $session, $inputString,
                    'No matches found (searched ' . scalar @objList . ' commands)',
                );
            }

            # Display header
            $session->writeText($header);

            # Display list
            foreach my $item ($axmud::CLIENT->clientCmdPrettyList) {

                # Cunning algorithm to display each matching command below its correct title
                #   (if $item begins with a '@' character, it's a title, not a command)
                if (substr($item, 0, 1) eq '@') {

                    $title = $item;

                } elsif (exists $matchHash{ lc($item) }) {

                    if ($title) {

                        # Need to display the title above this command
                        push (@showList, $title, lc($item));
                        # Don't show the title again
                        $title = undef;

                    } else {

                        # Just show the command
                        push (@showList, lc($item));
                    }
                }
            }

            $count = 0;
            foreach my $line (@showList) {

                if (index ($line, '@') == 0) {

                    # Display a heading
                    $line =~ s/\@/   /;
                    $session->writeText($line);

                } else {

                    $count++;

                    if ($axmud::BLIND_MODE_FLAG) {

                        # In blind mode, display a simpler format
                        $session->writeText($self->composeBlindHelpLine($session, lc($line)));

                    } else {

                        # Display the command and its description (the TRUE arguments means we
                        #   should show whether the command is available offline, or not)
                        $session->writeText(
                            $self->composeHelpLine($session, lc($line), TRUE),
                        );
                    }
                }
            }

            # Display footer
            if ($count == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (1 matching command found)',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (' . $count . ' matching commands found)',
                );
            }
        }

        # ;sh -b
        if ($basicFlag) {

            # Check matching keywords
            OUTER: foreach my $keyword ($scriptObj->keywordList) {

                my @list;

                # 'Weak' keywords don't have a help file
                if ($scriptObj->ivExists('weakKeywordHash', $keyword)) {

                    next OUTER;
                }

                # Load the help file for this keyword
                @list = $self->abHelp($session, $keyword, 'keyword');

                MIDDLE: foreach my $line (@list) {

                    INNER: foreach my $regex (@args) {

                        if ($line =~ m/$regex/i) {

                            # Use this keyword
                            push (@keywordList, $keyword);
                            next OUTER;
                        }
                    }
                }
            }

            # Check matching intrinsic functions
            OUTER: foreach my $func (sort {lc($a) cmp lc($b)} ($scriptObj->ivKeys('funcArgHash'))) {

                # Load the help file for this keyword
                my @list = $self->abHelp($session, $func, 'func');

                MIDDLE: foreach my $line (@list) {

                    INNER: foreach my $regex (@args) {

                        if ($line =~ m/$regex/i) {

                            # Use this intrinsic function
                            push (@funcList, $func);
                            next OUTER;
                        }
                    }
                }
            }

            # Display header
            $session->writeText($axmud::BASIC_NAME . ' help search');

            # Display list
            $session->writeText('Matching keywords:');
            if (@keywordList) {
                $session->writeText(join(' ', @keywordList));
            } else {
                $session->writeText('<none>');
            }

            $session->writeText('Matching intrinsic functions:');
            if (@funcList) {
                $session->writeText(join(' ', @funcList));
            } else {
                $session->writeText('<none>');
            }

            # Display footer
            $count = scalar @keywordList + scalar @funcList;
            if ($count == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (1 matching keywords/functions found)',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (' . $count . ' matching keywords/functions found)',
                );
            }

        # ;sh -t
        } elsif ($taskFlag) {

            # Get a sorted list of task package names
            @sortedList = sort {lc($a) cmp lc($b)} ($axmud::CLIENT->ivValues('taskPackageHash'));
            OUTER: foreach my $packageName (@sortedList) {

                my (
                    $task,
                    @list,
                );

                # Load the help file for this task. The task's name is the package name, minus the
                #   initial 'Games::Axmud::Task::'
                $task =~ s/^Games\:\:Axmud\:\:Task\:\://;
                $task = substr($packageName, 12);
                @list = $self->taskHelp($session, $task);

                MIDDLE: foreach my $line (@list) {

                    INNER: foreach my $regex (@args) {

                        if ($line =~ m/$regex/i) {

                            # Use this keyword
                            push (@taskList, $task);
                            next OUTER;
                        }
                    }
                }
            }

            # Display header
            $session->writeText($axmud::SCRIPT . ' task help search');

            # Display list
            $session->writeText('Matching tasks:');
            if (@taskList) {
                $session->writeText(join(' ', @taskList));
            } else {
                $session->writeText('<none>');
            }

            # Display footer
            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (1 matching tasks found)',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (' . scalar @taskList . ' matching tasks found)',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ListReserved;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listreserved', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lrd', 'listrd', 'listreserved'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists all reserved names';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Display header
        $session->writeText(
            'List of ' . $axmud::SCRIPT . ' reserved names',
        );

        # Display list
        # Import a list of reserved names, sorted alphabetically
        @list = sort {lc($a) cmp lc($b)} ($axmud::CLIENT->ivKeys('constReservedHash'));
        foreach my $name (@list) {

            $session->writeText('   ' . $name);
        }

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 reserved name found)');

        } else {

            return $self->complete(
            $session, $standardCmd,
                'End of list (' . scalar @list . ' reserved names found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::About;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('about', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ab', 'about'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows information about ' . $axmud::SCRIPT;

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $urlRegex,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get the standard 'about' text as a list of strings
        @list = $self->getAboutText();
        if (! @list) {

            return $self->error(
                $session, $inputString,
                '\'About\' information not available',
            );

        } else {

            $urlRegex = $axmud::CLIENT->constUrlRegex;

            foreach my $string (@list) {

                # One of the lines contains a URL to the GNU website. If so, make the link clickable
                if ($string =~ m/$urlRegex/i) {

                    my ($start, $stop);

                    $start = length ($`);
                    $stop = $start + length($&);

                    # Split $string into three segments: text before the link (if any), the link
                    #   itself, and text after the link (if any)
                    if ($start > 0) {

                        $session->writeText(substr($string, 0, $start), 'echo');
                    }

                    if ($stop < length $string) {

                        $session->writeText(
                            substr($string, $start, ($stop - $start)),
                            'link', 'echo',
                        );

                        $session->writeText(substr($string, $stop));

                    } else {

                        $session->writeText(substr($string, $start, ($stop - $start)), 'link');
                    }

                } else {

                    # No GNU website in this line
                    $session->writeText($string);
                }
            }

            $session->writeText(' ');  # Extra blank line

            return $self->complete(
                $session, $standardCmd,
                '\'About\' information displayed',
            );
        }
    }
}

{ package Games::Axmud::Cmd::OpenAboutWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('openaboutwindow', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['oaw', 'aboutwin', 'openaboutwindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens the ' . $axmud::SCRIPT . ' information window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my ($name, $page);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Convert a switch into the argument required by GA::OtherWin::About->new, or the page
        #   number to switch to
        if (defined $switch) {

            if ($switch eq '-a') {

                $name = 'about';
                $page = 0;

            } elsif ($switch eq '-c') {

                $name = 'credits';
                $page = 1;

            } elsif ($switch eq '-h') {

                $name = 'help';
                $page = 2;

            } elsif ($switch eq '-p') {

                $name = 'peek';
                $page = 3;

            } elsif ($switch eq '-n') {

                $name = 'changes';
                $page = 4;

            } elsif ($switch eq '-i') {

                $name = 'install';
                $page = 5;

            } elsif ($switch eq '-g') {

                $name = 'license';
                $page = 6;

            } elsif ($switch eq '-l') {

                $name = 'license_2';
                $page = 7;

            } else {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised switch \'' . $switch . '\'',
                );
            }
        }

        # Check that the About window isn't already open
        if ($axmud::CLIENT->aboutWin) {

            # Window already open; draw attention to the fact by presenting it and opening the
            #   right page
            $axmud::CLIENT->aboutWin->restoreFocus();
            $axmud::CLIENT->aboutWin->notebook->set_current_page($page);

            return $self->complete(
                $session, $standardCmd,
                'The ' . $axmud::SCRIPT . ' information window is already open',
            );
        }

        # Open the About window
        if (
            ! $session->mainWin->quickFreeWin(
                'Games::Axmud::OtherWin::About',
                $session,
                # config
                'first_tab' => $name,
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Failed to open the ' . $axmud::SCRIPT . ' information window',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                $axmud::SCRIPT . ' information window opened',
            );
        }
    }
}

{ package Games::Axmud::Cmd::CloseAboutWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('closeaboutwindow', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['caw', 'closeabout', 'closeaboutwindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Closes the ' . $axmud::SCRIPT . ' information window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (! $axmud::CLIENT->aboutWin) {

            return $self->error(
                $session, $inputString,
                'The ' . $axmud::SCRIPT . ' information window is not open',
            );

        } else {

            # Close the window
            $axmud::CLIENT->aboutWin->winDestroy();
            if ($axmud::CLIENT->aboutWin) {

                return $self->error(
                    $session, $inputString,
                    'Failed to close the ' . $axmud::SCRIPT . ' information window',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    $axmud::SCRIPT . ' information window closed',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::EditQuick;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editquick', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['edq', 'editquick'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens the quick preference window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Open up a 'pref' window to edit the GA::Client
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::PrefWin::Quick',
                $session->mainWin,
                $session,
                'Quick preferences',
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not open the quick preference window',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened quick preference window',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditClient;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editclient', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['edc', 'editclient'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens a preference window for the ' . $axmud::SCRIPT . ' client';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Open up a 'pref' window to edit the GA::Client
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::PrefWin::Client',
                $session->mainWin,
                $session,
                $axmud::SCRIPT . ' client preferences',
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not open the client preference window',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened client preference window',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditSession;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editsession', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['eds', 'editsession'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens a preference window for the current session';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Open up a 'pref' window to edit the GA::Client
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::PrefWin::Session',
                $session->mainWin,
                $session,
                'Session preferences',
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not open the session preference window',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened session preference window',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SwitchSession;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('switchsession', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sws', 'swsession', 'switchsession'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Switches the current session';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg, $char,
            $check,
        ) = @_;

        # Local variables
        my (
            $nextSession, $matchFlag,
            @sortedList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If there's only one session, then of course we can't switch to a different session
        if ($axmud::CLIENT->ivPairs('sessionHash') == 1) {

            return $self->error(
                $session, $inputString,
                'Cannot switch between sessions because there is only one session open',
            );
        }

        # Get a list of sessions in the order in which they were created
        @sortedList = $axmud::CLIENT->listSessions();

        # ;sws
        if (! defined $arg) {

            # Switch to the next session
            $nextSession = $axmud::CLIENT->getNextSession($axmud::CLIENT->currentSession);

        # ;sws <number>
        } elsif (! $axmud::CLIENT->intCheck($arg, 0)) {

            # Switch to the specified session
            $nextSession = $axmud::CLIENT->ivShow('sessionHash', $arg);
            if (! $nextSession) {

                return $self->error(
                    $session, $inputString,
                    'Session #' . $arg . ' not found (try \'listsession\')',
                );
            }

        # ;sws -c
        } elsif ($arg eq '-c') {

            # Make this command's session the current session
            $nextSession = $session;

        # ;sws <world>
        # ;sws <world> <char>
        } else {

            # Check the world profile exists
            OUTER: foreach my $worldObj ($axmud::CLIENT->ivValues('worldProfHash')) {

                if ($worldObj->name eq $arg) {

                    $matchFlag = TRUE;
                    last OUTER;
                }
            }

            if (! $matchFlag) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised world profile \'' . $arg . '\'',
                );
            }

            # ;sws <world>
            if (! defined $char) {

                # Find the first session in the session list matching the world (starting from the
                #   beginning of the list, not from the position of the current session)
                OUTER: foreach my $thisSession (@sortedList) {

                    if ($thisSession->currentWorld->name eq $arg) {

                        $nextSession = $thisSession;
                        last OUTER;
                    }
                }

                if (! $nextSession) {

                    return $self->error(
                        $session, $inputString,
                        'No session found using the \'' . $arg . '\' world profile',
                    );
                }

            # ;sws <world> <char>
            } else {

                OUTER: foreach my $thisSession (@sortedList) {

                    if (
                        $thisSession->currentWorld->name eq $arg
                        && $thisSession->currentChar
                        && $thisSession->currentChar->name eq $char
                    ) {
                        $nextSession = $thisSession;
                        last OUTER;
                    }
                }

                if (! $nextSession) {

                    return $self->error(
                        $session, $inputString,
                        'No session found using the \'' . $arg . '\' world and \'' . $char
                        . '\' character profiles found',
                    );
                }
            }
        }

        # Is the new current session the same as the current one?
        if ($nextSession eq $axmud::CLIENT->currentSession) {

            return $self->complete(
                $session, $standardCmd,
                'Session #' . $nextSession->number . ' is already the current session',
            );

        } else {

            # Make the session's default tab the new visible tab in its pane object (this will
            #   set GA::Win::Internal->visibleSession as well as GA::Client->currentSession)
            if ($nextSession->defaultTabObj) {

                $nextSession->defaultTabObj->paneObj->setVisibleTab($nextSession->defaultTabObj);
            }

            # If all sessions have their own 'main' window, make sure the new current
            #   session's window is visible
            if (! $axmud::CLIENT->shareMainWinFlag) {

                $nextSession->mainWin->restoreFocus();
            }

            # (This message will, of course, appear in the old current session's tab)
            return $self->complete(
                $session, $standardCmd,
                'Switched to session #' . $nextSession->number,
            );
        }
    }
}

{ package Games::Axmud::Cmd::MaxSession;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('maxsession', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mxs', 'mxsession', 'maxsession'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the maximum number of concurrent sessions';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my $count;

        # Check for improper arguments
        if (! defined $number || defined $check) {

            return $self->improper($session, $inputString);
        }

       $count = $axmud::CLIENT->ivPairs('sessionHash');

        # Check that $number is valid
        if (! $axmud::CLIENT->intCheck($number, 1, $axmud::CLIENT->constSessionMax)) {

            return $self->error(
                $session, $inputString,
                'Invalid maximum number of concurrent sessions (must be a value in the range 1-'
                .  $axmud::CLIENT->constSessionMax . ')',
            );

        } elsif ($number < $count) {

            return $self->error(
                $session, $inputString,
                'Can\'t reduce the maximum number of concurrent sessions to \'' . $number
                . '\' - there are already ' . $count . ' sessions open',
            );

        } else {

            $axmud::CLIENT->set_sessionMax($number);

            return $self->complete(
                $session, $standardCmd,
                'Maximum number of concurrent sessions set to ' . $axmud::CLIENT->sessionMax,
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListSession;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listsession', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ls', 'listsession'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists sessions';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @sortedList;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get a sorted list of sessions
        @sortedList = $axmud::CLIENT->listSessions();

        # Display header
        $session->writeText('List of sessions (C current session, V visible session, + logged in)');
        $session->writeText('     Num  Status        World            Character');

        # Display list
        foreach my $thisSession (@sortedList) {

            my ($string, $char);

            if ($thisSession eq $axmud::CLIENT->currentSession) {
                $string = ' C';
            } else {
                $string = '  ';
            }

            if (
                $thisSession->mainWin->visibleSession
                && $thisSession->mainWin->visibleSession eq $thisSession
            ) {
                $string .= 'V';
            } else {
                $string .= ' ';
            }


            if ($thisSession->loginFlag) {
                $string .= '+ ';
            } else {
                $string .= '  ';
            }

            if ($thisSession->currentChar) {
                $char = $thisSession->currentChar->name;
            } else {
                $char = '<none>';
            }

            $session->writeText(
                $string . sprintf(
                    '%-4.4s %-13.13s %-16.16s %-16.16s',
                    $thisSession->number,
                    $thisSession->status,
                    $thisSession->currentWorld->name,
                    $char,
                ),
            );
        }

        # Display footer
        if (scalar (@sortedList) == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 session displayed)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar (@sortedList) . ' sessions displayed)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetSession;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setsession', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ss', 'setsession'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Changes various session settings';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;ss
        if (! defined $switch) {

            # Display header
            $session->writeText('List of session settings');

            # Display list
            $session->writeText('   Tab label format: ');
            if ($axmud::CLIENT->sessionTabMode eq 'bracket') {
                $session->writeText('      \'bracket\' - displayed as \'deathmud (Gandalf)\'');
            } elsif ($axmud::CLIENT->sessionTabMode eq 'hyphen') {
                $session->writeText('      \'hyphen\' - displayed as \'deathmud - Gandalf\'');
            } elsif ($axmud::CLIENT->sessionTabMode eq 'world') {
                $session->writeText('      \'world\' - displayed as \'deathmud\'');
            } elsif ($axmud::CLIENT->sessionTabMode eq 'char') {
                $session->writeText('      \'char\' - displayed as \'Gandalf\'');
            }

            if (! $axmud::CLIENT->xTermTitleFlag) {
                $session->writeText('   Display xterm titles                           - OFF');
            } else {
                $session->writeText('   Display xterm titles                           - ON');
            }

            if (! $axmud::CLIENT->longTabLabelFlag) {
                $session->writeText('   Display long world names                       - OFF');
            } else {
                $session->writeText('   Display long world names                       - ON');
            }

            if (! $axmud::CLIENT->simpleTabFlag) {
                $session->writeText('   Display simple tabs (single tab with no label) - OFF');
            } else {
                $session->writeText('   Display simple tabs (single tab with no label) - ON');
            }

            if (! $axmud::CLIENT->confirmCloseMainWinFlag) {
                $session->writeText('   Confirm before click-closing \'main\' window   - OFF');
            } else {
                $session->writeText('   Confirm before click-closing \'main\' window   - ON');
            }

            if (! $axmud::CLIENT->confirmCloseTabFlag) {
                $session->writeText('   Confirm before click-closing tabs              - OFF');
            } else {
                $session->writeText('   Confirm before click-closing tabs              - ON');
            }

            if (! $axmud::CLIENT->confirmCloseMenuFlag) {
                $session->writeText('   Confirm before closing session from menu       - OFF');
            } else {
                $session->writeText('   Confirm before closing session from menu       - ON');
            }

            if (! $axmud::CLIENT->confirmCloseToolButtonFlag) {
                $session->writeText('   Confirm before closing session from toolbar    - OFF');
            } else {
                $session->writeText('   Confirm before closing session from toolbar    - ON');
            }

            if (! $axmud::CLIENT->offlineOnDisconnectFlag) {
                $session->writeText('   Switch to \'offline\' mode on disconnection    - OFF');
            } else {
                $session->writeText('   Switch to \'offline\' mode on disconnection      - ON');
            }

            # Display footer
            return $self->complete($session, $standardCmd, 'End of list');

        } elsif ($switch eq '-b') {

            $axmud::CLIENT->set_sessionTabMode('bracket');

            return $self->complete(
                $session, $standardCmd,
                'Session tab label format set to \'bracket\' (displayed as \'deathmud (Gandalf)\')',
            );

        } elsif ($switch eq '-h') {

            $axmud::CLIENT->set_sessionTabMode('hyphen');

            return $self->complete(
                $session, $standardCmd,
                'Session tab label format set to \'hyphen\' (displayed as \'deathmud - Gandalf\')',
            );

        } elsif ($switch eq '-w') {

            $axmud::CLIENT->set_sessionTabMode('world');

            return $self->complete(
                $session, $standardCmd,
                'Session tab label format set to \'world\' (displayed as \'deathmud\')',
            );

        } elsif ($switch eq '-c') {

            $axmud::CLIENT->set_sessionTabMode('char');

            return $self->complete(
                $session, $standardCmd,
                'Session tab label format set to \'char\' (displayed as \'Gandalf\')',
            );

        } elsif ($switch eq '-x') {

            $axmud::CLIENT->toggle_sessionFlag('xterm');
            if (! $axmud::CLIENT->xTermTitleFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Display xterm titles in tabs turned OFF',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Display xterm titles in tabs turned ON',
                );
            }

        } elsif ($switch eq '-l') {

            $axmud::CLIENT->toggle_sessionFlag('long');
            if (! $axmud::CLIENT->longTabLabelFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Display long world names in tabs turned OFF',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Display long world names in tabs turned ON',
                );
            }

        } elsif ($switch eq '-s') {

            $axmud::CLIENT->toggle_sessionFlag('simple');
            if (! $axmud::CLIENT->simpleTabFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Display simple tabs (single tab with no label) turned OFF',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Display simple tabs (single tab with no label) turned ON',
                );
            }

        } elsif ($switch eq '-m') {

            $axmud::CLIENT->toggle_sessionFlag('close_main');
            if (! $axmud::CLIENT->confirmCloseMainWinFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Confirm before click-closing \'main\' window turned OFF',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Confirm before click-closing \'main\' window turned ON',
                );
            }

        } elsif ($switch eq '-t') {

            $axmud::CLIENT->toggle_sessionFlag('close_tab');
            if (! $axmud::CLIENT->confirmCloseTabFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Confirm before click-closing tabs turned OFF',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Confirm before click-closing tabs turned ON',
                );
            }

        } elsif ($switch eq '-e') {

            $axmud::CLIENT->toggle_sessionFlag('close_menu');
            if (! $axmud::CLIENT->confirmCloseMenuFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Confirm before closing session from menu turned OFF',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Confirm before closing session from menu turned ON',
                );
            }

        } elsif ($switch eq '-r') {

            $axmud::CLIENT->toggle_sessionFlag('close_toolbutton');
            if (! $axmud::CLIENT->confirmCloseToolButtonFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Confirm before closing session from toolbar turned OFF',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Confirm before closing session from toolbar turned ON',
                );
            }

        } elsif ($switch eq '-o') {

            $axmud::CLIENT->toggle_sessionFlag('switch_offline');
            if (! $axmud::CLIENT->offlineOnDisconnectFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Switch to \'offline\' mode on disconnection turned OFF',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Switch to \'offline\' mode on disconnection turned ON',
                );
            }

        } else {

            return $self->error(
                $session, $inputString,
                'Unrecognised switch \'' . $switch . '\' - try \';help setsession\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Connect;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('connect', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cn', 'connect'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Connects to a new world';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $offlineFlag, $world, $char, $winObj, $host, $port, $pass, $account, $worldObj,
        );

        # Extract switches. If a world is not specified, the '-o' switch is ignored
        ($switch, @args) = $self->extract('-o', 0, @args);
        if (defined $switch) {
            $offlineFlag = TRUE;
        } else {
            $offlineFlag = FALSE;
        }

        # Extract remaining arguments (if any)
        $world = shift @args;
        $char = shift @args;

        # There should be no further arguments
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # Check that we don't already have too many sessions open. If the current session is not
        #   connected to a world, then the call to GA::Client->startSession will terminate that
        #   session before creating a new one, so that's ok
        if (
            $axmud::BLIND_MODE_FLAG
            && ($session->status eq 'connecting' || $session->status eq 'connected')
        ) {
            return $self->error(
                $session, $inputString,
                'Can\'t open multiple sessions when ' . $axmud::SCRIPT . ' is running in \'blind\''
                . ' mode',
            );

        } elsif ($axmud::CLIENT->ivPairs('sessionHash') >= $axmud::CLIENT->sessionMax) {

           return $self->error(
                $session, $inputString,
                'Can\'t open a new session (' . $axmud::SCRIPT . ' has reached its limit of '
                . $axmud::CLIENT->sessionMax . ' sessions)',
            );
        }

        # ;cn
        if (! $world) {

            # In blind mode, use the usual dialogue windows
            if ($axmud::BLIND_MODE_FLAG) {

                return $axmud::CLIENT->connectBlind();
            }

            # Otherwise, check that the Connections window isn't already open
            if ($axmud::CLIENT->connectWin) {

                # Window already open; draw attention to the fact by presenting it
                $axmud::CLIENT->connectWin->restoreFocus();

                return $self->error(
                    $session, $inputString,
                    'The Connections window is already open',
                );
            }

            # Open the Connections window. If the user wants to connect to a world, it calls
            #   GA::Client->startSession
            if (! $session->mainWin->quickFreeWin('Games::Axmud::OtherWin::Connect', $session)) {

                return $self->error(
                    $session, $inputString,
                    'Could not open the Connections window',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Connections window opened',
                );
            }

        # ;cn <world>
        # ;cn <world> <char>
        } else {

            # Check that the world exists
            if (! $axmud::CLIENT->ivExists('worldProfHash', $world)) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised world profile \'' . $world . '\'',
                );

            } else {

                $worldObj = $axmud::CLIENT->ivShow('worldProfHash', $world);
            }

            # Connect to the same world. If <char> was not specified, we need to use an argument
            #   set to 'undef'
            if (! $char) {

                $char = undef;
            }

            # Get the world's connection details
            ($host, $port, $char, $pass, $account) = $worldObj->getConnectDetails($char);

            # Start a new GA::Session in a new 'main' window tab
            if (
                ! $axmud::CLIENT->startSession(
                    $world,
                    $host,
                    $port,
                    $char,
                    $pass,
                    $account,
                    undef,              # Default protocol
                    undef,              # No login mode
                    $offlineFlag,
                )
            ) {
                return $self->error(
                    $session, $inputString,
                    'General error connecting to \'' . $world . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Connecting to \'' . $world . '\' (in a different tab)',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::Reconnect;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('reconnect', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rc', 'reconnect'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Saves files and reconnects to the same world';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my ($offlineFlag, $choice, $msg, $host, $port, $char, $pass, $account);

        # Check for improper arguments
        if ((defined $switch && $switch ne '-o') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that the current session isn't using a temporary profile
        if ($session->currentWorld->noSaveFlag) {

            return $self->error(
                $session, $inputString,
                'This command is unavailable because the current world is a temporary profile'
                . ' (try \';connect\' instead)',
            );
        }

        # Set the offline mode flag, if the switch was specified
        if ($switch) {
            $offlineFlag = TRUE;
        } else {
            $offlineFlag = FALSE;
        }

        # If the user is currently connected to a world, prompt before reconnecting (this only
        #   happens with ';reconnect' and ';xconnect'; with commands like ';quit', the disconnection
        #   happens right away)
        if ($session->status eq 'connecting' || $session->status eq 'connected') {

            if (! $offlineFlag) {
                $msg = 'Are you sure you want to reconnect?',
            } else {
                $msg = 'Are you sure you want to reconnect in offline mode?',
            }

            # Ask the user for permission to save the files
            $choice = $session->mainWin->showMsgDialogue(
                'Reconnect',
                'question',
                $msg,
                'yes-no',
            );

            if ($choice ne 'yes') {

                return $self->complete($session, $standardCmd, 'Reconnection abandoned');

            } else {

                # Terminate the connection
                $session->doDisconnect(TRUE);
                # React to the disconnection, as if it had been initiated by the host
                $session->reactDisconnect(TRUE);
            }
        }

        # Get the current character (if any)
        if ($session->currentChar) {

            $char = $session->currentChar->name;
        }

        # Get the current world's connection details
        ($host, $port, $char, $pass, $account) = $session->currentWorld->getConnectDetails($char);

        # Start a new GA::Session in a new 'main' window tab
        if (
            ! $axmud::CLIENT->startSession(
                $session->currentWorld->name,
                $host,
                $port,
                $char,
                $pass,
                $account,
                undef,              # Default protocol
                undef,              # No login mode
                $offlineFlag,
            )
        ) {
            return $self->error(
                $session, $inputString,
                'General error reconnecting to \'' . $session->currentWorld->name . '\'',
            );

        } else {

            # (Can't display a confirmation message - the new session has taken over the tab)
            return 1;
        }
    }
}

{ package Games::Axmud::Cmd::XConnect;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('xconnect', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['xcn', 'xconnect'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Reconnects to the same world without saving files';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my ($offlineFlag, $choice, $msg, $host, $port, $char, $pass, $account);

        # Check for improper arguments
        if ((defined $switch && $switch ne '-o') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that the current session isn't using a temporary profile
        if ($session->currentWorld->noSaveFlag) {

            return $self->error(
                $session, $inputString,
                'This command is unavailable because the current world is a temporary profile'
                . ' (try \';connect\' instead)',
            );
        }

        # Set the offline mode flag, if the switch was specified
        if ($switch) {
            $offlineFlag = TRUE;
        } else {
            $offlineFlag = FALSE;
        }

        # If the user is currently connected to a world, prompt before reconnecting (this only
        #   happens with ';reconnect' and ';xconnect'; with commands like ';quit', the disconnection
        #   happens right away)
        if ($session->status eq 'connecting' || $session->status eq 'connected') {

            if (! $offlineFlag) {
                $msg = 'Are you sure you want to reconnect?',
            } else {
                $msg = 'Are you sure you want to reconnect in offline mode?',
            }

            # Ask the user for permission to save the files
            $choice = $session->mainWin->showMsgDialogue(
                'Reconnect without saving',
                'question',
                $msg,
                'yes-no',
            );

            if ($choice ne 'yes') {

                return $self->complete(
                    $session, $standardCmd,
                    'Reconnection abandoned',
                );

            } else {

                # Prevent files for this session from being saved
                $session->set_disconnectNoSaveFlag(TRUE);
                # Terminate the connection
                $session->doDisconnect(TRUE);
                # React to the disconnection, as if it had been initiated by the host
                $session->reactDisconnect(TRUE);
            }
        }

        # Get the current character (if any)
        if ($session->currentChar) {

            $char = $session->currentChar->name;
        }

        # Get the current world's connection details
        ($host, $port, $char, $pass, $account) = $session->currentWorld->getConnectDetails($char);

        # Start a new GA::Session in a new 'main' window tab
        if (
            ! $axmud::CLIENT->startSession(
                $session->currentWorld->name,
                $host,
                $port,
                $char,
                $pass,
                $account,
                undef,              # Default protocol
                undef,              # No login mode
                $offlineFlag,
            )
        ) {
            return $self->error(
                $session, $inputString,
                'General error reconnecting to \'' . $session->currentWorld->name . '\'',
            );

        } else {

            # (Can't display a confirmation message - the new session has taken over the tab)
            return 1;
        }
    }
}

{ package Games::Axmud::Cmd::Telnet;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('telnet', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['telnet'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Connects to an unnamed world via telnet';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $host, $port,
            $check,
        ) = @_;

        # Local variables
        my $world;

        # Check for improper arguments
        if (! defined $host || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that we don't already have too many sessions open. If the current session is not
        #   connected to a world, then the call to GA::Client->startSession will terminate that
        #   session before creating a new one, so that's ok
        if (
            $axmud::BLIND_MODE_FLAG
            && ($session->status eq 'connecting' || $session->status eq 'connected')
        ) {
            return $self->error(
                $session, $inputString,
                'Can\'t open multiple sessions when ' . $axmud::SCRIPT . ' is running in \'blind\''
                . ' mode',
            );

        } elsif ($axmud::CLIENT->ivPairs('sessionHash') >= $axmud::CLIENT->sessionMax) {

            return $self->error(
                $session, $inputString,
                'Can\'t open a new session (' . $axmud::SCRIPT . ' has reached its limit of '
                . $axmud::CLIENT->sessionMax . ' sessions)',
            );
        }

        # Request a temporary world profile name from the client
        $world = $axmud::CLIENT->getTempProfName();
        if (! $world) {

            # No available temporary profile name (very unlikely)
            return $self->error(
                $session, $inputString,
                'General error setting up the connection',
            );
        }

        # If <port> was not specified, use the generic port
        if (! $port) {

            $port = undef;
        }

        # Start a new GA::Session in a new 'main' window tab
        if (
            ! $axmud::CLIENT->startSession(
                $world,
                $host,
                $port,
                undef,          # No character
                undef,          # No password
                undef,          # No associated account
                'telnet',       # Protocol
                undef,          # No login mode
                FALSE,          # Not offline
                TRUE,           # Temporary profile
            )
        ) {
            return $self->error(
                $session, $inputString,
                'General error connecting to \'' . $world . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Connecting to \'' . $world . '\' (in a different tab)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SSH;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('ssh', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ssh'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Connects to an unnamed world via SSH';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $host, $port,
            $check,
        ) = @_;

        # Local variables
        my $world;

        # Check for improper arguments
        if (! defined $host || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that we don't already have too many sessions open. If the current session is not
        #   connected to a world, then the call to GA::Client->startSession will terminate that
        #   session before creating a new one, so that's ok
        if (
            $axmud::BLIND_MODE_FLAG
            && ($session->status eq 'connecting' || $session->status eq 'connected')
        ) {
            return $self->error(
                $session, $inputString,
                'Can\'t open multiple sessions when ' . $axmud::SCRIPT . ' is running in \'blind\''
                . ' mode',
            );

        } elsif ($axmud::CLIENT->ivPairs('sessionHash') >= $axmud::CLIENT->sessionMax) {

            return $self->error(
                $session, $inputString,
                'Can\'t open a new session (' . $axmud::SCRIPT . ' has reached its limit of '
                . $axmud::CLIENT->sessionMax . ' sessions)',
            );
        }

        # Request a temporary world profile name from the client
        $world = $axmud::CLIENT->getTempProfName();
        if (! $world) {

            # No available temporary profile name (very unlikely)
            return $self->error(
                $session, $inputString,
                'General error setting up the connection',
            );
        }

        # If <port> was not specified, use the generic port
        if (! $port) {

            $port = undef;
        }

        # Start a new GA::Session in a new 'main' window tab
        if (
            ! $axmud::CLIENT->startSession(
                $world,
                $host,
                $port,
                undef,          # No character
                undef,          # No password
                undef,          # No associated account
                'ssh',          # Protocol
                undef,          # No login mode
                FALSE,          # Not offline
                TRUE,           # Temporary profile
            )
        ) {
            return $self->error(
                $session, $inputString,
                'General error connecting to \'' . $world . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Connecting to \'' . $world . '\' (in a different tab)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SSL;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('ssl', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ssl'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Connects to an unnamed world via SSL';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $host, $port,
            $check,
        ) = @_;

        # Local variables
        my $world;

        # Check for improper arguments
        if (! defined $host || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that we don't already have too many sessions open. If the current session is not
        #   connected to a world, then the call to GA::Client->startSession will terminate that
        #   session before creating a new one, so that's ok
        if (
            $axmud::BLIND_MODE_FLAG
            && ($session->status eq 'connecting' || $session->status eq 'connected')
        ) {
            return $self->error(
                $session, $inputString,
                'Can\'t open multiple sessions when ' . $axmud::SCRIPT . ' is running in \'blind\''
                . ' mode',
            );

        } elsif ($axmud::CLIENT->ivPairs('sessionHash') >= $axmud::CLIENT->sessionMax) {

            return $self->error(
                $session, $inputString,
                'Can\'t open a new session (' . $axmud::SCRIPT . ' has reached its limit of '
                . $axmud::CLIENT->sessionMax . ' sessions)',
            );
        }

        # Request a temporary world profile name from the client
        $world = $axmud::CLIENT->getTempProfName();
        if (! $world) {

            # No available temporary profile name (very unlikely)
            return $self->error(
                $session, $inputString,
                'General error setting up the connection',
            );
        }

        # If <port> was not specified, use the generic port
        if (! $port) {

            $port = undef;
        }

        # Start a new GA::Session in a new 'main' window tab
        if (
            ! $axmud::CLIENT->startSession(
                $world,
                $host,
                $port,
                undef,          # No character
                undef,          # No password
                undef,          # No associated account
                'ssl',          # Protocol
                undef,          # No login mode
                FALSE,          # Not offline
                TRUE,           # Temporary profile
            )
        ) {
            return $self->error(
                $session, $inputString,
                'General error connecting to \'' . $world . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Connecting to \'' . $world . '\' (in a different tab)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Login;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('login', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lgn', 'login'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Marks the current character as logged in';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Can't login if the session loop is suspended (because of a Perl error)
        if (! $session->sessionLoopObj) {

            return $self->error(
                $session, $inputString,
                'Can\'t process a login while ' . $axmud::SCRIPT . ' internal processes are'
                . ' suspended (try \';restart\' first)',
            );

        # Check that the character isn't already logged in
        } elsif ($session->loginFlag) {

            return $self->error(
                $session, $inputString,
                'The character is already marked as \'logged in\'',
            );

        } else {

            # Perform the login operation
            if (! $session->doLogin()) {

                return $self->error(
                    $session, $inputString,
                    'Login procedure failed - character not marked as \'logged in\'',
                );

            } else {

                # Don't need to display another message
                return 1;
            }
        }
    }
}

{ package Games::Axmud::Cmd::Quit;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('quit', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['quit'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends \'quit\' and saves files';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $minutes,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Delayed quit
        if ($minutes) {

            if (! $axmud::CLIENT->intCheck($minutes, 1)) {

                return $self->error(
                    $session, $inputString,
                    'The <number> must be a positive integer (in minutes)',
                );

            } else {

                $session->set_delayedQuit('quit', $minutes);

                if ($minutes == 1) {

                    return $self->complete(
                        $session, $standardCmd,
                        'This session will quit in 1 minute',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'This session will quit in ' . $minutes . ' minutes',
                    );
                }
            }

        } else {

            # Regular quit
            return $self->autoQuit(
                $session,
                $inputString,
                $standardCmd,
                'Sent \'quit\' command to the world',
                'Initiated auto-quit sequence',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Qquit;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('qquit', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['qquit'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends \'quit\' but doesn\'t save files';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $minutes,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Delayed quit
        if ($minutes) {

            if (! $axmud::CLIENT->intCheck($minutes, 1)) {

                return $self->error(
                    $session, $inputString,
                    'The <number> must be a positive integer (in minutes)',
                );

            } else {

                $session->set_delayedQuit('qquit', $minutes);

                if ($minutes == 1) {

                    return $self->complete(
                        $session, $standardCmd,
                        'This session will quit (without saving) in 1 minute',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'This session will quit (without saving) in ' . $minutes . ' minutes',
                    );
                }
            }

        } else {

            # Prevent files for this session from being saved while the disconnection is processed
            $session->set_disconnectNoSaveFlag(TRUE);

            return $self->autoQuit(
                $session,
                $inputString,
                $standardCmd,
                'Sent \'quit\' command to the world; not saving files',
                'Initiated auto-quit sequence; not saving files',
            );
        }
    }
}

{ package Games::Axmud::Cmd::QuitAll;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('quitall', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['qal', 'quitall'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends \'quit\' to every world and saves files';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $minutes,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Delayed quit
        if ($minutes) {

            if (! $axmud::CLIENT->intCheck($minutes, 1)) {

                return $self->error(
                    $session, $inputString,
                    'The <number> must be a positive integer (in minutes)',
                );

            } else {

                foreach my $otherSession ($axmud::CLIENT->listSessions()) {

                    $otherSession->set_delayedQuit('quit', $minutes);
                }

                if ($minutes == 1) {

                    return $self->complete(
                        $session, $standardCmd,
                        'All sessions will quit in 1 minute',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'All session will quit in ' . $minutes . ' minutes',
                    );
                }
            }

        } else {

            # Send the 'quit' command to every world, except this one (doing it this way prevents
            #   this command generating two calls to $self->complete in this session)
            $axmud::CLIENT->broadcastInstruct(';quit', $session);

            return $self->autoQuit(
                $session,
                $inputString,
                $standardCmd,
                'Sent \'quit\' command to every world',
                'Initiated auto-quit sequence in every world',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Exit;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('exit', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['exit'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Terminates the connection and saves files';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $minutes,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Delayed quit
        if ($minutes) {

            if (! $axmud::CLIENT->intCheck($minutes, 1)) {

                return $self->error(
                    $session, $inputString,
                    'The <number> must be a positive integer (in minutes)',
                );

            } else {

                $session->set_delayedQuit('exit', $minutes);

                if ($minutes == 1) {

                    return $self->complete(
                        $session, $standardCmd,
                        'This session will exit in 1 minute',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'This session will exit in ' . $minutes . ' minutes',
                    );
                }
            }

        } else {

            # Terminate the connection
            $session->doDisconnect(TRUE);
            # React to the disconnection, as if it had been initiated by the host
            $session->reactDisconnect(TRUE);

            return $self->complete(
                $session, $standardCmd,
                'Disconnected from \'' . $session->currentWorld->name . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Xxit;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('xxit', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['xxit'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Terminates the connection and doesn\'t save files';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $minutes,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Delayed quit
        if ($minutes) {

            if (! $axmud::CLIENT->intCheck($minutes, 1)) {

                return $self->error(
                    $session, $inputString,
                    'The <number> must be a positive integer (in minutes)',
                );

            } else {

                $session->set_delayedQuit('xxit', $minutes);

                if ($minutes == 1) {

                    return $self->complete(
                        $session, $standardCmd,
                        'This session will exit (without saving) in 1 minute',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'This session will exit (without saving) in ' . $minutes . ' minutes',
                    );
                }
            }

        } else {

            # Prevent files for this session from being saved
            $session->set_disconnectNoSaveFlag(TRUE);

            # Terminate the connection
            $session->doDisconnect(TRUE);
            # React to the disconnection, as if it had been initiated by the host
            $session->reactDisconnect(TRUE);

            return $self->complete(
                $session, $standardCmd,
                'Disconnected from \'' . $session->currentWorld->name . '\', files not saved',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ExitAll;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('exitall', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['eal', 'exitall'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Terminates every connection and saves files';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $minutes,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Delayed quit
        if ($minutes) {

            if (! $axmud::CLIENT->intCheck($minutes, 1)) {

                return $self->error(
                    $session, $inputString,
                    'The <number> must be a positive integer (in minutes)',
                );

            } else {

                foreach my $otherSession ($axmud::CLIENT->listSessions()) {

                    $otherSession->set_delayedQuit('exit', $minutes);
                }

                if ($minutes == 1) {

                    return $self->complete(
                        $session, $standardCmd,
                        'All sessions will exit in 1 minute',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'All session will exit in ' . $minutes . ' minutes',
                    );
                }
            }

        } else {

            # Terminate the connection in every world, except this one (doing it this way prevents
            #   this command generating two calls to $self->complete in this session)
            $axmud::CLIENT->broadcastInstruct(';exit', $session);
            # Terminate this connection
            $session->doDisconnect(TRUE);
            # React to the disconnection, as if it had been initiated by the host
            $session->reactDisconnect(TRUE);

            return $self->complete(
                $session, $standardCmd,
                'Terminated every connection',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AbortSelfDestruct;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('abortselfdestruct', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['asd', 'abortquit', 'abortexit', 'abortselfdestruct'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Aborts delayed quits/exits in all sessions';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $abortCount;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        $abortCount = 0;
        foreach my $otherSession ($axmud::CLIENT->listSessions()) {

            if (defined $otherSession->delayedQuitTime) {

                $abortCount++;
                $otherSession->reset_delayedQuit();
            }
        }

        return $self->complete(
            $session, $standardCmd,
            'Abort self-destruct: total sessions: ' . $axmud::CLIENT->ivPairs('sessionHash')
            . ', delayed quits/exits aborted: ' . $abortCount,
        );
    }
}

{ package Games::Axmud::Cmd::StopSession;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('stopsession', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ssn', 'closetab', 'stopsession'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Stops a session and closes its tab';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my ($closeSession, $fileCount, $result);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If a session was specified, check it exists
        if (! defined $number) {

            $closeSession = $session;

        } else {

            $closeSession = $axmud::CLIENT->ivShow('sessionHash', $number);
            if (! $closeSession) {

                return $self->error(
                    $session, $inputString,
                    'Session #' . $number . ' doesn\'t exist',
                );
            }
        }

        # See if there are any file objects in the session which need to be saved
        $fileCount = 0;
        foreach my $fileObj ($axmud::CLIENT->ivValues('fileObjHash')) {

            if ($fileObj->modifyFlag) {

                $fileCount++;
            }
        }

        foreach my $fileObj ($closeSession->ivValues('sessionFileObjHash')) {

            if ($fileObj->modifyFlag) {

                $fileCount++;
            }
        }

        if ($fileCount && ($axmud::CLIENT->saveConfigFlag || $axmud::CLIENT->saveDataFlag)) {

            # Ask the user for permission to save the files
            $result = $closeSession->mainWin->showMsgDialogue(
                'Save files',
                'question',
                'Do you want to save files before closing this session? (unsaved files: '
                . $fileCount . ')',
                'yes-no',
            );

            if ($result eq 'delete-event') {

                # User closed the 'dialogue' window, without clicking on either the 'yes' or 'no
                #   buttons
                return $self->complete(
                    $session, $standardCmd,
                    'Stop session operation cancelled',
                );

            } elsif ($result eq 'yes') {

                # Save files in this session
                $closeSession->pseudoCmd('save', 'show_all');
            }
        }

        # Terminate the session
        $axmud::CLIENT->stopSession($closeSession);

        if ($session eq $closeSession) {

            # (The $self->complete message would never be seen, so just return 1)
            return 1;

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Session #' . $closeSession->number . ' terminated',
            );
        }
    }
}

{ package Games::Axmud::Cmd::StopClient;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('stopclient', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['stc', 'stopclient'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Stops the client and saves files';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $fileCount, $sessionCount, $result,
            %hash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # See if there are any file objects (in any session) which need to be saved. Count the
        #   unsaved file objects by compiling a hash (because some sessions will have share file
        #   objects; compiling a hash eliminates duplicates)
        foreach my $fileObj ($axmud::CLIENT->ivValues('fileObjHash')) {

            if ($fileObj->modifyFlag) {

                $hash{$fileObj} = undef;
            }
        }

        foreach my $otherSession ($axmud::CLIENT->listSessions()) {

            foreach my $fileObj ($otherSession->ivValues('sessionFileObjHash')) {

                if ($fileObj->modifyFlag) {

                    $hash{$fileObj} = undef;
                }
            }
        }

        $fileCount = scalar (keys %hash);
        $sessionCount = $axmud::CLIENT->ivPairs('sessionHash');

        if ($fileCount && ($axmud::CLIENT->saveConfigFlag || $axmud::CLIENT->saveDataFlag)) {

            # Ask the user for permission to save the files
            $result = $session->mainWin->showMsgDialogue(
                'Save files',
                'question',
                'Do you want to save files before stopping ' . $axmud::SCRIPT . '? (Sessions: '
                . $sessionCount . ', unsaved files: ' . $fileCount . ')',
                'yes-no',
            );

            if ($result eq 'delete-event') {

                # User closed the 'dialogue' window, without clicking on either the 'yes' or 'no
                #   buttons
                return $self->complete(
                    $session, $standardCmd,
                    $axmud::SCRIPT . ' shutdown cancelled',
                );

            } elsif ($result eq 'yes') {

                # Save files in every session
                $axmud::CLIENT->broadcastInstruct(';save');
            }
        }

        # Terminate the client
        $axmud::CLIENT->stop();

        # (The $self->complete message would never be seen, so just return 1)
        return 1;
    }
}

{ package Games::Axmud::Cmd::Panic;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('panic', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['boss', 'panic'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Stops the client and doesn\'t save files';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Terminate the client without saving files
        $axmud::CLIENT->stop();

        # (The $self->complete message would never be seen, so just return 1)
        return 1;
    }
}

{ package Games::Axmud::Cmd::AwayFromKeys;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('awayfromkeys', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['afk', 'awayfromkeys'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets an alert for when the world sends text';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that a valid switch was used. Since the user is probably eager to leave their
        #   keyboard, in this case we'll explain which switch to use
        if (defined $switch && $switch ne '-v' && $switch ne '-s') {

            return $self->error(
                $session, $inputString,
                'Invalid switch \'' . $switch . '\' - use -v for a visual alert, -s for a sound'
                . ' alert, or use no switch at all for both',
            );
        }

        # ;afk
        # ;afk -v
        if (! $switch || $switch eq '-v') {

            $axmud::CLIENT->set_tempUrgencyFlag(TRUE);
        }

        # ;afk
        # ;afk -s
        if (! $switch || $switch eq '-s') {

            $axmud::CLIENT->set_tempSoundFlag(TRUE);
            if (! $axmud::CLIENT->allowSoundFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    '\'Away from keys\' alert set, but sound is currently off (use \';sound on\''
                    . ' to turn it on)',
                );
            }
        }

        # Operation complete
        return $self->complete($session, $standardCmd, '\'Away from keys\' alert set');
    }
}

{ package Games::Axmud::Cmd::SetReminder;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setreminder', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rmd', 'remind', 'setremind', 'setreminder'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets an alert for some time in the future';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($switch, $visualFlag, $soundFlag, $flagCount, $minutes, $response);

        # Extract switches
        $flagCount = 0;

        ($switch, @args) = $self->extract('-v', 0, @args);
        if (defined $switch) {

            $visualFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-s', 0, @args);
        if (defined $switch) {

            $soundFlag = TRUE;
            $flagCount++;
        }

        # Exactly one argument should be left
        $minutes = shift @args;
        if (! defined $minutes || @args) {

            return $self->improper($session, $inputString);
        }

        # Check the number is valid (any number above 0)
        if (! $axmud::CLIENT->floatCheck($minutes) || $minutes <= 0) {

            return $self->error(
                $session, $inputString,
                'Invalid time \'' . $minutes . '\' - you must specify a time in minutes, (mininum'
                . ' 1 minute)',
            );
        }

        # Create a timer that fires once
        if (! $flagCount || $flagCount == 2) {

            $response = ';quicksoundeffect alert -f';

        } elsif ($visualFlag) {

            $response = ';flashwindow';

        } else {

            $response = ';quicksoundeffect alert -f';
        }

        if (
            ! $session->createIndependentInterface(
                'timer',
                ($minutes * 60),
                $response,
                # Arguments
                'count'     => 1,
                'temporary' => 1,
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not create reminder (internal error)',
            );

        } elsif (
            ! $axmud::CLIENT->allowSoundFlag
            && (! $flagCount || $flagCount == 2)
        ) {
            return $self->complete(
                $session, $standardCmd,
                'Reminder alert set, but sound is currently off (use \';sound on\' to turn it'
                . ' on)',
            );

        } elsif ($minutes == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Reminder alert set for 1 minute from now',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Reminder alert set for ' . $minutes . ' minutes from now',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetCountdown;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setcountdown', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['scd', 'countdown', 'setcountdown'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets a countdown';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Start a task, or instruct an existing one
        return $self->countDownUp($session, $inputString, $standardCmd, 'down', @args);
    }
}

{ package Games::Axmud::Cmd::SetCountup;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setcountup', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['scu', 'countup', 'setcountup'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets a countdown, counting up from zero';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Start a task, or instruct an existing one
        return $self->countDownUp($session, $inputString, $standardCmd, 'up', @args);
    }
}

{ package Games::Axmud::Cmd::SetLookup;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setlookup', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['slu', 'setlu', 'setlookup'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the list of IP lookup servers';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @list,
        ) = @_;

        # Local variables
        my $urlRegex;

        # (No improper arguments to check)

        # ;slu
        if (! @list) {

            # Reset the list of IP lookup servers
            $axmud::CLIENT->set_ipLookupList();
            # Also reset the stored IP, as if it had never been fetched
            $axmud::CLIENT->reset_currentIP();

            return $self->complete($session, $standardCmd, 'IP lookup server list reset');

        # ;slu <list>
        } else {

            # Show an error if any of the URLs aren't valid
            $urlRegex = $axmud::CLIENT->constUrlRegex;
            foreach my $url (@list) {

                if (! ($url =~ m/$urlRegex/i)) {

                    return $self->error(
                        $session, $inputString,
                        'Cannot update IP lookup server list because of an invalid URL: ' . $url,
                    );
                }
            }

            # Set the list of IP lookup servers
            $axmud::CLIENT->set_ipLookupList(@list);

            return $self->complete($session, $standardCmd, 'IP lookup server list set');
        }
    }
}

{ package Games::Axmud::Cmd::ResetLookup;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resetlookup', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rlu', 'resetlu', 'resetlookup'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets the list of IP lookup servers';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Reset the IP lookup server list to its default state
        $axmud::CLIENT->set_ipLookupList($axmud::CLIENT->constIPLookupList);

        return $self->complete(
            $session, $standardCmd,
            'IP lookup server list reset to its default state',
        );
    }
}

{ package Games::Axmud::Cmd::ForceLookup;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('forcelookup', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['flu', 'forcelu', 'forcelookup'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Re-fetches the user\'s IP address';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $ip;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Re-fetch the user's IP. The TRUE argument forces the function to contact IP lookup servers
        #   and to ignore the IP fetched by any previous call to that function
        $ip = $axmud::CLIENT->ipv4Get(TRUE);
        if (! defined $ip) {

            return $self->complete(
                $session, $standardCmd,
                'Could not fetch your IP address (see the help for \';setlookup\')',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Your IP address is now: ' . $ip,
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListLookup;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listlookup', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['llu', 'listlu', 'listlookup'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows the list of IP lookup servers';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my ($ip, $count);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Display header
        $session->writeText('List of IP lookup servers');

        # Display list
        if (! $axmud::CLIENT->ipLookupList) {

            $session->writeText('   <none>');

        } else {

            foreach my $url ($axmud::CLIENT->ipLookupList) {

                $session->writeText('   ' . $url);
            }
        }

        # Display header
        $session->writeText('Default list of IP lookup servers');

        # Display list
        if (! $axmud::CLIENT->constIPLookupList) {

            $session->writeText('   <none>');

        } else {

            foreach my $url ($axmud::CLIENT->constIPLookupList) {

                $session->writeText('   ' . $url);
            }
        }

        # Display header
        $session->writeText('Your IP address:');
        $ip = $axmud::CLIENT->ipv4Get();
        if (! defined $ip) {
            $session->writeText('   <not known>');
        } else {
            $session->writeText('   ' . $ip);
        }

        # Display footer
        $count = scalar ($axmud::CLIENT->ipLookupList);
        if ($count == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 server found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . $count . ' servers found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetCharSet;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setcharset', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['scs', 'setcharset'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Specifies the character set to use';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $charSet,
            $check,
        ) = @_;

        # Local variables
        my ($matchFlag, $updateFlag);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;scs
        if (! $switch) {

            # Display list
            $session->writeText($axmud::SCRIPT . ' default character set:');
            $session->writeText('   ' . $axmud::CLIENT->charSet);

            $session->writeText('Current world\'s character set (overrides the default set):');
            if ($session->currentWorld->worldCharSet) {
                $session->writeText('   ' . $session->currentWorld->worldCharSet);
            } else {
                $session->writeText('   <none>');
            }

            $session->writeText('Character set used by this session:');
            $session->writeText('   ' . $session->sessionCharSet);

            $session->writeText(' ');
            $session->writeText('Available character sets:');
            $session->writeText(join(' ', $axmud::CLIENT->charSetList));
            $session->writeText(' ');

            return $self->complete($session, $standardCmd, 'Character sets information displayed');

        # ;scs -d <charset>
        # ;scs -w <charset>
        # ;scs -w
        } elsif ($switch eq '-d' || $switch eq '-w') {

            if ($charSet) {

                # Check that <charset> is available
                OUTER: foreach my $item ($axmud::CLIENT->charSetList) {

                    if ($item eq $charSet) {

                        $matchFlag = TRUE;
                        last OUTER;
                    }
                }

                if (! $matchFlag) {

                    return $self->error(
                        $session, $inputString,
                        'Unrecognised character set \'' . $charSet . '\' (try \';setcharset\' for a'
                        . ' list of available sets',
                    );
                }
            }

            # ;scs -d <charset>
            if ($switch eq '-d') {

                if (! $charSet) {

                    return $self->error(
                        $session, $inputString,
                        'Please specify a character set, e.g. \';setcharset -d iso-8859-1\'',
                    );
                }

                $axmud::CLIENT->set_charSet($charSet);
                # Update every session, if it is not using its current world's character set
                foreach my $otherSession ($axmud::CLIENT->listSessions()) {

                    if (! $otherSession->currentWorld->worldCharSet) {

                        $otherSession->setCharSet();

                        # (Display a different confirmation message, if this session was updated)
                        if ($otherSession eq $session) {

                            $updateFlag = TRUE;
                        }
                    }
                }

                if ($updateFlag) {

                    return $self->complete(
                        $session, $standardCmd,
                        $axmud::SCRIPT . ' default character set is now \'' . $charSet . '\' (and'
                        . ' this session has been updated)',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        $axmud::SCRIPT . ' default character set is now \'' . $charSet . '\' (but'
                        . ' this session is using the current world\'s character set)',
                    );
                }

            # ;scs -w <charset>
            # ;sws -w
            } else {

                if (defined $charSet) {
                    $session->currentWorld->ivPoke('worldCharSet', $charSet);
                } else {
                    $session->currentWorld->ivUndef('worldCharSet');
                }

                # Update every session using the current world as this one
                foreach my $otherSession ($axmud::CLIENT->listSessions()) {

                    if ($otherSession->currentWorld eq $session->currentWorld) {

                        $otherSession->setCharSet();
                    }
                }

                if (defined $charSet) {

                    return $self->complete(
                        $session, $standardCmd,
                        'Current world\'s character set has been set to \'' . $charSet . '\'',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'Current world\'s character set has been reset (this session is now using'
                        . ' \'' . $session->sessionCharSet . '\')',
                    );
                }
            }

        } else {

            return $self->error(
                $session, $inputString,
                'Unrecognised switch \'' . $switch . '\' - try \'-d\' or \'-w\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetCustomMonth;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setcustommonth', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['smo', 'setmonth', 'setcustommonth'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets customised months of the year';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @monthList,
        ) = @_;

        # Local variables
        my %checkHash;

        # (No improper arguments to check)

        # ;smo
        if (! @monthList) {

            # Display list
            $session->writeText('Lists of custom strings used for months in the year');

            # Display list
            $session->writeText('   Custom  : ' . join(' ', $axmud::CLIENT->customMonthList));
            $session->writeText('   Default : ' . join(' ', $axmud::CLIENT->constMonthList));

            # Display footer
            return $self->complete($session, $standardCmd, 'End of lists');

        # ;smo -r
        } elsif ($monthList[0] eq '-r') {

            # In case the user tries to use -r followed by a list of custom months, complain
            if ((scalar @monthList) > 1) {

                return $self->error(
                    $session, $inputString,
                    'The \'-r\' switch can\'t be combined with other arguments',
                );

            } else {

                $axmud::CLIENT->reset_customMonthList();

                return $self->complete(
                    $session, $standardCmd,
                    $axmud::SCRIPT . '\' custom list of months has been reset',
                );
            }

        # ;smo <jan> <feb> <mar...>
        } else {

            if ((scalar @monthList) < 12) {

                return $self->error(
                    $session, $inputString,
                    'If you change the custom list of months, you must specify exactly twelve'
                    . ' strings',
                );
            }

            # Check each string is valid, and check for duplicates at the same time
            foreach my $month (@monthList) {

                if (! $axmud::CLIENT->nameCheck($month, 16)) {

                    return $self->error(
                        $session, $inputString,
                        'Invalid month string \'' . $month . '\' (use A-Z, a-z, 0-9 or underlines;'
                        . ' first character must be a letter, non-Latin alphabets acceptable, max'
                        . ' 16 characters)',
                    );

                } elsif (exists $checkHash{$month}) {

                    return $self->error(
                        $session, $inputString,
                        'Duplicate month \'' . $month . '\' - each month must be unique',
                    );

                } else {

                    $checkHash{$month} = undef;
                }
            }

            # Set the list
            $axmud::CLIENT->set_customMonthList(@monthList);

            return $self->complete(
                $session, $standardCmd,
                $axmud::SCRIPT . '\' custom list of months set to: '
                . join(' ', @monthList),
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetCustomWeek;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setcustomweek', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['scw', 'setday', 'setweek', 'setcustomweek'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets customised days of the week';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @dayList,
        ) = @_;

        # Local variables
        my %checkHash;

        # (No improper arguments to check)

        # ;scw
        if (! @dayList) {

            # Display list
            $session->writeText('Lists of custom strings used for days of the week');

            # Display list
            $session->writeText('   Custom  : ' . join(' ', $axmud::CLIENT->customDayList));
            $session->writeText('   Default : ' . join(' ', $axmud::CLIENT->constDayList));

            # Display footer
            return $self->complete($session, $standardCmd, 'End of lists');

        # ;scw -r
        } elsif ($dayList[0] eq '-r') {

            # In case the user tries to use -r followed by a list of custom days, complain
            if ((scalar @dayList) > 1) {

                return $self->error(
                    $session, $inputString,
                    'The \'-r\' switch can\'t be combined with other arguments',
                );

            } else {

                $axmud::CLIENT->reset_customDayList();

                return $self->complete(
                    $session, $standardCmd,
                    $axmud::SCRIPT . '\' custom list of days of the week has been reset',
                );
            }

        # ;scw <mon> <tue> <wed...>
        } else {

            if ((scalar @dayList) < 7) {

                return $self->error(
                    $session, $inputString,
                    'If you change the custom list of days, you must specify exactly seven strings',
                );
            }

            # Check each string is valid
            foreach my $day (@dayList) {

                if (! $axmud::CLIENT->nameCheck($day, 16)) {

                    return $self->error(
                        $session, $inputString,
                        'Invalid day string \'' . $day . '\' (use A-Z, a-z, 0-9 or underlines;'
                        . ' first character must be a letter, non-Latin alphabets acceptable, max'
                        . ' 16 characters)',
                    );

                } elsif (exists $checkHash{$day}) {

                    return $self->error(
                        $session, $inputString,
                        'Duplicate day \'' . $day . '\' - each day must be unique',
                    );

                } else {

                    $checkHash{$day} = undef;
                }
            }

            # Set the list
            $axmud::CLIENT->set_customDayList(@dayList);

            return $self->complete(
                $session, $standardCmd,
                $axmud::SCRIPT . '\' custom list of days of the week set to: '
                . join(' ', @dayList),
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetCommifyMode;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setcommifymode', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['scf', 'commify', 'setcommifymode'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Customises conversion of long numbers';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my $msg;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;scf
        if (! $switch) {

            $msg = 'Current commification mode: \'';

            if ($axmud::CLIENT->commifyMode eq 'comma') {
                $msg .= 'comma\' - add commas, e.g. 1,000,000';
            } elsif ($axmud::CLIENT->commifyMode eq 'europe') {
                $msg .= 'europe\' - add full stops/periods, e.g. 1.000.000';
            } elsif ($axmud::CLIENT->commifyMode eq 'europe') {
                $msg .= 'brit\' - add spaces, e.g. 1 000 000';
            } elsif ($axmud::CLIENT->commifyMode eq 'europe') {
                $msg .= 'underline\' - add underlines/undescores, e.g. 1_000_000';
            } else {
                $msg .= 'none\' - don\'t commify large numbers';
            }

            return $self->complete($session, $standardCmd, $msg);

        # ;scf -c
        } elsif ($switch eq '-c') {

            $axmud::CLIENT->set_commifyMode('comma');

            return $self->complete(
                $session, $standardCmd,
                'Commification mode set to \'comma\' - add commas, e.g. 1,000,000',
            );

        # ;scf -e
        } elsif ($switch eq '-e') {

            $axmud::CLIENT->set_commifyMode('europe');

            return $self->complete(
                $session, $standardCmd,
                'Commification mode set to \'comma\' -  add full stops/periods, e.g. 1.000.000',
            );

        # ;scf -b
        } elsif ($switch eq '-b') {

            $axmud::CLIENT->set_commifyMode('brit');

            return $self->complete(
                $session, $standardCmd,
                'Commification mode set to \'comma\' - add spaces, e.g. 1 000 000',
            );

        # ;scf -u
        } elsif ($switch eq '-u') {

            $axmud::CLIENT->set_commifyMode('underline');

            return $self->complete(
                $session, $standardCmd,
                'Commification mode set to \'comma\' - add commas, e.g. 1,000,000',
            );

        # ;scf -n
        } elsif ($switch eq '-n') {

            $axmud::CLIENT->set_commifyMode('none');

            return $self->complete(
                $session, $standardCmd,
                'Commification mode set to \'comma\' - add underlines/undescores, e.g. 1_000_000',
            );

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid switch (try c, -e, -b, -u or -n)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetApplication;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setapplication', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sap', 'setapp', 'setapplication'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets external application commands';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $cmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (
            (
                defined $switch && $switch ne '-b' && $switch ne '-e' && $switch ne '-a'
                && $switch ne '-t'
            ) || defined $check
        ) {
            return $self->improper($session, $inputString);
        }

        # ;sap
        if (! $switch) {

            # Display header
            $session->writeText('Commands used to start external applications');
            # Display list
            if ($axmud::CLIENT->browserCmd) {
                $session->writeText('   Web browser:       ' . $axmud::CLIENT->browserCmd);
            } else {
                $session->writeText('   Web browser:       <not set>');
            }

            if ($axmud::CLIENT->emailCmd) {
                $session->writeText('   Email application: ' . $axmud::CLIENT->emailCmd);
            } else {
                $session->writeText('   Email application: <not set>');
            }

            if ($axmud::CLIENT->audioCmd) {
                $session->writeText('   Audio player:      ' . $axmud::CLIENT->audioCmd);
            } else {
                $session->writeText('   Audio player:      <not set>');
            }

            if ($axmud::CLIENT->textEditCmd) {
                $session->writeText('   Text editor:       ' . $axmud::CLIENT->textEditCmd);
            } else {
                $session->writeText('   Text editor:       <not set>');
            }

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of list (4 external applications found)',
            );

        # ;sap -b <cmd>
        } elsif ($switch eq '-b') {

            if (! $cmd) {

                $axmud::CLIENT->set_browserCmd('');

                return $self->complete(
                    $session, $standardCmd,
                    'Command to open external web browser reset',
                );

            } else {

                $axmud::CLIENT->set_browserCmd($cmd);

                return $self->complete(
                    $session, $standardCmd,
                    'Command to open external web browser set to \'' . $cmd . '\'',
                );
            }

        # ;sap -e <cmd>
        } elsif ($switch eq '-e') {

            if (! $cmd) {

                $axmud::CLIENT->set_emailCmd('');

                return $self->complete(
                    $session, $standardCmd,
                    'Command to open external email application reset',
                );

            } else {

                $axmud::CLIENT->set_emailCmd($cmd);

                return $self->complete(
                    $session, $standardCmd,
                    'Command to open external email application set to \'' . $cmd . '\'',
                );
            }

        # ;sap -a <cmd>
        } elsif ($switch eq '-a') {

            if (! $cmd) {

                $axmud::CLIENT->set_audioCmd('');

                return $self->complete(
                    $session, $standardCmd,
                    'Command to open external audio player reset',
                );

            } else {

                $axmud::CLIENT->set_audioCmd($cmd);

                return $self->complete(
                    $session, $standardCmd,
                    'Command to open external audio player set to \'' . $cmd . '\'',
                );
            }

        # ;sap -t <cmd>
        } elsif ($switch eq '-t') {

            if (! $cmd) {

                $axmud::CLIENT->set_textEditCmd('');

                return $self->complete(
                    $session, $standardCmd,
                    'Command to open external text editor reset',
                );

            } else {

                $axmud::CLIENT->set_textEditCmd($cmd);

                return $self->complete(
                    $session, $standardCmd,
                    'Command to open external text editor set to \'' . $cmd . '\'',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ResetApplication;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resetapplication', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rap', 'resetapp', 'resetapplication'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets external application commands';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $string,
            @cmdList,
        );

        # Check for improper arguments
        if (
            (defined $switch && $switch ne '-l' && $switch ne '-w')
            || defined $check
        ) {
            return $self->improper($session, $inputString);
        }

        # ;rap
        if ((! $switch && $^O eq 'linux') || $switch eq '-l') {

            $string = 'Linux';
            @cmdList = $axmud::CLIENT->constLinuxCmdList;

        } elsif ((! $switch && $^O eq 'MSWin32') || $switch eq '-w') {

            $string = 'MS Windows';
            @cmdList = $axmud::CLIENT->constMSWinCmdList;

        } elsif ((! $switch && $^O =~ m/bsd/) || $switch eq '-b') {

            $string = '*BSD';
            @cmdList = $axmud::CLIENT->constBSDCmdList;
        }

        # Very unlikely error, but better to be safe than sorry...
        if (! @cmdList) {

            return $self->error(
                $session, $inputString,
                'Can\'t reset external applications - general error',
            );

        } else {

            $axmud::CLIENT->set_browserCmd(shift @cmdList);
            $axmud::CLIENT->set_emailCmd(shift @cmdList);
            $axmud::CLIENT->set_audioCmd(shift @cmdList);
            $axmud::CLIENT->set_textEditCmd(shift @cmdList);

            return $self->complete(
                $session, $standardCmd,
                'External application commands reset for ' . $string,
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetPromptDelay;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setpromptdelay', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['spd', 'setdelay', 'setpromptdelay'],
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets a system prompt delay time';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $interval,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $switch  || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;sdl -p <interval>
        if ($switch eq '-p') {

            if (! defined $interval) {

                $axmud::CLIENT->set_promptWaitTime($axmud::CLIENT->constPromptWaitTime);

                return $self->complete(
                    $session, $standardCmd,
                    'Prompt delay set to ' . $axmud::CLIENT->promptWaitTime . ' seconds',
                );

            } elsif (! $axmud::CLIENT->floatCheck($interval, 0.1, 5)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid prompt delay time, must be a value in the range 0.1 - 5 seconds',
                );

            } else {

                $axmud::CLIENT->set_promptWaitTime($interval);

                return $self->complete(
                    $session, $standardCmd,
                    'Prompt delay set to ' . $axmud::CLIENT->promptWaitTime . ' seconds',
                );
            }

        # ;sdl -l <interval>
        } elsif ($switch eq '-l') {

            if (! defined $interval) {

                $axmud::CLIENT->set_loginWarningTime($axmud::CLIENT->constLoginWarningTime);

                return $self->complete(
                    $session, $standardCmd,
                    'Login warning delay set to ' . $axmud::CLIENT->loginWarningTime . ' seconds',
                );

            } elsif (! $axmud::CLIENT->floatCheck($interval, 0)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid login warning delay time, must be an integer in seconds (use 0 for'
                    . ' \'immediately\')',
                );

            } else {

                $axmud::CLIENT->set_loginWarningTime($interval);

                return $self->complete(
                    $session, $standardCmd,
                    'Login warning delay set to ' . $axmud::CLIENT->loginWarningTime . ' seconds',
                );
            }

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid switch \'' . $switch . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Repeat;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('repeat', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rp', 'repeat'],
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends a world command multiple times';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($number, $cmd);

        # Check for improper arguments
        if (@args < 2) {

            return $self->improper($session, $inputString);
        }

        # Check that <number> is valid
        $number = shift @args;
        if (! $axmud::CLIENT->intCheck($number, 1)) {

            return $self->error($session, $inputString, 'Invalid number \'' . $number . '\'');
        }

        # Combine the remaining arguments in a single string
        $cmd = join (' ', @args);

        # Send the command
        for (my $count = 0; $count < $number; $count++) {

            $session->worldCmd($cmd);
        }

        if ($number == 1) {
            return $self->complete($session, $standardCmd, 'Command sent 1 time');
        } else {
            return $self->complete($session, $standardCmd, 'Command sent ' . $number . ' times');
        }
    }
}

{ package Games::Axmud::Cmd::IntervalRepeat;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('intervalrepeat', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['irp', 'intrep', 'intervalrepeat'],
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends a world command multiple times at intervals';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($number, $time, $cmd, $obj);

        # Check for improper arguments
        if (@args < 3) {

            return $self->improper($session, $inputString);
        }

        # Check that <number> and <time> are valid
        $number = shift @args;
        $time = shift @args;

        if (! $axmud::CLIENT->intCheck($number, 1)) {
            return $self->error($session, $inputString, 'Invalid number \'' . $number . '\'');
        } elsif (! $axmud::CLIENT->intCheck($time, 1)) {
            return $self->error($session, $inputString, 'Invalid time interval \'' . $time . '\'');
        }

        # Combine the remaining arguments in a single string
        $cmd = join (' ', @args);

        # Create a GA::Obj::Repeat to send the command the specified number of times
        $obj = Games::Axmud::Obj::Repeat->new($session, $cmd, $number, $time);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'General error setting up the repeating command,
            ');

        } else {

            # Add the repeat object to the session's list, so that it can be checked on every spin
            #   of the task loop
            $session->add_repeatObj($obj);

            return $self->complete(
                $session, $standardCmd,
                'Repeating command created, times to send: ' . $number . ', interval: ' . $time
                . ' seconds',
            );
        }
    }
}

{ package Games::Axmud::Cmd::StopCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('stopcommand', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['stop', 'stopcmd', 'stopcommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Stops all repeating / excess commands';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # (Don't check for improper arguments - the user might have to type this command quickly)

        # Clear repeating commands
        $session->ivEmpty('repeatObjList');
        # Clear excess commands
        $session->ivEmpty('excessCmdList');

        return $self->complete(
            $session, $standardCmd,
            'Repeating / excess commands stopped',
        );
    }
}

{ package Games::Axmud::Cmd::RedirectMode;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('redirectmode', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rdm', 'redirect', 'redirectmode'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Turns redirect mode on/off';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $string,
            $check,
        ) = @_;

        # Local variables
        my $msg;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;rdm <string>
        if ($string) {

            # Check that <string> contains at least one '@' character and, if not, show a warning
            #   (but still go into redirect mode)
            if (! ($string =~ m/@/)) {

                return $self->error(
                    $session, $inputString,
                    'The redirect string \'' . $string . '\' must contain an \'@\' character',
                );
            }

            # Turn on redirect mode by setting the session's redirect string
            $session->set_redirectString($string);

            # Compose the confirmation message
            if ($session->redirectMode eq 'primary_only') {
                $msg .= ' (redirecting primary directions only)';
            } elsif ($session->redirectMode eq 'primary_secondary') {
                $msg .= ' (redirecting both primary and secondary directions)';
            } elsif ($session->redirectMode eq 'all_exits') {
                $msg .= ' (redirecting all direction commands)';
            }

            return $self->complete(
                $session, $standardCmd,
                'Redirect mode turned on using the string \'' . $string . '\'' . $msg,
            );

        # ;rdm
        } else {

            # Turn off redirect mode by setting the session's redirect string
            $session->set_redirectString();

            return $self->complete(
                $session, $standardCmd,
                'Redirect mode turned off',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetRedirectMode;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setredirectmode', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['srd', 'setredirect', 'setredirectmode'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Fine-tunes redirect mode';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my $msg;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;srd
        if (! $switch) {

            # Display the current redirect mode settings
            $msg = 'Redirect mode is ';

            if ($session->redirectString) {
                $msg .= 'on and set to';
            } else {
                $msg .= 'off, but set to';
            }

            if ($session->redirectMode eq 'primary_only') {
                $msg .= ' redirect primary directions only';
            } elsif ($session->redirectMode eq 'primary_secondary') {
                $msg .= ' redirect primary/secondary/relative directions';
            } else {
                $msg .= ' redirect all direction commands';
            }

            return $self->complete($session, $standardCmd, $msg);

        # ;srd -p
        } elsif ($switch eq '-p') {

            $session->set_redirectMode('primary_only');

            return $self->complete(
                $session, $standardCmd,
                'Redirect mode now redirecting primary directions only',
            );

        # ;srd -b
        } elsif ($switch eq '-b') {

            $session->set_redirectMode('primary_secondary');

            return $self->complete(
                $session, $standardCmd,
                'Redirect mode now redirecting primary/secondary/relative directions',
            );

        # ;srd -a
        } elsif ($switch eq '-a') {

            $session->set_redirectMode('all_exits');

            return $self->complete(
                $session, $standardCmd,
                'Redirect mode now redirecting all direction commands',
            );

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid switch - try \'-p\', \'-b\' or \'-a\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ToggleInstruction;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('toggleinstruction', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tin', 'toggleinstruct', 'toggleinstruction'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Enables/disables various instruction settings';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my $string;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;tin
        if (! defined $switch) {

            # Display header
            $session->writeText(
                'List of instruction settings',
            );

            # Display list
            if (! $axmud::CLIENT->confirmWorldCmdFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            $session->writeText(
                '   Confirm world commands in session\'s default tab                   - '
                . $string,
            );

            if (! $axmud::CLIENT->convertWorldCmdFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            $session->writeText(
                '   Convert single-word world commands to lower case                  - ' . $string,
            );

            if (! $axmud::CLIENT->preserveWorldCmdFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            $session->writeText(
                '   Retain last world/multi/speedwalk/bypass command in \'main\' window - '
                . $string,
            );

            if (! $axmud::CLIENT->preserveOtherCmdFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            $session->writeText(
                '   Retain other kinds of instruction in \'main\' window                - '
                . $string,
            );

            if (! $axmud::CLIENT->maxMultiCmdFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            $session->writeText(
                '   Send multi commands to all sessions (not just the same world)     - ' . $string,
            );

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of list (5 instruction settings found)',
            );

        # ;tin -c
        } elsif ($switch eq '-c') {

            $axmud::CLIENT->toggle_instructFlag('confirm');
            if (! $axmud::CLIENT->confirmWorldCmdFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Confirmation of world commands in session\'s default tab turned ' . $string,
            );

        # ;tin -v
        } elsif ($switch eq '-v') {

            $axmud::CLIENT->toggle_instructFlag('convert');
            if (! $axmud::CLIENT->convertWorldCmdFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Conversion of single-word world commands to lower case turned ' . $string,
            );

        # ;tin -w
        } elsif ($switch eq '-w') {

            $axmud::CLIENT->toggle_instructFlag('world');
            if (! $axmud::CLIENT->preserveWorldCmdFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Retention of last world/multi/speedwalk command in the \'main\' window turned '
                . $string,
            );

        # ;tin -o
        } elsif ($switch eq '-o') {

            $axmud::CLIENT->toggle_instructFlag('other');
            if (! $axmud::CLIENT->preserveOtherCmdFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Retention of other kinds of instruction in the \'main\' window turned ' . $string,
            );

        # ;tin -m
        } elsif ($switch eq '-m') {

            $axmud::CLIENT->toggle_instructFlag('max');
            if (! $axmud::CLIENT->maxMultiCmdFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Multi commands are now sent only to sessions with the same current world',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Multi commands are now sent to all sessions',
                );
            }

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid switch (try \'-c\', \'-v\', \'-w\', \'-o\' or \'-m\')',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ToggleSigil;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('togglesigil', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tsg', 'sigil', 'togglesigil'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Enables/disables instruction sigils';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my $column;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;tsg
        if (! defined $switch) {

            # Display header
            $session->writeText(
                'List of instruction sigils (* - enabled, + - enabled and can\'t be disabled)',
            );

            # Display list
            $session->writeText(
                ' + ' . sprintf('%-8.8s', $axmud::CLIENT->constClientSigil)
                . ' Client command sigil',
            );

            $session->writeText(
                ' + ' . sprintf('%-8.8s', $axmud::CLIENT->constForcedSigil)
                . ' Forced world command sigil',
            );

            if (! $axmud::CLIENT->echoSigilFlag) {
                $column = '   ';
            } else {
                $column = ' * ';
            }

            $session->writeText(
                $column . sprintf('%-8.8s', $axmud::CLIENT->constEchoSigil)
                . ' Echo command sigil',
            );

            if (! $axmud::CLIENT->perlSigilFlag) {
                $column = '   ';
            } else {
                $column = ' * ';
            }

            $session->writeText(
                $column . sprintf('%-8.8s', $axmud::CLIENT->constPerlSigil)
                . ' Perl command sigil',
            );

            if (! $axmud::CLIENT->scriptSigilFlag) {
                $column = '   ';
            } else {
                $column = ' * ';
            }

            $session->writeText(
                $column . sprintf('%-8.8s', $axmud::CLIENT->constScriptSigil)
                . ' Script command sigil',
            );

            if (! $axmud::CLIENT->multiSigilFlag) {
                $column = '   ';
            } else {
                $column = ' * ';
            }

            $session->writeText(
                $column . sprintf('%-8.8s', $axmud::CLIENT->constMultiSigil)
                . ' Multi command sigil',
            );

            if (! $axmud::CLIENT->speedSigilFlag) {
                $column = '   ';
            } else {
                $column = ' * ';
            }

            $session->writeText(
                $column . sprintf('%-8.8s', $axmud::CLIENT->constBypassSigil)
                . ' Bypass command sigil',
            );

            if (! $axmud::CLIENT->bypassSigilFlag) {
                $column = '   ';
            } else {
                $column = ' * ';
            }

            $session->writeText(
                $column . sprintf('%-8.8s', $axmud::CLIENT->constBypassSigil)
                . ' Bypass command sigil',
            );

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of list (8 instruction sigils found)',
            );

        # ;tsg -e
        } elsif ($switch eq '-e') {

            $axmud::CLIENT->toggle_sigilFlag('echo');
            if (! $axmud::CLIENT->echoSigilFlag) {
                return $self->complete($session, $standardCmd, 'Echo command sigils disabled');
            } else {
                return $self->complete($session, $standardCmd, 'Echo command sigils enabled');
            }

        # ;tsg -p
        } elsif ($switch eq '-p') {

            $axmud::CLIENT->toggle_sigilFlag('perl');
            if (! $axmud::CLIENT->perlSigilFlag) {
                return $self->complete($session, $standardCmd, 'Perl command sigils disabled');
            } else {
                return $self->complete($session, $standardCmd, 'Perl command sigils enabled');
            }

        # ;tsg -s
        } elsif ($switch eq '-s') {

            $axmud::CLIENT->toggle_sigilFlag('script');
            if (! $axmud::CLIENT->scriptSigilFlag) {
                return $self->complete($session, $standardCmd, 'Script command sigils disabled');
            } else {
                return $self->complete($session, $standardCmd, 'Script command sigils enabled');
            }

        # ;tsg -m
        } elsif ($switch eq '-m') {

            $axmud::CLIENT->toggle_sigilFlag('multi');
            if (! $axmud::CLIENT->multiSigilFlag) {
                return $self->complete($session, $standardCmd, 'Multi command sigils disabled');
            } else {
                return $self->complete($session, $standardCmd, 'Multi command sigils enabled');
            }

        # ;tsg -w
        } elsif ($switch eq '-w') {

            $axmud::CLIENT->toggle_sigilFlag('speed');
            if (! $axmud::CLIENT->speedSigilFlag) {
                return $self->complete($session, $standardCmd, 'Speedwalk command sigils disabled');
            } else {
                return $self->complete($session, $standardCmd, 'Speedwalk command sigils enabled');
            }

        # ;tsg -b
        } elsif ($switch eq '-b') {

            $axmud::CLIENT->toggle_sigilFlag('bypass');
            if (! $axmud::CLIENT->bypassSigilFlag) {
                return $self->complete($session, $standardCmd, 'Bypass command sigils disabled');
            } else {
                return $self->complete($session, $standardCmd, 'Bypass command sigils enabled');
            }

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid switch (try \'-e\', \'-p\', \'-s\', \'-m\', \'-w\' or \'b\')',
            );
        }
    }
}

{ package Games::Axmud::Cmd::CommandSeparator;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('commandseparator', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['csp', 'cmdsep', 'commandseparator'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the command separator';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $string,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;csp
        if (! defined $string) {

            # Use the default command separator
            $axmud::CLIENT->set_cmdSep($axmud::CLIENT->constCmdSep);

        # ;csp <string>
        } else {

            if (length ($string) > 4) {

                return $self->error(
                    $session, $inputString,
                    'Invalid command separator \'' . $string . '\' - maximum length is 4'
                    . ' characters',
                );
            }

            # Use the specfied command separator
            $axmud::CLIENT->set_cmdSep($string);
        }

        return $self->complete(
            $session, $standardCmd,
            'Command separator set to \'' . $axmud::CLIENT->cmdSep . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::Echo;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('echo', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['echo'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Displays an echo string as a system message';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my $string;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # User can use diamond brackets if they want to preserve spacing between words
        $string = join(' ', @args);
        if (! $session->echoCmd($string)) {

            return $self->error(
                $session, $inputString,
                'Failed to interpret \'' . $string . '\' as an echo string',
            );

        } else {

            # No standard message - behave as though the user had typed '"$string' in the command
            #   entry box
            return 1;
        }
    }
}

{ package Games::Axmud::Cmd::Perl;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('perl', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['perl'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Executes a Perl string as a programme';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $string,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $string || defined $check) {

            return $self->improper($session, $inputString);
        }

        # User must use diamond brackets
        if (! $session->perlCmd($string)) {

            return $self->error(
                $session, $inputString,
                'Failed to interpret \'' . $string . '\' as a Perl string',
            );

        } else {

            # No standard message - behave as though the user had typed '/$string' in the command
            #   entry box
            return 1;
        }
    }
}

{ package Games::Axmud::Cmd::Multi;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('multi', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['multi'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Interprets a multi string in multiple sessions';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my $string;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # User can use diamond brackets if they want to preserve spacing between words
        $string = join(' ', @args);
        if (! $session->multiCmd($string)) {

            return $self->error(
                $session, $inputString,
                'Failed to interpret \'' . $string . '\' as a multi string',
            );

        } else {

            # No standard message - behave as though the user had typed ':$string' in the command
            #   entry box
            return 1;
        }
    }
}

{ package Games::Axmud::Cmd::SpeedWalk;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('speedwalk', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['spw', 'speed', 'speedwalk'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Interprets a speedwalk string';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my $string;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # GA::Session removes whitespace between components in a speedwalk command, so we can
        #   simply join @args together
        $string = join(' ', @args);
        if (! $session->speedWalkCmd($string)) {

            return $self->error(
                $session, $inputString,
                'Failed to interpret \'' . $string . '\' as a speedwalk string',
            );

        } else {

            # No standard message - behave as though the user had typed '.$string' in the command
            #   entry box
            return 1;
        }
    }
}

{ package Games::Axmud::Cmd::SlowWalk;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('slowwalk', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['slw', 'slow', 'slowwalk'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Handles the current world\'s slowwalk settings';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $num, $delay,
            $check,
        ) = @_;

        # Local variables
        my $worldObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If $num is specified, it must an integer, >= 0; but 'off' and 'on' are also recognised
        if (defined $num) {

            if ($num eq 'off') {

                $num = 0;
                # Ignore $delay, if specified
                $delay = undef;

            } elsif ($num eq 'on') {

                $num = 1;
                $delay = undef;

            } elsif (! $axmud::CLIENT->intCheck($num, 0)) {

                return $self->error(
                    $session, $inputString,
                    'The number of commands must be an integer, 0 or above',
                );
            }
        }

        # If $delay is specified, same rule applies
        if (defined $delay && ! $axmud::CLIENT->floatCheck($delay, 0)) {

            return $self->error(
                $session, $inputString,
                'The slowwalk delay must be a number, 0 or above',
            );
        }

        # Import the current world (for convenience)
        $worldObj = $session->currentWorld;

        # ;slw
        if (! defined $num) {

            # Display header
            $session->writeText('List of current world\'s slowwalk settings');

            # Display list
            if (! $worldObj->excessCmdLimit) {

                $session->writeText('   World command limit     - unlimited');

            } else {

                $session->writeText('   World command limit     - ' . $worldObj->excessCmdLimit);

                if (!  $worldObj->excessCmdDelay) {

                    $session->writeText(
                        '   Delay time              - minimum system delay ('
                        . $session->sessionLoopDelay . 's)',
                    );

                } else {

                    $session->writeText(
                        '   Delay time (in seconds) - ' . $worldObj->excessCmdDelay,
                    );
                }
            }

            # Display list
            return $self->complete($session, $standardCmd, 'End of list');

        # ;slw <num>
        # ;slw on
        # ;slw off
        } elsif (! defined $delay) {

            # Update the current world. Setting ->excessCmdLimit to 0 turns off excess commands
            #   altogether
            $worldObj->ivPoke('excessCmdLimit', $num);
            $worldObj->ivPoke('excessCmdDelay', 1);

            if (! $num) {

                # For all sessions using the same current world, excess commands must be sent
                #   immediately
                foreach my $otherSession ($axmud::CLIENT->listSessions()) {

                    if ($otherSession->currentWorld eq $session->currentWorld) {

                        $otherSession->reset_lastExcessCmdTime();
                    }
                }

                return $self->complete($session, $standardCmd, 'Slowwalking turned off');

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Slowwalking turned on; max commands per second: ' . $num,
                );
            }

        # ;slw <num> <delay>
        } else {

            # Update the current world. Setting ->excessCmdLimit to 0 turns off excess commands
            #   altogether; this is somewhat redundant, but allowed
            $worldObj->ivPoke('excessCmdLimit', $num);
            $worldObj->ivPoke('excessCmdDelay', $delay);

            if (! $num) {

                # For all sessions using the same current world, excess commands must be sent
                #   immediately
                foreach my $otherSession ($axmud::CLIENT->listSessions()) {

                    if ($otherSession->currentWorld eq $session->currentWorld) {

                        $otherSession->reset_lastExcessCmdTime();
                    }
                }

                return $self->complete($session, $standardCmd, 'Slowwalking turned off');

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Slowwalking turned on; max commands: ' . $num  . ', delay time: '
                    . $delay,
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::Crawl;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('crawl', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['crawl'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Enables/disables crawl mode';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $num,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Default commands per second is 1
        if (defined $num && ! $axmud::CLIENT->intCheck($num, 1)) {

            return $self->error(
                $session, $inputString,
                'The number of commands must be an integer, 1 or above',
            );
        }

        if (defined $num || ! $session->crawlModeFlag) {

            if (! defined $num) {

                # Default command limit per second
                $num = 1;
            }

            # Enable crawl mode
            if (! $session->setCrawlMode($num)) {

                return $self->error(
                    $session, $inputString,
                    'Crawl mode could not be enabled (internal error)',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Crawl mode has been enabled for (at least) the next '
                    . $session->crawlModeWaitTime . ' seconds (command limit: ' . $num . ')',
                );
            }

        } else {

            # Disable crawl mode. The TRUE argument means 'don't display a system message'
            $session->resetCrawlMode(TRUE);

            return $self->complete(
                $session, $standardCmd,
                'Crawl mode has been disabled',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Bypass;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('bypass', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['byp', 'bypass'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Interprets a bypass string as a world command';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my $string;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # User can use diamond brackets if they want to preserve spacing between words
        $string = join(' ', @args);
        if (! $session->worldCmd($string, undef, undef, TRUE)) {

            return $self->error(
                $session, $inputString,
                'Failed to interpret \'' . $string . '\' as a bypass string',
            );

        } else {

            # No standard message - behave as though the user had typed '>$string' in the command
            #   entry box
            return 1;
        }
    }
}

{ package Games::Axmud::Cmd::AddUserCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addusercommand', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['auc', 'adduc', 'addusercmd', 'addusercommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a new user command';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $newStandard, $newUser,
            $check,
        ) = @_;

        # Local variables
        my $cmdObj;

        # Check for improper arguments
        if (! defined $newStandard || ! defined $newUser || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the new user command isn't already in use
        if ($axmud::CLIENT->ivExists('userCmdHash', $newUser)) {

            return $self->error(
                $session, $inputString,
                'The user command \'' . $newUser . '\' already exists (redirecting to the '
                . ' standard command \'' . $axmud::CLIENT->ivShow('userCmdHash', $newUser) . '\'',
            );
        }

        # Check the new user command is valid
        if (! $axmud::CLIENT->nameCheck($newUser, 32)) {

            return $self->error(
                $session, $inputString,
                'Invalid user command \'' . $newUser . '\'',
            );
        }

        # Check that the standard command exists
        if (! $axmud::CLIENT->ivExists('clientCmdHash', $newStandard)) {

            return $self->error(
                $session, $inputString,
                'The standard command \'' . $newStandard . '\' doesn\'t exist',
            );

        } else {

            $cmdObj = $axmud::CLIENT->ivShow('clientCmdHash', $newStandard);
        }

        # Add the user command
        $axmud::CLIENT->add_userCmd($newUser, $newStandard);
        $cmdObj->add_userCmd($newUser);

        return $self->complete(
            $session, $standardCmd,
            'Added the user command \'' . $newUser . '\' which redirects to standard command \''
            . $newStandard . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::DeleteUserCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteusercommand', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['duc', 'deluc', 'deleterusercmd', 'deleteusercommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a user command';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $oldUser,
            $check,
        ) = @_;

        # Local variables
        my ($oldStandard, $cmdObj);

        # Check for improper arguments
        if (! defined $oldUser || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the user command exists
        if (! $axmud::CLIENT->ivExists('userCmdHash', $oldUser)) {

            return $self->error(
                $session, $inputString,
                'The user command \'' . $oldUser . '\' doesn\'t exist',
            );
        }

        # Can't delete the standard form
        $oldStandard = $axmud::CLIENT->ivShow('userCmdHash', $oldUser);
        if ($oldStandard eq $oldUser) {

            return $self->error(
                $session, $inputString,
                'A user command that\'s identical to the corresponding standard command can\'t'
                . ' be deleted',
            );
        }

        # Check the command object exists
        $cmdObj = $axmud::CLIENT->ivShow('clientCmdHash', $oldStandard);
        if (! $cmdObj) {

            return $self->error(
                $session, $inputString,
                'General error deleting the user command \'' . $oldUser . '\'',
            );
        }

        # Delete the user command
        $axmud::CLIENT->del_userCmd($oldUser);
        $cmdObj->del_userCmd($oldUser);

        return $self->complete(
            $session, $standardCmd,
            'Deleted the user command \'' . $oldUser . '\' which redirected to standard command \''
            . $oldStandard . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::ListUserCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listusercommand', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['luc', 'listuc', 'listusercmd', 'listusercommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists user commands';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $header,
            @list,
            %hash,
        );

        # Check for improper arguments
        if ((defined $switch && $switch ne '-d') || defined $check) {

            return $self->improper($session, $inputString);
        }

        if ($switch) {

            # Compile a list of default user commands in alphabetical order
            %hash = $axmud::CLIENT->constUserCmdHash;
            @list = sort {lc($a) cmp lc($b)} (keys %hash);

            $header = 'Default user command list';

        } else {

            # Compile a list of (custom) user commands in alphabetical order
            %hash = $axmud::CLIENT->userCmdHash;
            @list = sort {lc($a) cmp lc($b)} (keys %hash);

            $header = 'User command list';
        }

        if (! @list) {

            return $self->complete($session, $standardCmd, 'The user command list is empty');
        }

        # Display header
        $session->writeText('Default user command list');

        # Display list
        $session->writeText('   User command                     Standard command');
        foreach my $cmd (@list) {

            $session->writeText('   ' . sprintf('%-32.32s %-32.32s', $cmd, $hash{$cmd}));
        }

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 user command found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . @list . ' user commands found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ResetUserCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resetusercommand', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ruc', 'resetuc', 'resetusercmd', 'resetusercommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets the user command list';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Reset the GA::Client's list
        $axmud::CLIENT->reset_userCmd();
        # Reset the user command list for each command object
        foreach my $obj ($axmud::CLIENT->ivValues('clientCmdHash')) {

            $obj->reset_userCmd();
        }

        return $self->complete($session, $standardCmd, 'User commands set to their default values');
    }
}

{ package Games::Axmud::Cmd::DisplayBuffer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('displaybuffer', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['db', 'dispbuff', 'displaybuffer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows the status of the session\'s display buffer';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Display header
        $session->writeText('Session display buffer status');

        # Display list
        $session->writeText('  Buffer size: ' . $axmud::CLIENT->customDisplayBufferSize);
        if (! $session->displayBufferCount) {

            $session->writeText('  Buffer is empty');

        } else {

            $session->writeText(
                '  First line: ' . $session->displayBufferFirst . ', last line: '
                . $session->displayBufferLast,
            );
        }

        # Display footer
        return $self->complete($session, $standardCmd, 'End of buffer status');
    }
}

{ package Games::Axmud::Cmd::SetDisplayBuffer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setdisplaybuffer', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sdb', 'setdispbuff', 'setdisplaybuffer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the size of display buffers';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $size,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Use a default size, if none specified
        if (! defined $size) {

            $size = $axmud::CLIENT->constDisplayBufferSize;

        } elsif (
            ! $axmud::CLIENT->intCheck(
                $size,
                $axmud::CLIENT->constMinBufferSize,
                $axmud::CLIENT->constMaxBufferSize,
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Invalid size (must be a number in the range '
                . $axmud::CLIENT->constMinBufferSize . '-'
                . $axmud::CLIENT->constMaxBufferSize . ')',
            );
        }

        # Set the buffer size, updating both the GA::Client and all GA::Sessions
        $axmud::CLIENT->set_customDisplayBufferSize($size);
        foreach my $thisSession($axmud::CLIENT->ivValues('sessionHash')) {

            $thisSession->updateBufferSize('display', $size);
        }

        return $self->complete($session, $standardCmd, 'Display buffer size set to ' . $size);
    }
}

{ package Games::Axmud::Cmd::EditDisplayBuffer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editdisplaybuffer', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['edb', 'editdispbuff', 'editdisplaybuffer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a display buffer line';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the display buffer line exists. If no line number was specified, use the most recent
        #   one
        if (! defined $number) {

            $number = $session->displayBufferLast;
        }

        $obj = $session->ivShow('displayBufferHash', $number);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Could not edit the display buffer line #' . $number . ' - the line does not exist'
                . ' (or no longer exists)',
            );
        }

        # Open an 'edit' window for the display buffer line
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Buffer::Display',
                $session->mainWin,
                $session,
                'Edit display buffer line #' . $number,
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the display buffer line #' . $number,
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the display buffer line #' . $number,
            );
        }
    }
}

{ package Games::Axmud::Cmd::DumpDisplayBuffer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('dumpdisplaybuffer', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ddb', 'dumpdispbuff', 'dumpdisplaybuffer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Displays contents of the session\'s display buffer';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $start, $stop,
            $check,
        ) = @_;

        # Local variables
        my ($firstObj, $lastObj, $obj, $firstLine, $lastLine, $step);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import IVs
        $firstLine = $session->displayBufferFirst;
        $lastLine = $session->displayBufferLast;
        if (! defined $firstLine) {

            return $self->complete($session, $standardCmd, 'The display buffer is empty');
        }

        # Import GA::Buffer::Display objects
        $firstObj = $session->ivShow('displayBufferHash', $firstLine);
        $lastObj = $session->ivShow('displayBufferHash', $lastLine);

        # Replace the words 'start', 'stop', 'first', 'last' and 'all' with line numbers, if any of
        #   those words were used
        if ( (defined $start && $start eq 'all') || (defined $stop && $stop eq 'all')) {

            # If $start is 'all' and $stop is also defined, ignore $stop (and vice versa)
            $start = $firstLine;
            $stop = $lastLine;

        } else {

            if (defined $start) {

                if ($start eq 'start' || $start eq 'first') {

                    $start = $firstLine;

                } elsif ($start eq 'stop' || $start eq 'last') {

                    $start = $lastLine;
                }
            }

            if (defined $stop) {

                if ($stop eq 'start' || $stop eq 'first') {

                    $stop = $firstLine;

                } elsif ($stop eq 'stop' || $stop eq 'last') {

                    $stop = $lastLine;
                }
            }
        }

        # ;ddb
        if (! defined $start) {

            # Display the most recently received line
            $session->writeText('Display buffer line ' . $lastLine);
            $session->writeText(sprintf('   %-8.8s %-64.64s', $lastLine, $lastObj->stripLine));

            return $self->complete($session, $standardCmd, 'End of display buffer dump');

        # ;ddb <number>
        } elsif (! defined $stop) {

            # Check that <number> is a valid buffer line
            if (! $session->ivExists('displayBufferHash', $start)) {

                return $self->error(
                    $session, $inputString,
                    'Line #' . $start . ' isn\'t a valid buffer line, or has been deleted from the'
                    . ' display buffer',
                );

            } else {

                # Display the given line
                $obj = $session->ivShow('displayBufferHash', $start);

                $session->writeText('Display buffer line ' . $start);
                $session->writeText(sprintf('   %-8.8s %-64.64s', $start, $obj->stripLine));

                return $self->complete($session, $standardCmd, 'End of display buffer dump');
            }

        # ;ddb <start> <stop>
        } else {

            # Check that <start> and <stop> are valid received lines
            if (! $session->ivExists('displayBufferHash', $start)) {

                return $self->error(
                    $session, $inputString,
                    'Line #' . $start . ' isn\'t a valid buffer line, or has been deleted from the'
                    . ' display buffer',
                );

            } elsif (! $session->ivExists('displayBufferHash', $stop)) {

                return $self->error(
                    $session, $inputString,
                    'Line #' . $stop . ' isn\'t a valid buffer line, or has been deleted from the'
                    . ' display buffer',
                );
            }

            # If <start> and <stop> have been specified in the wrong order (e.g. 56, 55), display
            #  lines from the buffer in the reverse order
            if ($start <= $stop) {
                $step = 1;
            } else {
                $step = -1;
            }

            # Display header
            if ($start == $stop) {

                # $start and $stop are identical
                $session->writeText('Display buffer line ' . $start . ' (* - complete line)');

            } else {

                $session->writeText(
                    'Display buffer lines ' . $start . ' - ' . $stop . ' (* - complete line)',
                );
            }

            # Display list
            for (my $lineNum = $start; $lineNum != ($stop + $step); $lineNum += $step) {

                my ($thisObj, $column);

                $thisObj = $session->ivShow('displayBufferHash', $lineNum);

                if ($thisObj->newLineFlag) {
                    $column = ' * ';
                } else {
                    $column = '   ';
                }

                $session->writeText(
                    $column . sprintf('Line #%-8.8s Time %-32.32s', $lineNum, $thisObj->time)
                );

                if ($thisObj->stripLine) {
                    $session->writeText('   ' . $thisObj->stripLine);
                } else {
                    $session->writeText('   <empty line>');
                }
            }

            # Display footer
            return $self->complete($session, $standardCmd, 'End of display buffer dump');
        }
    }
}

{ package Games::Axmud::Cmd::InstructionBuffer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('instructionbuffer', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ib', 'instructbuff', 'instructionbuffer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows the status of an instruction buffer';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my ($owner, $string);

        # Check for improper arguments
        if (
            (defined $switch && $switch ne '-c' && $switch ne '-s')
            || defined $check
        ) {
            return $self->improper($session, $inputString);
        }

        # Display header
        if (defined $switch && $switch eq '-c') {

            $session->writeText('Combined instruction buffer status');
            $owner = $axmud::CLIENT;
            $string = 'combined';

        } else {

            $session->writeText('Session instruction buffer status');
            $owner = $session;
            $string = 'session';
        }

        # Display list
        $owner->writeText('  Buffer size: ' . $axmud::CLIENT->customInstructBufferSize);
        if (! $owner->instructBufferCount) {

            $owner->writeText('  Buffer is empty');

        } else {

            $owner->writeText(
                '  First item: ' . $owner->instructBufferFirst . ', last item: '
                . $owner->instructBufferLast,
            );
        }

        # Display footer
        return $self->complete(
            $session, $standardCmd,
            'End of ' . $string . ' instruction buffer status',
        );
    }
}

{ package Games::Axmud::Cmd::SetInstructionBuffer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setinstructionbuffer', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sib', 'setinstructbuff', 'setinstructionbuffer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the size of instruction buffers';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $size,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Use a default size, if none specified
        if (! defined $size) {

            $size = $axmud::CLIENT->constInstructBufferSize;

        } elsif (
            ! $axmud::CLIENT->intCheck(
                $size,
                $size < $axmud::CLIENT->constMinBufferSize,
                $size > $axmud::CLIENT->constMaxBufferSize,
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Invalid size (must be a number in the range '
                . $axmud::CLIENT->constMinBufferSize . '-'
                . $axmud::CLIENT->constMaxBufferSize . ')',
            );
        }

        # Set the buffer size, updating both the GA::Client and all GA::Sessions
        $axmud::CLIENT->set_customInstructBufferSize($size);
        foreach my $thisSession ($axmud::CLIENT->ivValues('sessionHash')) {

            $thisSession->updateBufferSize('instruct', $size);
        }

        return $self->complete($session, $standardCmd, 'Instruction buffer size set to ' . $size);
    }
}

{ package Games::Axmud::Cmd::EditInstructionBuffer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editinstructionbuffer', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['eib', 'editinstructbuff', 'editinstructionbuffer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens \'edit\' window for an instruction buffer item';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($switch, $clientFlag, $sessionFlag, $owner, $string, $number, $obj);

        # Extract switches
        ($switch, @args) = $self->extract('-c', 0, @args);
        if (defined $switch) {

            $clientFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-s', 0, @args);
        if (defined $switch) {

            $sessionFlag = TRUE;
        }

        # Extract remaining arguments (if any)
        $number = shift @args;

        # There should be nothing left in @args
        if (($clientFlag && $sessionFlag) || @args) {

            return $self->improper($session, $inputString);
        }

        # Set which buffer to use
        if ($clientFlag) {

            $owner = $axmud::CLIENT;
            $string = 'combined';

        } else {

            $owner = $session;
            $string = 'session';
        }

        # Check the instruction buffer item exists. If no item number was specified, use the most
        #   recent one
        if (! defined $number) {

            $number = $owner->instructBufferLast;
        }

        $obj = $owner->ivShow('instructBufferHash', $number);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Could not edit the ' . $string . ' instruction buffer item #' . $number
                . ' - the item does not exist (or no longer exists)',
            );
        }

        # Open an 'edit' window for the instruction buffer item
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Buffer::Instruct',
                $session->mainWin,
                $session,
                'Edit ' . $string . ' instruction buffer item #' . $number,
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the  ' . $string . ' instruction buffer item #' . $number,
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the ' . $string . ' instruction buffer item #'
                . $number,
            );
        }
    }
}

{ package Games::Axmud::Cmd::DumpInstructionBuffer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('dumpinstructionbuffer', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dib', 'dumpinstructbuff', 'dumpinstructionbuffer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Displays contents of an instruction buffer';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $clientFlag, $sessionFlag, $string, $owner, $start, $stop, $firstObj, $lastObj,
            $obj, $firstItem, $lastItem, $step,
        );

        # Extract switches
        ($switch, @args) = $self->extract('-c', 0, @args);
        if (defined $switch) {

            $clientFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-s', 0, @args);
        if (defined $switch) {

            $sessionFlag = TRUE;
        }

        # Extract remaining arguments (if any)
        $start = shift @args;
        $stop = shift @args;

        # There should be nothing left in @args
        if (($clientFlag && $sessionFlag) || @args) {

            return $self->improper($session, $inputString);
        }

        # Set which buffer to use
        if ($clientFlag) {

            $owner = $axmud::CLIENT;
            $string = 'combined';

        } else {

            $owner = $session;
            $string = 'session';
        }

        # Import IVs
        $firstItem = $owner->instructBufferFirst;
        $lastItem = $owner->instructBufferLast;
        if (! defined $firstItem) {

            return $self->complete(
                $session,
                $standardCmd, 'The ' . $string . ' instruction buffer is empty',
            );
        }

        # Import GA::Buffer::Instruct objects
        $firstObj = $owner->ivShow('instructBufferHash', $firstItem);
        $lastObj = $owner->ivShow('instructBufferHash', $lastItem);

        # Replace the words 'start', 'stop', 'first', 'last' and 'all' with item numbers, if any of
        #   those words were used
        if ( (defined $start && $start eq 'all') || (defined $stop && $stop eq 'all')) {

            # If $start is 'all' and $stop is also defined, ignore $stop (and vice versa)
            $start = $firstItem;
            $stop = $lastItem;

        } else {

            if (defined $start) {

                if ($start eq 'start' || $start eq 'first') {

                    $start = $firstItem;

                } elsif ($start eq 'stop' || $start eq 'last') {

                    $start = $lastItem;
                }
            }

            if (defined $stop) {

                if ($stop eq 'start' || $stop eq 'first') {

                    $stop = $firstItem;

                } elsif ($stop eq 'stop' || $stop eq 'last') {

                    $stop = $lastItem;
                }
            }
        }

        # ;dib
        if (! defined $start) {

            # Display the most recent item
            $session->writeText(ucfirst($string) . ' instruction buffer item ' . $lastItem);
            $session->writeText(
                sprintf(
                        '   Item: %-8.8s Time: %-16.16s Type: %-4.4s   Cmd: %-32.32s',
                    $lastItem,
                    $lastObj->time,
                    $lastObj->type,
                    $lastObj->instruct,
                )
            );

            return $self->complete(
                $session, $standardCmd,
                'End of ' . $string . ' instruction buffer dump',
            );

        # ;dib <number>
        } elsif (! defined $stop) {

            # Check that <number> is a valid buffer item
            if (! $owner->ivExists('instructBufferHash', $start)) {

                return $self->error(
                    $session, $inputString,
                    'Item #' . $start . ' isn\'t a valid buffer item, or has been deleted from the'
                    . $string . ' instruction buffer',
                );

            } else {

                # Display the given item
                $obj = $owner->ivShow('instructBufferHash', $start);

                $session->writeText(
                    ucfirst($string) . ' instruction buffer item ' . $start
                    . '(* - client command)',
                );

                $session->writeText(
                    sprintf(
                        '   Item: %-8.8s Time: %-16.16s Type: %-4.4s   Cmd: %-32.32s',
                        $start,
                        $obj->time,
                        $obj->type,
                        $obj->instruct,
                    ),
                );

                return $self->complete(
                    $session, $standardCmd,
                    'End of ' . $string . ' instruction buffer dump',
                );
            }

        # ;dib <start> <stop>
        } else {

            # Check that <start> and <stop> are valid items
            if (! $owner->ivExists('instructBufferHash', $start)) {

                return $self->error(
                    $session, $inputString,
                    'Item #' . $start . ' isn\'t a valid buffer item, or has been deleted from the'
                    . $string . ' instruction buffer',
                );

            } elsif (! $owner->ivExists('instructBufferHash', $stop)) {

                return $self->error(
                    $session, $inputString,
                    'Item #' . $stop . ' isn\'t a valid buffer item, or has been deleted from the'
                    . $string . ' instruction buffer',
                );
            }

            # If <start> and <stop> have been specified in the wrong order (e.g. 56, 55), display
            #  items from the buffer in the reverse order
            if ($start <= $stop) {
                $step = 1;
            } else {
                $step = -1;
            }

            # Display header
            if ($start == $stop) {

                # $start and $stop are identical
                $session->writeText(ucfirst($string) . ' instruction buffer item ' . $start);

            } else {

                $session->writeText(
                    ucfirst($string) . ' instruction buffer items ' . $start . ' - ' . $stop
                    . '(* - client command)',
                );
            }

            # Display list
            for (my $itemNum = $start; $itemNum != ($stop + $step); $itemNum += $step) {

                my $thisObj = $owner->ivShow('instructBufferHash', $itemNum);

                $session->writeText(
                    sprintf(
                        '   Item: %-8.8s Time: %-16.16s Type: %-4.4s   Cmd: %-32.32s',
                        $itemNum,
                        $thisObj->time,
                        $thisObj->type,
                        $thisObj->instruct,
                    ),
                );
            }

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of ' . $string . ' instruction buffer dump',
            );
        }
    }
}

{ package Games::Axmud::Cmd::CommandBuffer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('commandbuffer', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cb', 'cmdbuff', 'commandbuffer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows the status of a world command buffer';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my ($owner, $string);

        # Check for improper arguments
        if (
            (defined $switch && $switch ne '-c' && $switch ne '-s')
            || defined $check
        ) {
            return $self->improper($session, $inputString);
        }

        # Display header
        if (defined $switch && $switch eq '-c') {

            $session->writeText('Combined world command buffer status');
            $owner = $axmud::CLIENT;
            $string = 'combined';

        } else {

            $session->writeText('Session world command buffer status');
            $owner = $session;
            $string = 'session';
        }

        # Display list
        $owner->writeText('  Buffer size: ' . $axmud::CLIENT->customCmdBufferSize);
        if (! $owner->cmdBufferCount) {

            $owner->writeText('  Buffer is empty');

        } else {

            $owner->writeText(
                '  First item: ' . $owner->cmdBufferFirst . ', last item: '
                . $owner->cmdBufferLast,
            );
        }

        # Display footer
        return $self->complete(
            $session, $standardCmd,
            'End of ' . $string . ' world command buffer status',
        );
    }
}

{ package Games::Axmud::Cmd::SetCommandBuffer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setcommandbuffer', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['scb', 'setcmdbuff', 'setcommandbuffer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the size of world command buffers';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $size,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Use a default size, if none specified
        if (! defined $size) {

            $size = $axmud::CLIENT->constCmdBufferSize;

        } elsif (
            ! $axmud::CLIENT->intCheck(
                $size,
                $size < $axmud::CLIENT->constMinBufferSize,
                $size > $axmud::CLIENT->constMaxBufferSize,
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Invalid size (must be a number in the range '
                . $axmud::CLIENT->constMinBufferSize . '-'
                . $axmud::CLIENT->constMaxBufferSize . ')',
            );
        }

        # Set the buffer size, updating both the GA::Client and all GA::Sessions
        $axmud::CLIENT->set_customCmdBufferSize($size);
        foreach my $thisSession ($axmud::CLIENT->ivValues('sessionHash')) {

            $thisSession->updateBufferSize('cmd', $size);
        }

        return $self->complete($session, $standardCmd, 'Command buffer size set to ' . $size);
    }
}

{ package Games::Axmud::Cmd::EditCommandBuffer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editcommandbuffer', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ecb', 'editcmdbuff', 'editcommandbuffer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a command buffer item';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($switch, $clientFlag, $sessionFlag, $owner, $string, $number, $obj);

        # Extract switches
        ($switch, @args) = $self->extract('-c', 0, @args);
        if (defined $switch) {

            $clientFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-s', 0, @args);
        if (defined $switch) {

            $sessionFlag = TRUE;
        }

        # Extract remaining arguments (if any)
        $number = shift @args;

        # There should be nothing left in @args
        if (($clientFlag && $sessionFlag) || @args) {

            return $self->improper($session, $inputString);
        }

        # Set which buffer to use
        if ($clientFlag) {

            $owner = $axmud::CLIENT;
            $string = 'combined';

        } else {

            $owner = $session;
            $string = 'session';
        }

        # Check the world command buffer item exists. If no item number was specified, use the most
        #   recent one
        if (! $number) {

            $number = $owner->cmdBufferLast;
        }

        $obj = $owner->ivShow('cmdBufferHash', $number);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Could not edit the ' . $string . ' world command buffer item #' . $number
                . ' - the item does not exist (or no longer exists)',
            );
        }

        # Open an 'edit' window for the world command buffer item
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Buffer::Cmd',
                $session->mainWin,
                $session,
                'Edit ' . $string . ' world command buffer item #' . $number,
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the  ' . $string . ' world command buffer item #' . $number,
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the ' . $string . ' world command buffer item #'
                . $number,
            );
        }
    }
}

{ package Games::Axmud::Cmd::DumpCommandBuffer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('dumpcommandbuffer', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dcb', 'dumpcmdbuff', 'dumpcommandbuffer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Displays contents of a world command buffer';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $clientFlag, $sessionFlag, $string, $owner, $start, $stop, $firstObj, $lastObj,
            $obj, $firstItem, $lastItem, $step,
        );

        # Extract switches
        ($switch, @args) = $self->extract('-c', 0, @args);
        if (defined $switch) {

            $clientFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-s', 0, @args);
        if (defined $switch) {

            $sessionFlag = TRUE;
        }

        # Extract remaining arguments (if any)
        $start = shift @args;
        $stop = shift @args;

        # There should be nothing left in @args
        if (($clientFlag && $sessionFlag) || @args) {

            return $self->improper($session, $inputString);
        }

        # Set which buffer to use
        if ($clientFlag) {

            $owner = $axmud::CLIENT;
            $string = 'combined';

        } else {

            $owner = $session;
            $string = 'session';
        }

        # Import IVs
        $firstItem = $owner->cmdBufferFirst;
        $lastItem = $owner->cmdBufferLast;
        if (! defined $firstItem) {

            return $self->complete(
                $session,
                $standardCmd, 'The ' . $string . ' world command buffer is empty',
            );
        }

        # Import GA::Buffer::Cmd objects
        $firstObj = $owner->ivShow('cmdBufferHash', $firstItem);
        $lastObj = $owner->ivShow('cmdBufferHash', $lastItem);

        # Replace the words 'start', 'stop', 'first', 'last' and 'all' with item numbers, if any of
        #   those words were used
        if ( (defined $start && $start eq 'all') || (defined $stop && $stop eq 'all')) {

            # If $start is 'all' and $stop is also defined, ignore $stop (and vice versa)
            $start = $firstItem;
            $stop = $lastItem;

        } else {

            if (defined $start) {

                if ($start eq 'start' || $start eq 'first') {

                    $start = $firstItem;

                } elsif ($start eq 'stop' || $start eq 'last') {

                    $start = $lastItem;
                }
            }

            if (defined $stop) {

                if ($stop eq 'start' || $stop eq 'first') {

                    $stop = $firstItem;

                } elsif ($stop eq 'stop' || $stop eq 'last') {

                    $stop = $lastItem;
                }
            }
        }

        # ;dcb
        if (! defined $start) {

            # Display the most recent item
            $session->writeText(ucfirst($string) . ' world command buffer item ' . $lastItem);
            $session->writeText(
                sprintf(
                    '   Item: %-8.8s Time: %-16.16s Cmd: %-32.32s',
                    $lastItem,
                    $lastObj->time,
                    $lastObj->cmd,
                )
            );

            return $self->complete(
                $session, $standardCmd,
                'End of ' . $string . ' world command buffer dump',
            );

        # ;dcb <number>
        } elsif (! defined $stop) {

            # Check that <number> is a valid buffer item
            if (! $owner->ivExists('cmdBufferHash', $start)) {

                return $self->error(
                    $session, $inputString,
                    'Item #' . $start . ' isn\'t a valid buffer item, or has been deleted from the'
                    . $string . ' world command buffer',
                );

            } else {

                # Display the given item
                $obj = $owner->ivShow('cmdBufferHash', $start);

                $session->writeText(ucfirst($string) . ' world command buffer item ' . $start);
                $session->writeText(
                    sprintf(
                        '   Item: %-8.8s Time: %-16.16s Cmd: %-32.32s',
                        $start,
                        $obj->time,
                        $obj->cmd,
                    ),
                );

                return $self->complete(
                    $session, $standardCmd,
                    'End of ' . $string . ' world command buffer dump',
                );
            }

        # ;dcb <start> <stop>
        } else {

            # Check that <start> and <stop> are valid items
            if (! $owner->ivExists('cmdBufferHash', $start)) {

                return $self->error(
                    $session, $inputString,
                    'Item #' . $start . ' isn\'t a valid buffer item, or has been deleted from the'
                    . $string . ' world command buffer',
                );

            } elsif (! $owner->ivExists('cmdBufferHash', $stop)) {

                return $self->error(
                    $session, $inputString,
                    'Item #' . $stop . ' isn\'t a valid buffer item, or has been deleted from the'
                    . $string . ' world command buffer',
                );
            }

            # If <start> and <stop> have been specified in the wrong order (e.g. 56, 55), display
            #  items from the buffer in the reverse order
            if ($start <= $stop) {
                $step = 1;
            } else {
                $step = -1;
            }

            # Display header
            if ($start == $stop) {

                # $start and $stop are identical
                $session->writeText(ucfirst($string) . ' world command buffer item ' . $start);

            } else {

                $session->writeText(
                    ucfirst($string) . ' world command buffer items ' . $start . ' - ' . $stop,
                );
            }

            # Display list
            for (my $itemNum = $start; $itemNum != ($stop + $step); $itemNum += $step) {

                my $thisObj = $owner->ivShow('cmdBufferHash', $itemNum);

                $session->writeText(
                    sprintf(
                        '   Item: %-8.8s Time: %-16.16s Cmd: %-32.32s',
                        $itemNum,
                        $thisObj->time,
                        $thisObj->cmd,
                    ),
                );
            }

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of ' . $string . ' world command buffer dump',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SaveBuffer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('savebuffer', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['svb', 'savebuff', 'savebuffer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Saves display/command buffers to file';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $displayFlag, $cmdFlag, $beginTime, $beginFlag, $endTime, $endFlag, $path,
            $fileHandle, $offset, $string,
            @list,
            %combHash, %hash,
        );

        # Extract switches
        ($switch, @args) = $self->extract('-d', 0, @args);
        if (defined $switch) {

            $displayFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-c', 0, @args);
        if (defined $switch) {

            $cmdFlag = TRUE;
        }

        ($switch, $beginTime, @args) = $self->extract('-b', 1, @args);
        if (defined $switch) {

            $beginFlag = TRUE;
        }

        ($switch, $endTime, @args) = $self->extract('-e', 1, @args);
        if (defined $switch) {

            $endFlag = TRUE;
        }

        # There should be nothing left in @args
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # If neither -t or -c are specified, save both the display and world command buffers
        if (! $displayFlag && ! $cmdFlag) {

            $displayFlag = TRUE;
            $cmdFlag = TRUE;
        }

        # If either/both of -b and -e are specified, check they're valid
        if ($beginFlag && ! $axmud::CLIENT->intCheck($beginTime, 0)) {

            return $self->error(
                $session, $inputString,
                'Invalid value for the begin time \'' . $beginTime . '\'',
            );

        } elsif ($endFlag && ! $axmud::CLIENT->intCheck($endTime, 0)) {

            return $self->error(
                $session, $inputString,
                'Invalid value for the end time \'' . $endTime . '\'',
            );

        } elsif ($beginFlag && $endFlag && $beginTime > $endTime) {

            return $self->error(
                $session, $inputString,
                'Invalid values for the begin/end times: the begin time must be less than the'
                . ' end time',
            );
        }

        # Don't do anything if file saving is disabled
        if (! $axmud::CLIENT->saveDataFlag) {

            return $self->error(
                $session, $inputString,
                'File save has been disabled in all sessions',
            );
        }

        # Don't do anything if a replay is already in progress
        if (defined $session->replayLoopCheckTime) {

            return $self->error(
                $session, $inputString,
                'Can\'t save a buffer file while a buffer replay is in progress (try'
                . ' \';haltreplay\' first)',
            );
        }

        # Compile a hash, in the form
        #   $combHash{time} = reference_to_list
        # Where
        #   time                - the time at which a buffer line/item was added, in seconds
        #                           (matches $session->sessionTime)
        #   reference_to_list   - a list in the form (type, item, type, item...), where 'type' is
        #                           'd' for a display buffer line or 'c' for a world command buffer
        #                           item
        if ($displayFlag) {

            # Get a sorted list of display buffer lines
            %hash = $session->displayBufferHash;
            @list = sort {$a <=> $b} (keys %hash);

            # Add each line to the combined hash
            OUTER: foreach my $lineNum (@list) {

                my ($bufferObj, $time, $listRef);

                $bufferObj = $hash{$lineNum};
                $time = $bufferObj->time;

                if ($beginFlag && $time < $beginTime) {

                    next OUTER;
                }

                if ($endFlag && $time > $endTime) {

                    next OUTER;
                }

                if (exists $combHash{$time}) {

                    $listRef = $combHash{$time};
                }

                push (@$listRef, 'd', $bufferObj->stripLine);
                $combHash{$time} = $listRef;
            }
        }

        if ($cmdFlag) {

            # Get a sorted list of world command buffer items
            %hash = $session->cmdBufferHash;
            @list = sort {$a <=> $b} (keys %hash);

            # Add each line to the combined hash
            OUTER: foreach my $itemNum (@list) {

                my ($bufferObj, $time, $listRef);

                $bufferObj = $hash{$itemNum};
                $time = $bufferObj->time;

                if ($beginFlag && $time < $beginTime) {

                    next OUTER;
                }

                if ($endFlag && $time > $endTime) {

                    next OUTER;
                }

                if (exists $combHash{$time}) {

                    $listRef = $combHash{$time};
                }

                push (@$listRef, 'c', $bufferObj->cmd);
                $combHash{$bufferObj->time} = $listRef;
            }
        }

        if (! %combHash) {

            return $self->error(
                $session, $inputString,
                'Buffers not saved - the buffer(s) appear to be empty',
            );
        }

        # Choose a filename
        $path = $axmud::DATA_DIR . '/buffers/' . $session->currentWorld->name . '_'
                    . $axmud::CLIENT->localDateString() . '_' . $axmud::CLIENT->localClockString();

        # Open the file for writing, overwriting any existing contents
        if (! open ($fileHandle, ">$path")) {

            return $self->error(
                $session, $inputString,
                'General error saving the buffer file',
            );
        }

        # The time for each buffer line must be adjusted, so that the first line saved has a zero
        #   time
        @list = sort {$a <=> $b} (keys %combHash);
        $offset = $list[0];

        # Write the file
        foreach my $number (@list) {

            my $listRef = $combHash{$number};
            if ($listRef) {

                do {

                    my ($type, $text, $line);

                    $type = shift @$listRef;
                    $text = shift @$listRef;

                    $line = "[" . $type . "] [" . ($number - $offset) . "] " . $text . "\n";
                    print $fileHandle $line;

                } until (! @$listRef);
            }
        }

        # Close the file
        close $fileHandle;

        if ($displayFlag && ! $cmdFlag) {
            $string = 'Display buffer';
        } elsif ($cmdFlag && ! $displayFlag) {
            $string = 'World command buffer';
        } else {
            $string = 'Display/world command buffer';
        }

        return $self->complete(
            $session, $standardCmd,
            $string . ' file saved to \'' . $path . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::LoadBuffer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('loadbuffer', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ldb', 'loadbuff', 'loadbuffer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Loads display/command buffers from file';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $path,
            $check,
        ) = @_;

        # Local variables
        my (
            $fileHandle, $count,
            @list, @extractList,
            %displayHash, %cmdHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Don't do anything if file loading is disabled
        if (! $axmud::CLIENT->loadDataFlag) {

            return $self->error(
                $session, $inputString,
                'File load has been disabled in all sessions',
            );
        }

        # This command can only be used in 'offline' mode
        if ($session->status ne 'offline') {

            return $self->error(
                $session, $inputString,
                'The \';loadbuffer\' command can only be used in \'offline\' mode',
            );
        }

        # Don't do anything if a replay is already in progress
        if (defined $session->replayLoopCheckTime) {

            return $self->error(
                $session, $inputString,
                'Can\'t load a buffer file while a buffer replay is in progress (try'
                . ' \';haltreplay\' first)',
            );
        }

        # If <path> was specified, check it exists
        if ($path) {

            if (! -e $path) {

                return $self->error(
                    $session, $inputString,
                    'Can\'t load buffer file - file not found',
                );
            }

        } else {

            # Prompt the user to choose a file
            $path = $session->mainWin->showFileChooser(
                'Load buffer file',
                'open',
                $axmud::DATA_DIR . '/buffers',
            );

            if (! $path) {

                return $self->complete(
                    $session, $standardCmd,
                    'Load buffer file operation cancelled',
                );
            }
        }

        # Open the file for reading
        if (! open ($fileHandle, "<$path")) {

            return $self->error(
                $session, $inputString,
                'General error loading the buffer file',
            );
        }

        # Read the file
        while (<$fileHandle>) {

            chomp $_;
            push (@list, $_);
        }
        # Close the file
        close $fileHandle;

        # Check every item in @list; if it contains invalid lines, show an error
        $count = 0;
        OUTER: foreach my $item (@list) {

            my $type;

            $count++;

            if (! ($item =~ m/\S/)) {

                # Ignore empty lines, and don't display an error
                next OUTER;

            # Each line is in the form '[type] [time] text'
            } elsif (! ($item =~ m/\[(.*)\]\s+\[(.*)\]\s+(.*)/)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid line #' . $count . ' in buffer file: ' . $item,
                );

            } else {

                # Store the 'type', 'time' and 'text' components
                $type = $1;
                push (@extractList, $type, $2, $3);

                # In an earlier Axmud version, 't' was used instead of 'c'
                if ($type eq 't') {

                    $type = 'd';
                }

                if ($type ne 'd' && $type ne 'c') {

                    return $self->error(
                        $session, $inputString,
                        'Invalid line #' . $count . ' in buffer file: ' . $item,
                    );
                }
            }
        }

        if (! @extractList) {

            return $self->error(
                $session, $inputString,
                'The buffer file is empty',
            );
        }

        # Empty the session's replay display buffer and replay command buffer
        $session->reset_replayDisplayBufferHash();
        $session->reset_replayCmdBufferHash();

        # Prepare the new buffers, %displayHash and %cmdHash
        $count = 0;
        do {

            my ($type, $time, $text, $obj);

            $type = shift @extractList;
            $time = shift @extractList;
            $text = shift @extractList;

            if ($type eq 'd') {

                # Add a new display buffer object
                if ($session->add_replayDisplayBuffer($text, $time)) {

                    $count++;
                }

            } else {

                # Add a new world command buffer object
                if ($session->add_replayCmdBuffer($text, $time)) {

                    $count++;
                }
            }

        } until (! @extractList);

        if (! $count) {

            return $self->error(
                $session, $inputString,
                'General error processing the loaded buffer file',
            );
        }

        return $self->complete(
            $session, $standardCmd,
            'Buffer file loaded',
        );
    }
}

{ package Games::Axmud::Cmd::ReplayBuffer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('replaybuffer', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rpb', 'rpbuff', 'replaybuffer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Replays display/command buffers in \'offline\' mode';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($switch, $displayFlag, $cmdFlag, $beginTime, $beginFlag, $endTime, $endFlag);

        # Extract switches
        ($switch, @args) = $self->extract('-d', 0, @args);
        if (defined $switch) {

            $displayFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-c', 0, @args);
        if (defined $switch) {

            $cmdFlag = TRUE;
        }

        ($switch, $beginTime, @args) = $self->extract('-b', 1, @args);
        if (defined $switch) {

            $beginFlag = TRUE;
        }

        ($switch, $endTime, @args) = $self->extract('-e', 1, @args);
        if (defined $switch) {

            $endFlag = TRUE;
        }

        # There should be nothing left in @args
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # If neither -d nor -c are specified, replay both the display and command buffers
        if (! $displayFlag && ! $cmdFlag) {

            $displayFlag = TRUE;
            $cmdFlag = TRUE;
        }

        # If either/both of -b and -e are specified, check they're valid
        if ($beginFlag && ! $axmud::CLIENT->intCheck($beginTime, 0)) {

            return $self->error(
                $session, $inputString,
                'Invalid value for the begin time \'' . $beginTime . '\'',
            );

        } elsif ($endFlag && ! $axmud::CLIENT->intCheck($endTime, 0)) {

            return $self->error(
                $session, $inputString,
                'Invalid value for the end time \'' . $endTime . '\'',
            );

        } elsif ($beginFlag && $endFlag && $beginTime > $endTime) {

            return $self->error(
                $session, $inputString,
                'Invalid values for the begin/end times: the begin time must be less than the'
                . ' end time',
            );
        }

        # This command can only be used in 'offline' mode
        if ($session->status ne 'offline') {

            return $self->error(
                $session, $inputString,
                'The \';replaybuffer\' command can only be used in offline mode',
            );
        }

        # Check that there's not a replay already in progress
        if (defined $session->replayLoopCheckTime) {

            return $self->error(
                $session, $inputString,
                'There is already a buffer replay in progress (try \';haltreplay\' first)',
            );
        }

        # Check that the replay buffers are not both empty
        if (! $session->replayDisplayBufferHash && ! $session->replayCmdBufferHash) {

            return $self->error(
                $session, $inputString,
                'The replay buffers are empty; fill them by loading a buffer file using the'
                . ' \';loadbuffer\' command',
            );
        }

        # Start the replay loop
        if (! $session->startReplayLoop($displayFlag, $cmdFlag, $beginTime, $endTime)) {

            return $self->error($session, $inputString, 'Unable to start the buffer replay');

        } else {

            return $self->complete($session, $standardCmd, 'Buffer replay started');
        }
    }
}

{ package Games::Axmud::Cmd::HaltReplay;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('haltreplay', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['hrp', 'haltrp', 'haltreplay'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Halts a buffer replay currently in progress';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # This command can only be used while in 'offline' mode
        if ($session->status ne 'offline') {

            return $self->complete(
                $session, $standardCmd,
                'The \';replaybuffer\' command can only be used in \'offline\' mode',
            );
        }

        # Check that there's a replay already in progress
        if (defined $session->replayLoopCheckTime) {

            return $self->error($session, $inputString, 'There is no buffer replay in progress');
        }

        # Stop the replay loop
        if (! $session->stopReplayLoop()) {

            return $self->error($session, $inputString, 'Unable to halt the buffer replay');

        } else {

            return $self->complete($session, $standardCmd, 'Buffer replay halted');
        }
    }
}

{ package Games::Axmud::Cmd::SetAutoComplete;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setautocomplete', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sac', 'setauto', 'setautocomplete'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets auto-complete options';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $flagCount, $switch, $noneFlag, $autoFlag, $instructFlag, $cmdFlag, $combinedFlag,
            $sessionFlag, $string,
        );

        # Extract arguments
        $flagCount = 0;

        ($switch, @args) = $self->extract('-x', 0, @args);
        if (defined $switch) {

            $noneFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-a', 0, @args);
        if (defined $switch) {

            $autoFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-i', 0, @args);
        if (defined $switch) {

            $instructFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-w', 0, @args);
        if (defined $switch) {

            $cmdFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-c', 0, @args);
        if (defined $switch) {

            $combinedFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-s', 0, @args);
        if (defined $switch) {

            $sessionFlag = TRUE;
            $flagCount++;
        }

        # There should be nothing left in @args
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # Some flags can't be combined
        if ($noneFlag && $autoFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -x and -a can\'t be combined',
            );

        } elsif ($instructFlag && $cmdFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -i and -c can\'t be combined',
            );

        } elsif ($combinedFlag && $sessionFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -l and -s can\'t be combined',
            );
        }

        # ;sac
        if (! $flagCount) {

            # Display header
            $session->writeText('Auto-complete options');

            # Display list
            $session->writeText('   Auto-complete mode - when tab/up/down arrow keys pressed:');
            if ($axmud::CLIENT->autoCompleteMode eq 'none') {
                $session->writeText('      Do nothing');
            } else {
                $session->writeText('      Auto-complete the instruction/world command');
            }

            $session->writeText('   Auto-complete type - when auto-completing:');

            if ($axmud::CLIENT->autoCompleteType eq 'instruct') {

                $session->writeText(
                    '      Use the ' . $axmud::CLIENT->autoCompleteType . ' instruction buffer',
                );

            } elsif ($axmud::CLIENT->autoCompleteType eq 'cmd') {

                $session->writeText(
                    '      Use the ' . $axmud::CLIENT->autoCompleteType . ' world command buffer',
                );
            }

            $session->writeText('   Auto-complete location - when auto-completing:');

            if ($axmud::CLIENT->autoCompleteParent eq 'combined') {
                $session->writeText('      Use the combined instruction/world command buffers');
            } else {
                $session->writeText('      Use the session\'s instruction/world command buffers');
            }

            # Display footer
            return $self->complete($session, $standardCmd, 'End of list');

        # ;sac <switches>
        } else {

            # Update IVs
            if ($noneFlag) {
                $axmud::CLIENT->set_autoCompleteMode('none');
            } elsif ($autoFlag) {
                $axmud::CLIENT->set_autoCompleteMode('auto');
            }

            if ($instructFlag) {
                $axmud::CLIENT->set_autoCompleteType('instruct');
            } elsif ($cmdFlag) {
                $axmud::CLIENT->set_autoCompleteType('cmd');
            }

            if ($combinedFlag) {
                $axmud::CLIENT->set_autoCompleteParent('combined');
            } elsif ($sessionFlag) {
                $axmud::CLIENT->set_autoCompleteParent('session');
            }

            return $self->complete($session, $standardCmd, 'Auto-complete options updated');
        }
    }
}

{ package Games::Axmud::Cmd::ToggleWindowKey;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('togglewindowkey', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['twk', 'togglewinkey', 'togglewindowkey'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles special keys used with ' . $axmud::SCRIPT . ' windows';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my $string;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;twk
        if (! defined $switch) {

            # Display header
            $session->writeText(
                'List of ' . $axmud::SCRIPT . ' window special keys/key combinations',
            );

            # Display list
            if (! $axmud::CLIENT->useScrollKeysFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            $session->writeText(
                '   Page up/page down/home/end keys scroll the window pane             - '
                . $string,
            );

            if (! $axmud::CLIENT->smoothScrollKeysFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            $session->writeText(
                '   Page up/page down keys smooth-scroll the window pane               - '
                . $string,
            );

            if (! $axmud::CLIENT->autoSplitKeysFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            $session->writeText(
                '   Page up/page down keys engage split screen mode, if not already on - '
                . $string,
            );

            if (! $axmud::CLIENT->useCompleteKeysFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            $session->writeText(
                '   Tab/cursor up/cursor down keys autocomplete instructions           - '
                . $string,
            );

            if (! $axmud::CLIENT->useSwitchKeysFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            $session->writeText(
                '   CTRL+TAB switches between tabs in a window pane                    - '
                . $string,
            );

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of list (5 keys/key combinations found)',
            );

        # ;twk -s
        } elsif ($switch eq '-s') {

            $axmud::CLIENT->toggle_keysFlag('scroll');
            if (! $axmud::CLIENT->useScrollKeysFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Page up/page down/home/end keys scroll the window pane turned ' . $string,
            );

        # ;twk -m
        } elsif ($switch eq '-m') {

            $axmud::CLIENT->toggle_keysFlag('smooth_scroll');
            if (! $axmud::CLIENT->smoothScrollKeysFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Page up/page down keys smooth-scroll the window pane turned ' . $string,
            );

        # ;twk -p
        } elsif ($switch eq '-p') {

            $axmud::CLIENT->toggle_keysFlag('auto_split');
            if (! $axmud::CLIENT->autoSplitKeysFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Page up/page down keys engage split screen mode, if not already on, turned '
                . $string,
            );

        # ;twk -t
        } elsif ($switch eq '-t') {

            $axmud::CLIENT->toggle_keysFlag('auto_complete');
            if (! $axmud::CLIENT->useCompleteKeysFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Tab/cursor up/cursor down keys autocomplete instructions turned ' . $string,
            );

        # ;twk -c
        } elsif ($switch eq '-c') {

            $axmud::CLIENT->toggle_keysFlag('switch_tab');
            if (! $axmud::CLIENT->useSwitchKeysFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'CTRL+TAB switches between tabs in a window pane turned ' . $string,
            );

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid switch (try \'-s\', \'-m\', \'-p\', \'-t\' or \'-c\')',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ToggleMainWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('togglemainwindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tmw', 'togglemain', 'togglemainwin', 'togglemainwindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles special features of \'main\' windows';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my $string;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;tmw
        if (! defined $switch) {

            # Display header
            $session->writeText(
                'List of special \'main\' window features',
            );

            # Display list
            if (! $axmud::CLIENT->mainWinSystemMsgFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            $session->writeText(
                '   Allow system messages to be displayed in the \'main\' window - ' . $string,
            );

            if (! $axmud::CLIENT->mainWinUrgencyFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            $session->writeText(
                '   Set window\'s urgency hint when text is received from world - ' . $string,
            );

            if (! $axmud::CLIENT->mainWinTooltipFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            $session->writeText(
                '   Show tooltips in the session\'s default tab                 - ' . $string,
            );

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of list (2 features found)',
            );

        # ;tmw -s
        } elsif ($switch eq '-s') {

            if (! $axmud::CLIENT->mainWinSystemMsgFlag) {

                $axmud::CLIENT->set_mainWinSystemMsgFlag(TRUE);
                $string = 'ON';

            } else {

                $axmud::CLIENT->set_mainWinSystemMsgFlag(FALSE);
                $string = 'OFF';
            }

            return $self->complete(
                $session, $standardCmd,
                'Allow system messages to be displayed in the \'main\' window turned ' . $string,
            );

        # ;tmw -u
        } elsif ($switch eq '-u') {

            if (! $axmud::CLIENT->mainWinUrgencyFlag) {

                $axmud::CLIENT->set_mainWinUrgencyFlag(TRUE);
                $string = 'ON';

            } else {

                $axmud::CLIENT->set_mainWinUrgencyFlag(FALSE);
                $string = 'OFF';
            }

            return $self->complete(
                $session, $standardCmd,
                'Set \'main\' window\'s urgency hint when text received from world turned '
                . $string,
            );

        # ;tmw -t
        } elsif ($switch eq '-t') {

            if (! $axmud::CLIENT->mainWinTooltipFlag) {

                $axmud::CLIENT->set_mainWinTooltipFlag(TRUE);
                $string = 'ON';

            } else {

                $axmud::CLIENT->set_mainWinTooltipFlag(FALSE);
                $string = 'OFF';
            }

            return $self->complete(
                $session, $standardCmd,
                'Show tooltips in the session\'s default tab turned ' . $string,
            );

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid switch (try \'-s\', \'-m\', \'-p\', \'-t\' or \'-c\')',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ToggleLabel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('togglelabel', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tlb', 'togglelabel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles toolbar button labels';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $string;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;tlb
        if (! $axmud::CLIENT->toolbarLabelFlag) {

            $axmud::CLIENT->set_toolbarLabelFlag(TRUE);
            $string = 'ON';

        } else {

            $axmud::CLIENT->set_toolbarLabelFlag(FALSE);
            $string = 'OFF';
        }

        # Update all 'main' and automapper windows
        foreach my $winObj ($axmud::CLIENT->desktopObj->ivValues('gridWinHash')) {

            my $stripObj;

            if ($winObj->winType eq 'main') {

                $stripObj = $winObj->getStrip('toolbar');
                if ($stripObj) {

                    $stripObj->resetToolbar();
                }

            } elsif ($winObj->winType eq 'map') {

                $winObj->redrawWidgets('menu_bar', 'toolbar', 'treeview', 'canvas');
            }
        }

        return $self->complete(
            $session, $standardCmd,
            'Toolbar button labels turned ' . $string,
        );
    }
}

{ package Games::Axmud::Cmd::ToggleIrreversible;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('toggleirreversible', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tir', 'toggleir', 'toggleirreversible'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles irreversible icons in \'edit\' windows';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my $string;

        # Check for improper arguments
        if ((defined $switch && $switch ne '-t') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;tir
        if (! defined $switch) {

            if (! $axmud::CLIENT->irreversibleIconFlag) {

                $axmud::CLIENT->set_irreversibleIconFlag(TRUE);
                $string = 'ON';

            } else {

                $axmud::CLIENT->set_irreversibleIconFlag(FALSE);
                $string = 'OFF';
            }

            return $self->complete(
                $session, $standardCmd,
                'Irreversible icons in \'edit\' windows turned ' . $string,
            );

        # ;tir -t
        } else {

            # Show a window containing a button that uses the irreversible icon, regardless of
            #   whether GA::Client->irreversibleIconFlag is set, or not
            $session->mainWin->showIrreversibleTest();

            return $self->complete($session, $standardCmd, 'Irreversible icon test completed');
        }
    }
}

{ package Games::Axmud::Cmd::TogglePopup;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('togglepopup', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tpp', 'togglepop', 'togglepopup'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles popup windows';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $string;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (! $axmud::CLIENT->allowBusyWinFlag) {

            $axmud::CLIENT->set_allowBusyWinFlag(TRUE);
            $string = 'ON';

        } else {

            $axmud::CLIENT->set_allowBusyWinFlag(FALSE);
            $string = 'OFF';
        }

        return $self->complete(
            $session, $standardCmd,
            'Popup windows turned ' . $string,
        );
    }
}

{ package Games::Axmud::Cmd::ToggleShortLink;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('toggleshortlink', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tsl', 'togglelink', 'toggleshortlink'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles detection of short weblinks';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Toggle the flag
        if (! $axmud::CLIENT->shortUrlFlag) {

            $axmud::CLIENT->set_shortUrlFlag(TRUE);

            return $self->complete(
                $session, $standardCmd,
                'Auto-detection of short weblinks turned ON',
            );

        } else {

            $axmud::CLIENT->set_shortUrlFlag(FALSE);

            return $self->complete(
                $session, $standardCmd,
                'Auto-detection of short weblinks turned OFF',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ShowFile;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('showfile', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['shf', 'showfile'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows information about data files';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my (
            $count, $fileObj, $string,
            @list,
            %regHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;shf
        if (! defined $arg) {

            # Get a list of client file objects, and sort them alphabetically
            %regHash = $axmud::CLIENT->fileObjHash;
            @list = sort {
                if ($a->fileType ne $b->fileType) {
                    $a->fileType cmp $b->fileType
                } else {
                    lc($a->name) cmp lc($b->name)
                }
            } (values %regHash);
            # Display the list
            $self->displayList($session, 'Global', @list);
            $count = scalar @list;

            # Get a list of session file objects, and sort them alphabetically
            %regHash = $session->sessionFileObjHash;
            @list = sort {lc($a->name) cmp lc($b->name)} (values %regHash);
            # Display the list
            $self->displayList($session, 'Session', @list);
            $count += scalar @list;

            # Display footer
            if ($count == 1) {

                return $self->complete($session, $standardCmd, 'End of list (1 file displayed)');

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (' . $count . ' files displayed)',
                );
            }

        # ;shf <name>
        } else {

            # Check that the file <name> exists. First look in the GA::Client's file object registry
            if ($axmud::CLIENT->ivExists('fileObjHash', $arg)) {

                $fileObj = $axmud::CLIENT->ivShow('fileObjHash', $arg);
                $string = 'global';

            # The check in the GA::Session's file object registry
            } elsif ($session->ivExists('sessionFileObjHash', $arg)) {

                $fileObj = $session->ivShow('sessionFileObjHash', $arg);
                $string = 'session';

            } else {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised data file \'' . $arg . '\'',
                );
            }

            # Display header
            if ($fileObj->modifyFlag) {

                $session->writeText(
                    'Metadata for the ' . $string . ' file \'' . $arg . '\' (data modified and not'
                    . ' saved)',
                );

            } else {

                $session->writeText(
                    'Metadata for the ' . $string . ' file \'' . $arg . '\' (data not modified)',
                );
            }

            # Display list
            $session->writeText('   File type     : ' . $fileObj->fileType);
            if (defined $fileObj->scriptName) {

                $session->writeText(
                    '   Script         : ' . $fileObj->scriptName . ' v' . $fileObj->scriptVersion,
                );

                $session->writeText(
                    '   Saved at       : ' . $fileObj->saveDate . ', ' . $fileObj->saveTime,
                );

            } else {

                $session->writeText('   Metadata       : (not set)');
            }

            if (defined $fileObj->actualFileName) {

                $session->writeText('   Actual file    : ' . $fileObj->actualFileName);
                $session->writeText('   Path           : ' . $fileObj->actualPath);
                $session->writeText('   Directory      : ' . $fileObj->actualDir);

            } else {

                $session->writeText('   Actual file    : (not set)');
            }

            if (defined $fileObj->standardFileName) {

                $session->writeText('   Standard file  : ' . $fileObj->standardFileName);
                $session->writeText('   Path           : <script_dir>' . $fileObj->standardPath);
                $session->writeText('   Directory      : <script_dir>' . $fileObj->standardDir);

            } else {

                $session->writeText('   Standard file  : (not set)');
            }

            if (defined $fileObj->altFileName) {

                $session->writeText('   Alternative    : <script_dir>' . $fileObj->altFileName);
                $session->writeText('   (Path)         : ' . $fileObj->altPath);
            }

            if (defined $fileObj->assocWorldProf) {
                $session->writeText('   Assoc\'d prof   : ' . $fileObj->assocWorldProf);
            } else {
                $session->writeText('   Assoc\'d prof   : (not set)');
            }

            # Display footer
            return $self->complete($session, $standardCmd, 'End of file metadata');
        }
    }

    sub displayList {

        # Called by $self->do
        # Shows a list of file objects
        #
        # Expected arguments
        #   $session    - The calling function's GA::Session
        #   $type       - 'Global' or 'Session'
        #
        # Optional arguments
        #   @list       - The list of file objects to show (may be empty, but very unlikely)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $session, $type, @list) = @_;

        # Local variables
        my $string;

        # Check for improper arguments
        if (! defined $session || ! defined $type) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->displayList', @_);
        }

        if (! @list) {

            $session->writeText('(No global file objects found');

        } else {

            # Display header
            if ($type eq 'Global') {

                $string = 'File permissions: config ';
                if ($axmud::CLIENT->loadConfigFlag) {
                    $string .= 'load/';
                } else {
                    $string .= '-/';
                }

                if ($axmud::CLIENT->loadConfigFlag) {
                    $string .= 'save';
                } else {
                    $string .= '-';
                }

                $string .= ', data ';
                if ($axmud::CLIENT->loadConfigFlag) {
                    $string .= 'load/';
                } else {
                    $string .= '-/';
                }

                if ($axmud::CLIENT->loadConfigFlag) {
                    $string .= 'save';
                } else {
                    $string .= '-';
                }

                $session->writeText($string);
            }

            $session->writeText(
                $type . ' list of data files (* - not saved, T - temporary world)',
            );

            $session->writeText('    File type  File name        Path');

            # Display list
            foreach my $fileObj (@list) {

                my ($column, $worldObj);

                if ($fileObj->modifyFlag) {
                    $column = ' *';
                } else {
                    $column = '  ';
                }

                if (
                    $fileObj->fileType eq 'worldprof'
                    || $fileObj->fileType eq 'otherprof'
                    || $fileObj->fileType eq 'worldmodel'
                ) {
                    $worldObj
                        = $axmud::CLIENT->ivShow('worldProfHash', $fileObj->assocWorldProf);

                    if ($worldObj && $worldObj->noSaveFlag) {
                        $column .= 'T ';
                    } else {
                        $column .= '  ';
                    }

                } else {

                    $column .= '  ';
                }


                if (defined $fileObj->actualPath) {

                    if (length ($fileObj->actualPath) > 50) {

                        $session->writeText(
                            $column . sprintf(
                                '%-10.10s %-16.16s %-50.50s...',
                                $fileObj->fileType,
                                $fileObj->name,
                                $fileObj->actualPath,
                            )
                        );

                    } else {

                        $session->writeText(
                            $column . sprintf(
                                '%-10.10s %-16.16s %-50.50s',
                                $fileObj->fileType,
                                $fileObj->name,
                                $fileObj->actualPath,
                            )
                        );
                    }
                } else {

                    $session->writeText(
                        $column . sprintf(
                            '%-10.10s %-16.16s (none)',
                            $fileObj->fileType,
                            $fileObj->name,
                        )
                    );
                }
            }
        }

        return 1;
    }
}

{ package Games::Axmud::Cmd::DisableSaveLoad;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('disablesaveload', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dsl', 'disablesaveload'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Disables saving/loading of all files';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $worldObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (
            ! $axmud::CLIENT->loadConfigFlag
            && ! $axmud::CLIENT->saveConfigFlag
            && ! $axmud::CLIENT->loadDataFlag
            && ! $axmud::CLIENT->saveDataFlag
        ) {
            return $self->error(
                $session, $inputString,
                'File save/load is already disabled in all sessions',
            );

        } else {

            # Disable loading/saving of all files
            $axmud::CLIENT->set_loadConfigFlag(FALSE);
            $axmud::CLIENT->set_saveConfigFlag(FALSE);
            $axmud::CLIENT->set_loadDataFlag(FALSE);
            $axmud::CLIENT->set_saveDataFlag(FALSE);

            return $self->complete(
                $session, $standardCmd,
                'File save/load has been disabled in all sessions (use \';emergencysave\' if'
                . ' you need to override this)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DisableSaveWorld;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('disablesaveworld', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dsw', 'disablesaveworld'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Disables saving files associated with a world';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $world,
            $check,
        ) = @_;

        # Local variables
        my $worldObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (
            ! $axmud::CLIENT->loadConfigFlag
            && ! $axmud::CLIENT->saveConfigFlag
            && ! $axmud::CLIENT->loadDataFlag
            && ! $axmud::CLIENT->saveDataFlag
        ) {
            return $self->error(
                $session, $inputString,
                'File save/load is already disabled in all sessions',
            );
        }

        # The default world is the session's current world
        if (! $world) {

            $world = $session->currentWorld->name;
        }

        # Check the world exists
        if (! $axmud::CLIENT->ivExists('worldProfHash', $world)) {

            return $self->error(
                $session, $inputString,
                'Unrecognised world profile \'' . $world . '\'',
            );

        } else {

            $worldObj = $axmud::CLIENT->ivShow('worldProfHash', $world);
        }

        # Check saves are not already disabled
        if ($worldObj->noSaveFlag) {

            return $self->error(
                $session, $inputString,
                'Saving of files for the \'' . $world . '\' world has already been disabled',
            );

        } else {

            $worldObj->ivPoke('noSaveFlag', TRUE);

            return $self->complete(
                $session, $standardCmd,
                'Saving of files for the \'' . $world . '\' world has been disabled (use'
                . ' \';emergencysave\' if you need to override this)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Save;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('save', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sv', 'save'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Saves a file (or files)';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $forceFlag, $allSessionFlag, $configFlag, $allProfFlag, $tasksFlag,
            $scriptsFlag, $contactsFlag, $dictsFlag, $toolbarFlag, $userCmdFlag, $zonemapsFlag,
            $winmapsFlag, $ttsFlag, $currentWorldFlag, $otherWorldFlag, $worldNoModelFlag,
            $modelFlag, $count, $errorCount, $saveMsg,
            @otherWorldList,
            %fileObjHash, %profHash, %worldHash, %otherHash,
        );

        # Check that saving is allowed at all
        if (! $axmud::CLIENT->saveConfigFlag && ! $axmud::CLIENT->saveDataFlag) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            # If the session has just disconnected (or if the client is shutting down), show a
            #   normal message; otherwise, show an error message
            if ($session->status eq 'disconnected' || $axmud::CLIENT->shutdownFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'No files saved (file operations disabled)',
                );

            } else {

                return $self->error(
                    $session, $inputString,
                    'File operations disabled in all sessions',
                );
            }
        }

        # Extract the force-save switches
        ($switch, @args) = $self->extract('-f', 0, @args);
        if (defined $switch) {

            $forceFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-a', 0, @args);
        if (defined $switch) {

            $allSessionFlag = TRUE;
        }

        # ;sv -a
        # ;sv -f -a
        if ($allSessionFlag && @args) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'The switch -a can be combined with -f, but not with other arguments',
            );
        }

        # ; sv
        # ; sv -f
        if (! @args) {

            # Set all flags, so that any unsaved files will be saved
            $configFlag = TRUE;
            $allProfFlag = TRUE;
            $tasksFlag = TRUE;
            $scriptsFlag = TRUE;
            $contactsFlag = TRUE;
            $dictsFlag = TRUE;
            $toolbarFlag = TRUE;
            $userCmdFlag = TRUE;
            $zonemapsFlag = TRUE;
            $winmapsFlag = TRUE;
            $ttsFlag = TRUE;

        # ; sv <options>
        # ; sv -f <options>
        # ; sv <options> -f
        } else {

            # Extract more switches
            ($switch, @args) = $self->extract('-i', 0, @args);
            if (defined $switch) {

                $configFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-d', 0, @args);
            if (defined $switch) {

                $allProfFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-c', 0, @args);
            if (defined $switch) {

                $currentWorldFlag = TRUE;
            }

            # Multiple world profiles can be specified (with the -o pattern)
            do {

                my $name;

                ($switch, $name, @args) = $self->extract('-o', 1, @args);
                if (defined $switch) {

                    $otherWorldFlag = TRUE;
                    push (@otherWorldList, $name);
                }

            } until (! defined $switch);

            # Extract remaining switches
            ($switch, @args) = $self->extract('-w', 0, @args);
            if (defined $switch) {

                $worldNoModelFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-m', 0, @args);
            if (defined $switch) {

                $modelFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-t', 0, @args);
            if (defined $switch) {

                $tasksFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-s', 0, @args);
            if (defined $switch) {

                $scriptsFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-n', 0, @args);
            if (defined $switch) {

                $contactsFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-y', 0, @args);
            if (defined $switch) {

                $dictsFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-b', 0, @args);
            if (defined $switch) {

                $toolbarFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-u', 0, @args);
            if (defined $switch) {

                $userCmdFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-z', 0, @args);
            if (defined $switch) {

                $zonemapsFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-p', 0, @args);
            if (defined $switch) {

                $winmapsFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-x', 0, @args);
            if (defined $switch) {

                $ttsFlag = TRUE;
            }

            # @args should now contain 0, 1 or more arguments. Any remaining arguments must be one
            #   of the strings 'config', 'worldmodel', 'tasks', 'scripts', 'contacts', 'dicts',
            #   'toolbar', 'usercmds', 'zonemaps', 'winmaps', 'tts'
            while (@args) {

                my $string = shift @args;

                if ($string eq 'config') {
                    $configFlag = TRUE;
                } elsif ($string eq 'worldmodel') {
                    $modelFlag = TRUE;
                } elsif ($string eq 'tasks') {
                    $tasksFlag = TRUE;
                } elsif ($string eq 'scripts') {
                    $scriptsFlag = TRUE;
                } elsif ($string eq 'contacts') {
                    $contactsFlag = TRUE;
                } elsif ($string eq 'dicts') {
                    $dictsFlag = TRUE;
                } elsif ($string eq 'toolbar') {
                    $toolbarFlag = TRUE;
                } elsif ($string eq 'usercmds') {
                    $userCmdFlag = TRUE;
                } elsif ($string eq 'zonemaps') {
                    $zonemapsFlag = TRUE;
                } elsif ($string eq 'winmaps') {
                    $winmapsFlag = TRUE;
                } elsif ($string eq 'tts') {
                    $ttsFlag = TRUE;

                } elsif ($string eq 'worldprof' || $string eq 'otherprof') {

                    $axmud::CLIENT->set_fileFailFlag(TRUE);

                    return $self->error(
                        $session, $inputString,
                        '\'' . $string . '\' files can\'t be referenced by name with this command',
                    );

                } elsif ($session->ivExists('profHash', $string)) {

                    $axmud::CLIENT->set_fileFailFlag(TRUE);

                    return $self->error(
                        $session, $inputString,
                        'Profile files can\'t be referenced by name with this command',
                    );

                } else {

                    $axmud::CLIENT->set_fileFailFlag(TRUE);

                    return $self->error(
                        $session, $inputString,
                        'Unrecognised file \'' . $string . '\'',
                    );
                }
            }

            # If <world> was specified, it must be a world profile that's not the current one
            if (@otherWorldList) {

                foreach my $world (@otherWorldList) {

                    my $profObj;

                    if (! $axmud::CLIENT->ivExists('worldProfHash', $world)) {

                        $axmud::CLIENT->set_fileFailFlag(TRUE);

                        return $self->error(
                            $session, $inputString,
                            'Unrecognised world profile \'' . $world . '\'',
                        );

                    } else {

                        $profObj = $axmud::CLIENT->ivShow('worldProfHash', $world);
                    }

                    if ($profObj->category ne 'world') {

                        $axmud::CLIENT->set_fileFailFlag(TRUE);

                        return $self->error(
                            $session, $inputString,
                            'The \'' . $world . '\' profile isn\'t a world profile',
                        );

                    } elsif ($world eq $session->currentWorld->name) {

                        $axmud::CLIENT->set_fileFailFlag(TRUE);

                        return $self->error(
                            $session, $inputString,
                            'The current world profile can\'t be referenced by name with this'
                            . ' command (but other world profile can)',
                        );
                    }
                }
            }
        }

        # Import the client's hash of file objects (which only contains file objects used by every
        #   session)
        %fileObjHash = $axmud::CLIENT->fileObjHash;
        # Get a hash of file objects for world profiles
        foreach my $fileName (keys %fileObjHash) {

            my $fileObj = $fileObjHash{$fileName};

            if ($fileObj->fileType eq 'worldprof') {

                $profHash{$fileName} = $fileObj;
            }
        }

        # Compile two hashes of files to save, so that certain types of files can be saved before
        #   others. Hashes in the form
        #   $worldHash{world_profile_file_object_name} = undef
        #   $otherHash{other_profile_file_object_name} = undef
        if ($configFlag) {

            $otherHash{'config'} = undef;
        }

        if ($allProfFlag) {

            %worldHash = %profHash;
            $otherHash{'otherprof'} = undef;
            $otherHash{'worldmodel'} = undef;
            $otherHash{'config'} = undef;
        }

        if ($currentWorldFlag) {

            $worldHash{$session->currentWorld->name} = undef;
            $otherHash{'otherprof'} = undef;
            $otherHash{'worldmodel'} = undef;
            $otherHash{'config'} = undef;
        }

        if ($otherWorldFlag) {

            # Add every specified world
            foreach my $world (@otherWorldList) {

                $worldHash{$world} = undef;
            }

            $otherHash{'config'} = undef;
        }

        if ($worldNoModelFlag) {

            $worldHash{$session->currentWorld->name} = undef;
            $otherHash{'otherprof'} = undef;
            $otherHash{'config'} = undef;
        }

        if ($modelFlag) {

            $otherHash{'worldmodel'} = undef;
        }

        if ($tasksFlag) {

            $otherHash{'tasks'} = undef;
        }

        if ($scriptsFlag) {

            $otherHash{'scripts'} = undef;
        }

        if ($contactsFlag) {

            $otherHash{'contacts'} = undef;
        }

        if ($dictsFlag) {

            $otherHash{'dicts'} = undef;
        }

        if ($toolbarFlag) {

            $otherHash{'toolbar'} = undef;
        }

        if ($userCmdFlag) {

            $otherHash{'usercmds'} = undef;
        }

        if ($zonemapsFlag) {

            $otherHash{'zonemaps'} = undef;
        }

        if ($winmapsFlag) {

            $otherHash{'winmaps'} = undef;
        }

        if ($ttsFlag) {

            $otherHash{'tts'} = undef;
        }

        # Check to be safe - check at least one file has been marked for saving
        if (! %worldHash && ! %otherHash) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->complete($session, $standardCmd, 'No files to save');
        }

        # Saves the files in the order (1) 'config', (2) world profiles, (3) 'otherprof', (4)
        #   everything else
        $count = 0;
        $errorCount = 0;
        # For large files (e.g. world models containing tens of thousands of rooms), we need to
        #   display an initial message to explain the pause
        # However, in blind mode don't display a message at all; speech engine struggle to read
        #   'file(s)' correctly, and those users are probably not using the automapper anyway, so
        #   file saves will be more or less instantaneous
        if (! $axmud::BLIND_MODE_FLAG) {

            $session->writeText('Saving file(s)...');
        }

        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->do');

        # (1) 'config'
        if (exists $otherHash{'config'}) {

            my $fileObj = $fileObjHash{'config'};

            if ($fileObj->modifyFlag || $forceFlag) {

                if ($fileObj->saveConfigFile()) {
                    $count++;
                } else {
                    $errorCount++;
                }
            }

            # Only save it once
            delete $otherHash{'config'};
        }

        # (2) world profiles
        foreach my $file (keys %worldHash) {

            my $fileObj = $fileObjHash{$file};

            if ($fileObj->modifyFlag || $forceFlag) {

                if ($fileObj->saveDataFile()) {
                    $count++;
                } else {
                    $errorCount++;
                }
            }
        }

        # (3) 'otherprof'
        if (exists $otherHash{'otherprof'}) {

            my $fileObj = $session->ivShow('sessionFileObjHash', 'otherprof');

            if ($fileObj->modifyFlag || $forceFlag) {

                if ($fileObj->saveDataFile()) {
                    $count++;
                } else {
                    $errorCount++;
                }
            }

            # Only save it once
            delete $otherHash{'otherprof'};
        }

        # (4) everything else
        OUTER: foreach my $file (keys %otherHash) {

            my $fileObj;

            if (exists $fileObjHash{$file}) {
                $fileObj = $fileObjHash{$file};
            } else {
                $fileObj = $session->ivShow('sessionFileObjHash', $file);
            }

            if ($fileObj->modifyFlag || $forceFlag) {

                if ($fileObj->saveDataFile()) {
                    $count++;
                } else {
                    $errorCount++;
                }
            }
        }

        if ($count == 0 && $errorCount > 0) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error($session, $inputString, 'Files saved: 0, errors: ' . $errorCount);

        } else {

            # If the Automapper window is open, its 'free click mode' must be reset after a save
            if ($session->mapWin) {

                $session->mapWin->reset_freeClickMode();
            }

            if ($allSessionFlag) {

                if (! $forceFlag) {

                    # Save modified files in all sessions, except this one
                    $axmud::CLIENT->broadcastInstruct(';save', $session);

                    $saveMsg = 'Files saved: ' . $count . ', errors: ' . $errorCount
                                    . ' (also saved files in other sessions)';

                } else {

                    # Force-save files in all sessions, except this one
                    $axmud::CLIENT->broadcastInstruct(';save -f', $session);

                    $saveMsg = 'Files saved: ' . $count . ', errors: ' . $errorCount
                                . ' (also force-saved files in other sessions)';
                }

            } else {

                $saveMsg = 'Files saved: ' . $count . ', errors: ' . $errorCount;
            }

            if (! $axmud::BLIND_MODE_FLAG) {

                return $self->complete($session, $standardCmd, $saveMsg);

            } else {

                # The success message appears whenever a blind user stops a session, and they
                #   probably don't care how many files have been saved, so just confirm that they
                #   have been saved
                return $self->complete($session, $standardCmd, 'Files saved');
            }
        }
    }
}

{ package Games::Axmud::Cmd::Load;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('load', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['load'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Loads a file (or files)';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $count, $loadCount, $msg, $result, $errorCount,
            %loadHash,
        );

        # Check that loading is allowed at all
        if (! $axmud::CLIENT->loadDataFlag) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'File load/save is disabled in all sessions',
            );
        }

        # ;load <options>
        if (@args) {

            # Go through the list, eliminating any duplicates by compiling %loadHash, in the form
            #   $loadHash{file_object_name} = undef
            while (@args) {

                my $string = shift @args;

                if (
                    $string eq 'worldmodel' || $string eq 'tasks' || $string eq 'scripts'
                    || $string eq 'contacts' || $string eq 'dicts' || $string eq 'toolbar'
                    || $string eq 'usercmds' || $string eq 'zonemaps' || $string eq 'winmaps'
                    || $string eq 'tts'
                ) {
                    $loadHash{$string} = undef;
                } elsif ($string eq '-m') {
                    $loadHash{'worldmodel'} = undef;
                } elsif ($string eq '-t') {
                    $loadHash{'tasks'} = undef;
                } elsif ($string eq '-s') {
                    $loadHash{'scripts'} = undef;
                } elsif ($string eq '-n') {
                    $loadHash{'contacts'} = undef;
                } elsif ($string eq '-y') {
                    $loadHash{'dicts'} = undef;
                } elsif ($string eq '-b') {
                    $loadHash{'toolbar'} = undef;
                } elsif ($string eq '-u') {
                    $loadHash{'usercmds'} = undef;
                } elsif ($string eq '-z') {
                    $loadHash{'zonemaps'} = undef;
                } elsif ($string eq '-p') {
                    $loadHash{'winmaps'} = undef;
                } elsif ($string eq '-x') {
                    $loadHash{'tts'} = undef;

                } elsif ($string eq 'config' || $string eq 'worldprof' || $string eq 'otherprof') {

                    $axmud::CLIENT->set_fileFailFlag(TRUE);

                    return $self->error(
                        $session, $inputString,
                        'The file \'' . $string . '\' can\'t be loaded with this command',
                    );

                } elsif (
                    $axmud::CLIENT->ivExists('worldProfHash', $string)
                    || $session->ivExists('profHash', $string)
                ) {
                    $axmud::CLIENT->set_fileFailFlag(TRUE);

                    return $self->error(
                        $session, $inputString,
                        'Profile files can\'t be loaded with this command',
                    );

                } else {

                    $axmud::CLIENT->set_fileFailFlag(TRUE);

                    return $self->error(
                        $session, $inputString,
                        'Unrecognised file \'' . $string . '\'',
                    );
                }
            }

        # ;load
        } else {

            # Mark the nine (allowed) files for loading
            $loadHash{'worldmodel'} = undef;
            $loadHash{'tasks'} = undef;
            $loadHash{'scripts'} = undef;
            $loadHash{'contacts'} = undef;
            $loadHash{'dicts'} = undef;
            $loadHash{'toolbar'} = undef;
            $loadHash{'usercmds'} = undef;
            $loadHash{'zonemaps'} = undef;
            $loadHash{'winmaps'} = undef;
            $loadHash{'tts'} = undef;
        }

        # Check to be safe - check at least one file has been marked for loading
        if (! %loadHash) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->complete($session, $standardCmd, 'No files to load');
        }

        # Count how many of the selected files are marked as needing to be saved (so that loading
        #   the file would cause the data in memory to be lost)
        $count = 0;
        $loadCount = 0;
        foreach my $file (keys %loadHash) {

            my $fileObj;

            $loadCount++;

            if ($file eq 'worldmodel') {
                $fileObj = $session->ivShow('sessionFileObjHash', $file);
            } else {
                $fileObj = $axmud::CLIENT->ivShow('fileObjHash', $file);
            }

            if ($fileObj && $fileObj->modifyFlag) {

                $count++;
            }
        }

        # Ask for permission to load any files that will cause data in memory to be lost
        if ($count) {

            if ($count == 1 && $loadCount == 1) {

                $msg = 'The file you have specified will overwrite unsaved data in memory. Load'
                . ' it anyway?';

            } elsif ($count != 1 && $loadCount == 1) {

                $msg = '1 of the files you have specified will overwrite unsaved data in'
                . ' memory. Load it anyway?';

            } else {

                $msg = $count . ' of the ' . $loadCount . ' files you have specified will'
                . ' overwite unsaved data in memory. Load them anyway?';
            }

            $result = $session->mainWin->showMsgDialogue(
                'Overwrite unsaved data',
                'question',
                $msg,
                'yes-no',
            );

            if ($result eq 'no') {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->complete($session, $standardCmd, 'No files loaded');
            }
        }

        # For large files (e.g. world model containing tens of thousands of rooms), we need to
        #   display an initial message to explain the pause
        $session->writeText('Loading file(s)...');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->do');

        # Load every file in the hash
        $count = 0;
        $errorCount = 0;
        foreach my $file (keys %loadHash) {

            my $fileObj;

            if ($file eq 'worldmodel') {
                $fileObj = $session->ivShow('sessionFileObjHash', $file);
            } else {
                $fileObj = $axmud::CLIENT->ivShow('fileObjHash', $file);
            }

            # Load the file, replacing data stored in memory
            if (! $fileObj->loadDataFile()) {

                # Try loading the automatic backup, i.e. 'tasks.axm.bu'
                if (! $fileObj->loadDataFile(undef, undef, undef, TRUE)) {
                    $errorCount++;
                } else {
                    $count++;
                }

            } else {
                $count++;
            }
        }

        if ($count == 0 && $errorCount > 0) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error($session, $inputString, 'Files loaded: 0, errors: ' . $errorCount);

        } else {

            # Multiple sessions can connect to the same world (in online or offline mode) and, in
            #   that case, GA::Session->setupProfiles uses the same GA::Obj::WorldModel for each
            # If a new world model has been loaded, it must be applied to each of those sessions,
            #   and their automapper objects/windows must be reset
            if (exists $loadHash{'worldmodel'}) {

                # Calling GA::Session->set_worldModelObj to replace the value with the same value
                #   has the fortunate effect of handling everything
                $session->set_worldModelObj($session->worldModelObj);
            }

            return $self->complete(
                $session, $standardCmd,
                'Files loaded: ' . $count . ', errors: ' . $errorCount,
            );
        }
    }
}

{ package Games::Axmud::Cmd::AutoSave;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('autosave', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ats', 'autosave'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Turns auto-saves on/off';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my $msg;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;ats
        if (! $arg) {

            if ($axmud::CLIENT->autoSaveFlag) {

                $msg = 'Auto-saves are turned on';
                if ($session->autoSaveLastTime) {

                    if ($session->autoSaveLastTime > ($session->sessionTime - 60)) {

                        $msg .= ', last save < 1 min ago';

                    } elsif ($session->autoSaveLastTime > ($session->sessionTime - 120)) {

                        $msg .= ', last save 1 min ago';

                    } else {

                        $msg .= ', last save '
                                . int(($session->sessionTime - $session->autoSaveLastTime) / 60)
                                . ' minutes ago';
                    }
                }

            } else {

                $msg = 'Auto-saves are turned off';
            }

            if ($axmud::CLIENT->autoSaveWaitTime == 1) {
                $msg .= ' (auto-save interval set to 1 minute)';
            } else {
                $msg .= ' (auto-save interval set to ' . $axmud::CLIENT->autoSaveWaitTime
                            . ' minutes)';
            }

            return $self->complete($session, $standardCmd, $msg);

        # ;ats on
        } elsif ($arg eq 'on') {

            if ($axmud::CLIENT->autoSaveFlag) {

                return $self->error($session, $inputString, 'Auto-saves are already turned on');

            } else {

                $axmud::CLIENT->set_autoSaveFlag(TRUE);
                # For each session, set the time at which the next auto-save will occur
                foreach my $thisSession ($axmud::CLIENT->listSessions()) {

                    $thisSession->resetAutoSave();
                }

                return $self->complete(
                    $session, $standardCmd,
                    'Auto-saves turned on (data will be saved every '
                    . $axmud::CLIENT->autoSaveWaitTime . ' minutes)',
                );
            }

        # ;ats off
        } elsif ($arg eq 'off') {

            if (! $axmud::CLIENT->autoSaveFlag) {

                return $self->error(
                    $session, $inputString,
                    'Auto-saves are already turned off',
                );

            } else {

                $axmud::CLIENT->set_autoSaveFlag(FALSE);
                foreach my $thisSession ($axmud::CLIENT->listSessions()) {

                    $thisSession->resetAutoSave();
                }

                return $self->complete($session, $standardCmd, 'Auto-save turned off');
            }

        # ;ats <minutes>
        } else {

            if (! $axmud::CLIENT->intCheck($arg, 1)) {

                return $self->error(
                    $session, $inputString,
                    'Auto-save time must be an integer greater than 0',
                );

            } else {

                $axmud::CLIENT->set_autoSaveWaitTime($arg);

                if ($axmud::CLIENT->autoSaveWaitTime == 1) {
                    $msg = 'Auto-save time set to 1 minute';
                } else {
                    $msg = 'Auto-save time set to ' . $axmud::CLIENT->autoSaveWaitTime . ' minutes';
                }

                # For each session, set the time at which the next auto-save will occur (but not
                #   if auto-saves are currently turned off)
                if ($axmud::CLIENT->autoSaveFlag) {

                    foreach my $thisSession ($axmud::CLIENT->listSessions()) {

                        $thisSession->resetAutoSave();
                    }

                    $msg .= ' (auto-saves currently turned on)';

                } else {

                    $msg .= ' (auto-saves currently turned off)';
                }

                return $self->complete(
                    $session, $standardCmd,
                    $msg,
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::EmergencySave;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('emergencysave', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ems', 'emsave', 'emergencysave'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Performs an emergency save';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $dir;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Perform the emergency save
        $dir = $axmud::CLIENT->doEmergencySave();

        if (! $dir) {

            # (The user cancelled the operation)
            return $self->error($session, $inputString, 'Emergency save not performed');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Emergency save complete (' . $dir . ')',
            )
        }
    }
}

{ package Games::Axmud::Cmd::ExportFiles;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);
    # Include module here, as well as in axmud.pl, so that .../t/00-compile.t won't fail
    use Archive::Tar;

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('exportfiles', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['exf', 'exportfile', 'exportfiles'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Exports a file (or files)';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $string, $worldFlag,  $modelFlag, $tasksFlag, $scriptsFlag, $contactsFlag,
            $dictsFlag, $toolbarFlag, $userCmdFlag, $zonemapsFlag, $winmapsFlag, $ttsFlag,
            $exportPath, $tarObj, $total,
            @exportList, @namedWorldList, @namedModelList, @combinedList,
            %miniFileHash,
        );

        # Check that saving is allowed at all
        if (! $axmud::CLIENT->saveConfigFlag && ! $axmud::CLIENT->saveDataFlag) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'File load/save is disabled in all sessions',
            );
        }

        # ;exp
        if (! @args) {

            # Compile a list of all the files that (should be) in the /data/ directory, not
            #   including temp files, or files for which Axmud has lost track
            foreach my $worldObj ($axmud::CLIENT->ivValues('worldProfHash')) {

                push (@exportList,
                    'data/worlds/' . $worldObj->name . '/worldprof.axm',
                    'data/worlds/' . $worldObj->name . '/otherprof.axm',
                    'data/worlds/' . $worldObj->name . '/worldmodel.axm'
                   );
            }

            push (@exportList, 'data/tasks.axm');
            push (@exportList, 'data/scripts.axm');
            push (@exportList, 'data/contacts.axm');
            push (@exportList, 'data/dicts.axm');
            push (@exportList, 'data/toolbar.axm');
            push (@exportList, 'data/usercmds.axm');
            push (@exportList, 'data/zonemaps.axm');
            push (@exportList, 'data/winmaps.axm');
            push (@exportList, 'data/tts.axm');

        # ;exp <options>
        } else {

            # Extract all the -w switch options
            do {

                ($switch, $string, @args) = $self->extract('-w', 1, @args);
                if (defined $switch) {

                    $worldFlag = TRUE;

                    if (defined $string) {

                        push (@namedWorldList, $string);

                    } else {

                        $axmud::CLIENT->set_fileFailFlag(TRUE);

                        return $self->error($session, $inputString, 'Export which world?');
                    }
                }

            } until (! defined $switch);

            # Extract all the -m switch options
            do {

                ($switch, $string, @args) = $self->extract('-m', 1, @args);
                if (defined $switch) {

                    $modelFlag = TRUE;

                    if (defined $string) {

                        push (@namedModelList, $string);

                    } else {

                        $axmud::CLIENT->set_fileFailFlag(TRUE);

                        return $self->error($session, $inputString, 'Export which world model?');
                    }
                }

            } until (! defined $switch);

            # Extract remaining switches patterns
            ($switch, @args) = $self->extract('-t', 0, @args);
            if (defined $switch) {

                $tasksFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-s', 0, @args);
            if (defined $switch) {

                $scriptsFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-n', 0, @args);
            if (defined $switch) {

                $contactsFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-y', 0, @args);
            if (defined $switch) {

                $dictsFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-b', 0, @args);
            if (defined $switch) {

                $toolbarFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-u', 0, @args);
            if (defined $switch) {

                $userCmdFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-z', 0, @args);
            if (defined $switch) {

                $zonemapsFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-p', 0, @args);
            if (defined $switch) {

                $winmapsFlag = TRUE;
            }

            ($switch, @args) = $self->extract('-x', 0, @args);
            if (defined $switch) {

                $ttsFlag = TRUE;
            }

            # @args should now contain 0, 1 or more arguments. Any remaining arguments must be one
            #   of the strings 'tasks', 'contacts', 'dicts', 'toolbar', 'usercmds', 'zonemaps',
            #   'winmaps' or 'tts'
            while (@args) {

                my $string = shift @args;

                if ($string eq 'tasks') {
                    $tasksFlag = TRUE;
                } elsif ($string eq 'scripts') {
                    $scriptsFlag = TRUE;
                } elsif ($string eq 'contacts') {
                    $contactsFlag = TRUE;
                } elsif ($string eq 'dicts') {
                    $dictsFlag = TRUE;
                } elsif ($string eq 'toolbar') {
                    $toolbarFlag = TRUE;
                } elsif ($string eq 'usercmds') {
                    $userCmdFlag = TRUE;
                } elsif ($string eq 'zonemaps') {
                    $zonemapsFlag = TRUE;
                } elsif ($string eq 'winmaps') {
                    $winmapsFlag = TRUE;
                } elsif ($string eq 'tts') {
                    $ttsFlag = TRUE;

                } elsif (
                    $string eq 'worldprof' || $string eq 'otherprof' || $string eq 'worldmodel'
                    || $string eq 'config'
                ) {
                    $axmud::CLIENT->set_fileFailFlag(TRUE);

                    return $self->error(
                        $session, $inputString,
                        '\'' . $string . '\' files can\'t be referenced by name with this command',
                    );

                } else {

                    $axmud::CLIENT->set_fileFailFlag(TRUE);

                    return $self->error(
                        $session, $inputString,
                        'Unrecognised file \'' . $string . '\'',
                    );
                }
            }

            # If <world> was specified, check it exists
            @combinedList = (@namedWorldList, @namedModelList);

            foreach my $world (@combinedList) {

                my $profObj;

                if (! $axmud::CLIENT->ivExists('worldProfHash', $world)) {

                    $axmud::CLIENT->set_fileFailFlag(TRUE);

                    return $self->error(
                        $session, $inputString,
                        'Unrecognised world profile \'' . $world . '\'',
                    );

                } else {

                    $profObj = $axmud::CLIENT->ivShow('worldProfHash', $world);
                }

                if ($profObj->category ne 'world') {

                    $axmud::CLIENT->set_fileFailFlag(TRUE);

                    return $self->error(
                        $session, $inputString,
                        'The profile \'' . $world . '\' isn\'t a world profile',
                    );
                }
            }

            # Compile a list of files to export
            if ($worldFlag) {

                foreach my $world (@namedWorldList) {

                    push(@exportList,
                        'data/worlds/' . $world . '/worldprof.axm',
                        'data/worlds/' . $world . '/otherprof.axm',
                        'data/worlds/' . $world . '/worldmodel.axm',
                    );
                }
            }

            if ($modelFlag) {

                foreach my $world (@namedModelList) {

                    push(@exportList, 'data/worlds/' . $world . '/worldmodel.axm');
                }
            }

            if ($tasksFlag) {

                push (@exportList, 'data/tasks.axm');
            }

            if ($scriptsFlag) {

                push (@exportList, 'data/scripts.axm');
            }

            if ($contactsFlag) {

                push (@exportList, 'data/contacts.axm');
            }

            if ($dictsFlag) {

                push (@exportList, 'data/dicts.axm');
            }

            if ($toolbarFlag) {

                push (@exportList, 'data/toolbar.axm');
            }

            if ($userCmdFlag) {

                push (@exportList, 'data/usercmds.axm');
            }

            if ($zonemapsFlag) {

                push (@exportList, 'data/zonemaps.axm');
            }

            if ($winmapsFlag) {

                push (@exportList, 'data/winmaps.axm');
            }

            if ($ttsFlag) {

                push (@exportList, 'data/tts.axm');
            }

            if ($worldFlag || $modelFlag) {

                foreach my $world (@namedModelList) {

                    my ($count, $exitFlag);

                    # Large world models are split into multiple files. Make sure we're exporting
                    #   all of them together
                    $count = 0;
                    do {

                        my $miniFile;

                        $count++;
                        $miniFile = 'data/worlds/' . $world . '/worldmodel_' . $count . '.axm';
                        if (! -e $axmud::DATA_DIR . '/' . $miniFile) {

                            $exitFlag = TRUE;

                        } else {

                            push (@exportList, $miniFile);
                            # At the end of this function, when listing the files exported, only
                            #   include the main world model file
                            $miniFileHash{$miniFile} = undef;
                        }

                    } until ($exitFlag);
                }
            }
        }

        # Check at least one file has been marked for exporting
        if (! @exportList) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->complete($session, $standardCmd, 'No files to export');
        }

        # Check that all the files in @exportList actually exist
        foreach my $file (@exportList) {

            if (! (-e $axmud::DATA_DIR . '/' . $file)) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->error(
                    $session, $inputString,
                    'File not found: ' . $file . ', no files exported',
                );
            }
        }

        # Open a file chooser dialog to decide where to save the exported file
        # NB Private code, not included in the public release, sets the IV
        #   GA::Client->privConfigAllWorld, in which case we use a certain file path, rather than
        #   prompting the user for one
        if (! $axmud::CLIENT->privConfigAllWorld) {

            $exportPath = $session->mainWin->showFileChooser(
                'Export file(s)',
                'save',
                $axmud::NAME_FILE . '.tgz',
            );

            if (! $exportPath) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->complete($session, $standardCmd, 'File(s) not exported');
            }

        } else {

            $exportPath = $axmud::SHARE_DIR . '/items/worlds/' . $axmud::CLIENT->privConfigAllWorld
                            . '/' . $axmud::CLIENT->privConfigAllWorld . '.tgz';
        }

        # For large files (e.g. world models containing tens of thousands of rooms), we need to
        #   display an initial message to explain the pause
        $session->writeText('Exporting file(s)...');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->do');

        # Create a tar object
        $tarObj = Archive::Tar->new();
        # Save the list of files to the tar object's memory archive
        foreach my $file (@exportList) {

            my $path = $axmud::DATA_DIR . '/' . $file;

            $tarObj->add_files($path);
            # Rename each file in the archive to remove the directory structure
            $tarObj->rename($path, $file);
        }

        # Export the files as a .tgz file
        if (! $tarObj->write($exportPath, Archive::Tar::COMPRESS_GZIP, 'export')) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->complete($session, $standardCmd, 'No files exported (archive error)');

        } else {

            # Display list of exported files

            # Display header
            $session->writeText('List of exported files (destination: ' . $exportPath . ')');

            # Display list
            foreach my $file (@exportList) {

                if (! exists $miniFileHash{$file}) {

                    # When the world model is exported as multiple files, only show the main file
                    $session->writeText('   ' . $file);
                }
            }

            # Display footer
            $total = (scalar @exportList) - (keys %miniFileHash);
            if ($total == 1) {

                return $self->complete($session, $standardCmd, '1 file exported to ' . $exportPath);

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    $total . ' files exported to ' . $exportPath,
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ImportFiles;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('importfiles', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['imf', 'importfile', 'importfiles'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Imports a file (or files)';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $importPath,
            $check,
        ) = @_;

        # Local variables
        my (
            $extractObj, $tempDir, $slash, $count, $mainModelHashRef, $thisWorldProf,
            $assocWorldProf, $choice, $regex,
            @fileList, @failList, @worldList, @exceptList, @relatedList, @otherList, @successList,
        );

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->improper($session, $inputString);
        }

        # Check that loading is allowed at all
        if (! $axmud::CLIENT->loadDataFlag) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'File load/save is disabled in all sessions',
            );
        }

        # If a file path was not specified, open a file chooser dialog to decide which file to
        #   import
        if (! $importPath) {

            $importPath = $session->mainWin->showFileChooser(
                'Import file',
                'open',
            );

            if (! $importPath) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->complete($session, $standardCmd, 'File(s) not imported');
            }
        }

        # Check that $importPath is a valid compressed file (ending .tar, .tar.gz, .tgz, .gz, .zip,
        #   .bz2, .tar.bz2, .tbz or .lzma)
        if (
            ! ($importPath =~ m/\.tar$/)
            && ! ($importPath =~ m/\.tgz$/)
            && ! ($importPath =~ m/\.gz$/)
            && ! ($importPath =~ m/\.zip$/)
            && ! ($importPath =~ m/\.bz2$/)
            && ! ($importPath =~ m/\.tbz$/)
            && ! ($importPath =~ m/\.lzma$/)
        ) {
            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'File(s) not imported (you specified something that doesn\'t appear to be a'
                . ' compressed archive, e.g. a .zip or .tar.gz file)',
            );
        }

        # For large files (e.g. world models containing tens of thousands of rooms), we need to
        #   display an initial message to explain the pause
        $session->writeText('Importing file(s)...');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->do');

        # Build an Archive::Extract object
        $extractObj = Archive::Extract->new(archive => $importPath);
        if (! $extractObj) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'No files imported (file decompression error)',
            );
        }

        # Extract the object to a temporary directory
        $tempDir = $axmud::DATA_DIR . '/data/temp';
        if (! $extractObj->extract(to => $tempDir)) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'No files imported (file decompression error)',
            );
        }

        # All the files are now in /data/temp/export. Get a list of paths, relative to $tempDir, of
        #   all the extracted files
        @fileList = @{$extractObj->files};  # e.g. export/tasks.axm
        # Convert all the paths into absolute paths
        foreach my $file (@fileList) {

            $file = $axmud::DATA_DIR . '/data/temp/' . $file;
            if ($^O eq 'MSWin32') {

                $slash = '\\';
                $file =~ s/\//$slash/g;
            }
        }

        # Before v1.0.868, 'otherprof.axm' files were called 'otherdefn.amd' files. Change the
        #   filename of any affected files
        foreach my $file (@fileList) {

            my $oldFile = $file;

            if ($file =~ m/otherdefn\.amd$/) {

                $file =~ s/otherdefn\.amd$/otherprof.axm/;

                File::Copy::move($oldFile, $file);
            }
        }

        # The world model, if it is large, may have been divided into multiple files - a main one,
        #   and several 'mini' files containing a limited number of model objects
        # Divide @fileList into groups: (1) 'worldprof' files, (2) 'otherprof' files, the main
        #   'worldmodel' file and any 'mini' world model files, (3) everything else
        # At the same time, remove any files from @fileList which don't seem to be Axmud data files,
        #   or which are Axmud config files, or which are files that seem to be corrupted
        OUTER: foreach my $file (@fileList) {

            my (
                $matchFlag,
                %headerHash,
            );

            # Ignore files that don't end with a compatible file extension (like .axm)
            INNER: foreach my $ext (@axmud::COMPAT_EXT_LIST) {

                if ($file =~ m/\.$ext$/) {

                    $matchFlag = TRUE;
                    last INNER;
                }
            }

            if (! $matchFlag) {

                next OUTER;
            }

            # Check it's really an Axmud file by loading the file into a hash
            %headerHash = $axmud::CLIENT->configFileObj->examineDataFile($file, 'return_header');
            if (
                ! %headerHash
                || ! $axmud::CLIENT->configFileObj->checkCompatibility($headerHash{'script_name'})
                || $axmud::CLIENT->convertVersion($headerHash{'script_version'})
                    > $axmud::CLIENT->convertVersion($axmud::VERSION)
            ) {
                push (@failList, $file);
                next OUTER;
            }

            # Decide what to do with this type of file
            if ($headerHash{'file_type'} eq 'config') {

                # An unlikely error - ;exportfiles doesn't export config files
                push (@failList, $file);
                next OUTER;

            } elsif ($headerHash{'file_type'} eq 'worldprof') {

                # Put the file into the world profile list
                $headerHash{'file'} = $file;
                push (@worldList, \%headerHash);

            } elsif (
                $headerHash{'file_type'} eq 'otherprof'
                || $headerHash{'file_type'} eq 'worldmodel'
            ) {
                # Put the file into the world profile-related list
                $headerHash{'file'} = $file;

                # Special case: if the archive contains only 'worldmodel' files (perhaps a single
                #   file, or perhaps multiple files), treat them slightly differently (but only if
                #   this command was called from the Automapper window, in which case
                #   GA::Session->transferWorldModelFlag will be set)
                if ($headerHash{'file_type'} eq 'worldmodel' && $session->transferWorldModelFlag) {
                    push (@exceptList, \%headerHash);
                } else {
                    push (@relatedList, \%headerHash);
                }

            } else {

                # Put the file in the other file list
                $headerHash{'file'} = $file;
                push (@otherList, \%headerHash);
            }
        }

        # If the archive contains only a world model (perhaps a single file, or perhaps a world
        #   model split across multiple files, one of them being a 'main' file), and if its parent
        #   world is different to the current world, ask the user if they'd like to associate the
        #   world model with the current world instead
        if (! @failList && ! @worldList && ! @relatedList && ! @otherList && @exceptList) {

            # The world model file(s) can now be added to group (2)
            push (@relatedList, @exceptList);

            # Check against the (unlikely) possibility that the archive contains more than one
            #   world model by counting the number of 'main' world model files
            $count = 0;
            OUTER: foreach my $hashRef (@exceptList) {

                my $regex = 'worldmodel\.(' . join('|', @axmud::COMPAT_EXT_LIST) . ')$';

                if ($$hashRef{'file'} =~ m/$regex/) {

                    # This is a 'main' world model file
                    $count++;
                    $mainModelHashRef = $hashRef;
                }
            }

            if ($count == 1 && $session->transferWorldModelFlag) {

                $thisWorldProf = $session->currentWorld->name;
                $assocWorldProf = $$mainModelHashRef{'assoc_world_prof'};

                if ($assocWorldProf ne $thisWorldProf) {

                    $choice = $session->mainWin->showMsgDialogue(
                        'Import world model',
                        'question',
                        'The world model belongs to a profile called \'' . $assocWorldProf
                        . '\'. Would you like to associate it with the current world profile, \''
                        . $session->currentWorld->name . '\', instead?',
                        'yes-no',
                    );

                    if ($choice && $choice eq 'yes') {

                        # Change the name of the file(s) so that when they are copied into their
                        #   permanent locations, the file(s) will be associated with a different
                        #   world profile
                        foreach my $hashRef (@exceptList) {

                            my ($thisFile, $thisDir, $regex);

                            $thisFile = $$hashRef{'file'};
                            $thisFile =~ s/$assocWorldProf/$thisWorldProf/s;        # Last match

                            # Make a copy of the temporary file, creating its directory if it
                            #   doesn't already exist
                            $thisDir = $thisFile;
                            $thisDir =~ s/\/[^\/]+$//;
                            mkdir ($thisDir, 0755);

                            if (! File::Copy::copy($$hashRef{'file'}, $thisFile)) {

                                return $self->error(
                                    $session, $inputString,
                                    'No files imported (file copy error)',
                                );
                            }

                            # Update the header hash to use the new temporary file
                            $$hashRef{'file'} = $thisFile;
                            $$hashRef{'assoc_world_prof'} = $thisWorldProf;
                        }
                    }
                }
            }
        }

        # Deal with world profiles first
        OUTER: foreach my $hashRef (@worldList) {

            my (
                $newDir, $newFile,
                %headerHash,
            );

            %headerHash = %$hashRef;
            $newDir = $axmud::DATA_DIR . '/data/worlds/' . $headerHash{'assoc_world_prof'};
            $newFile = $newDir . '/worldprof.axm';

            # If the world's directory doesn't already exist, create it
            if (! (-d $newDir)) {

                if (! mkdir ($newDir, 0755)) {

                    push (@failList, $headerHash{'file'});
                    next OUTER;
                }
            }

            # Copy the file into the directory
            if (! File::Copy::copy($headerHash{'file'}, $newFile)) {
                push (@failList, $headerHash{'file'});
            } else {
                push (@successList, $newFile);
            }
        }

        # Now deal with other world-related files
        OUTER: foreach my $hashRef (@relatedList) {

            my (
                $newDir, $newFile, $miniFlag,
                %headerHash,
            );

            %headerHash = %$hashRef;
            $newDir = $axmud::DATA_DIR . '/data/worlds/' . $headerHash{'assoc_world_prof'};

            if (! $headerHash{'file_type'} eq 'worldmodel') {

                $newFile = $newDir . '/' . $headerHash{'file_type'} . '.axm';

            } else {

                # The world model might be split across several files, a 'main' one (worldmodel.axm)
                #   and several others (worldmodel_1.axm, worldmodel_2.axm, etc)
                $newFile = $headerHash{'file'};
                $regex = '\_(\d+)\.(' . join('|', @axmud::COMPAT_EXT_LIST) . ')$';

                if ($newFile =~ m/$regex/) {

                    $newFile = $newDir . '/' . $headerHash{'file_type'} . '_' . $1 . '.axm';
                    # Don't display this mini-file at the bottom of this function
                    $miniFlag = TRUE;

                } else {

                    $newFile = $newDir . '/' . $headerHash{'file_type'} . '.axm';
                }
            }

            # Check that the directory exists, and that the corresponding 'worldprof' file already
            #   exists in it
            if (! (-d $newDir) || ! (-e $newDir . '/worldprof.axm')) {

                push (@failList, $headerHash{'file'});

            } else {

                # Copy the file into the world's directory
                if (! File::Copy::copy($headerHash{'file'}, $newFile)) {

                    push (@failList, $headerHash{'file'});

                # Only display the 'main' world model file at the bottom of this function
                } elsif (! $miniFlag) {

                    push (@successList, $newFile);
                }
            }
        }

        # Finally deal with all other files (which are simpy copied into the /data directory)
        OUTER: foreach my $hashRef (@otherList) {

            my (
                $newFile,
                %headerHash,
            );

            %headerHash = %$hashRef;
            $newFile = $axmud::DATA_DIR . '/data/' . $headerHash{'file_type'} . '.axm';

            # Copy the file into the directory
            if (! File::Copy::copy($headerHash{'file'}, $newFile)) {
                push (@failList, $headerHash{'file'});
            } else {
                push (@successList, $newFile);
            }
        }

        # Display results
        if (@successList) {

            $session->writeText('Files imported:');
            foreach my $file (sort {$a cmp $b} (@successList)) {

                $session->writeText('   ' . $file);
            }
        }

        if (@failList) {

            $session->writeText('Files not imported:');
            foreach my $file (sort {$a cmp $b} (@failList)) {

                $session->writeText('   ' . $file);
            }
        }

        if (! @successList) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'No files imported (errors: ' . scalar @failList . ')',
            );

        } elsif (@failList) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'Not all files imported (imported: ' . scalar @successList . ', errors: '
                . scalar @failList . ')',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Files imported: ' . scalar @successList . ', errors: ' . scalar @failList,
            );
        }
    }

}

{ package Games::Axmud::Cmd::ExportData;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('exportdata', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['exd', 'exportdata'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Exports an object (or objects)';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $name,
            $check,
        ) = @_;

        # Local variables
        my ($profObj, $exportFile);

        # Check for improper arguments
        if (! defined $switch || ! defined $name || defined $check) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->improper($session, $inputString);
        }

        # Check that saving is allowed at all
        if (! $axmud::CLIENT->saveConfigFlag && ! $axmud::CLIENT->saveDataFlag) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'File load/save is disabled in all sessions',
            );
        }

        # Do checks on <switch> and <name>, before trying to call the file object's methods

        # ;exd -d <world>
        if ($switch eq '-d') {

            # Check the profile exists
            if (! $session->ivExists('profHash', $name)) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->error(
                    $session, $inputString,
                    'Unknown non-world profile \'' . $name . '\'',
                );

            } else {

                $profObj = $session->ivShow('profHash', $name);
            }

            if ($profObj->category eq 'world') {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->error(
                    $session, $inputString,
                    'This command can\'t be used to export world profiles',
                );
            }

        # ;exd -t <cage>
        } elsif ($switch eq '-t') {

            # Check the cage exists
            if (! $session->ivExists('cageHash', $name)) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->error($session, $inputString, 'Unknown cage \'' . $name . '\'');
            }

        # ;exd -f <profile>
        } elsif ($switch eq '-f') {

            # Check the profile exists
            if (! $session->ivExists('profHash', $name)) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->error($session, $inputString, 'Unknown profile \'' . $name . '\'');
            }

        # ;exd -s <skel>
        } elsif ($switch eq '-s') {

            # Check the profile template exists
            if (! $session->ivExists('templateHash', $name)) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->error(
                    $session, $inputString,
                    'Unknown profile template \'' . $name . '\'',
                );
            }

        # ;exd -i <task>
        } elsif ($switch eq '-i') {

            # Check the (global) initial task exists
            if (! $axmud::CLIENT->ivExists('initTaskHash', $name)) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->error(
                    $session, $inputString,
                    'Unknown (global) initial task \'' . $name . '\' (tasks from profile'
                    . ' initial tasklists can\'t be exported)',
                );
            }

        # ;exd -c <task>
        } elsif ($switch eq '-c') {

            # Check the (global) custom task exists
            if (! $axmud::CLIENT->ivExists('customTaskHash', $name)) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->error(
                    $session, $inputString,
                    'Unknown (global) custom task \'' . $name . '\'',
                );
            }

        # ;exd -y <dict>
        } elsif ($switch eq '-y') {

            # Check the dictionary object exists
            if (! $axmud::CLIENT->ivExists('dictHash', $name)) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->error(
                    $session, $inputString,
                    'Unknown dictionary object \'' . $name . '\'',
                );
            }

        # ;exd -z <map>
        } elsif ($switch eq '-z') {

            # Check the zonemap object exists
            if (! $axmud::CLIENT->ivExists('zonemapHash', $name)) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->error(
                    $session, $inputString,
                    'Unknown zonemap object \'' . $name . '\'',
                );
            }

        # ;exd -p <map>
        } elsif ($switch eq '-p') {

            # Check the winmap object exists
            if (! $axmud::CLIENT->ivExists('winmapHash', $name)) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->error(
                    $session, $inputString,
                    'Unknown winmap object \'' . $name . '\'',
                );
            }

        # ;exd -o <col>
        } elsif ($switch eq '-o') {

            # Check the colour scheme object exists
            if (! $axmud::CLIENT->ivExists('colourSchemeHash', $name)) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->error(
                    $session, $inputString,
                    'Unknown colour scheme object \'' . $name . '\'',
                );
            }

        # ;exd -x <obj>
        } elsif ($switch eq '-x') {

            # Check the TTS object exists
            if (! $axmud::CLIENT->ivExists('ttsObjHash', $name)) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->error(
                    $session, $inputString,
                    'Unknown text-to-speech configuration object \'' . $name . '\'',
                );
            }
        }

        # For large files (e.g. world models containing tens of thousands of rooms), we need to
        #   display an initial message to explain the pause
        $session->writeText('Exporting data...');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->do');

        # %saveHash doesn't include the data file's header information
        # Insert the header information into %saveHash, and then export the data by saving it as a
        #   file
        $exportFile = $axmud::CLIENT->configFileObj->exportDataFile($session, $switch, $name);
        if (! $exportFile) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error($session, $inputString, 'No data exported');

        } else {

            return $self->complete($session, $standardCmd, 'Data exported to ' . $exportFile);
        }
    }
}

{ package Games::Axmud::Cmd::ImportData;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('importdata', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['imd', 'importdata'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Imports an object (or objects)';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $importPath,
            $check,
        ) = @_;

        # Local variables
        my ($configObj, $fileType);

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->improper($session, $inputString);
        }

        # Check that loading is allowed at all
        if (! $axmud::CLIENT->loadDataFlag) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'File load/save is disabled in all sessions',
            );
        }

        # If a file path was not specified, open a file chooser dialog to decide which file to
        #   import
        if (! $importPath) {

            $importPath = $session->mainWin->showFileChooser(
                'Import file',
                'open',
            );

            if (! $importPath) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->complete($session, $standardCmd, 'Data not imported');
            }
        }

        # For large files (e.g. world models containing tens of thousands of rooms), we need to
        #   display an initial message to explain the pause
        $session->writeText('Importing data...');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->do');

        # Import the data into memory
        $fileType = $axmud::CLIENT->configFileObj->importDataFile($session, $importPath);
        if (! $fileType) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error($session, $inputString, 'No data imported');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Data imported from \'' . $fileType . '\' file ' . $importPath,
            );
        }
    }
}

{ package Games::Axmud::Cmd::RetainFileCopy;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('retainfilecopy', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rfc', 'retaincopy', 'retainfilecopy'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Retains copy of old files in file-save operations';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my $msg;

        # Check for improper arguments
        if (! defined $arg || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;rfc on
        if ($arg eq 'on') {

            if ($axmud::CLIENT->autoRetainFileFlag) {

                return $self->error(
                    $session, $inputString,
                    'Copies of old files are already retained after file save operations',
                );

            } else {

                $axmud::CLIENT->set_autoRetainFileFlag(TRUE);
                return $self->complete(
                    $session, $standardCmd,
                    'Retention of copies of old files turned \'on\' (copies created during file'
                    . ' file save operations are not deleted automatically)',
                );
            }

        # ;rfc off
        } elsif ($arg eq 'off') {

            if (! $axmud::CLIENT->autoRetainFileFlag) {

                return $self->error(
                    $session, $inputString,
                    'Copies of old files are already deleted after file save operations',
                );

            } else {

                $axmud::CLIENT->set_autoRetainFileFlag(FALSE);
                return $self->complete(
                    $session, $standardCmd,
                    'Retention of copies of old files turned \'off\' (copies created during file'
                    . ' save operations are deleted automatically)',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ListDataDirectory;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);
    # Include module here, as well as in axmud.pl, so that .../t/00-compile.t won't fail
    use Archive::Tar;

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listdatadirectory', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ldd', 'listdatadir', 'listdatafolder', 'listdatadirectory'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows the location of the ' . $axmud::SCRIPT . ' data directory';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my ($fileHandle, $nextDir);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Read the data directory location that will be used the next time Axmud starts
        if (-e $axmud::TOP_DIR . '/datadir.cfg') {

            if (open $fileHandle, '<', $axmud::TOP_DIR . '/datadir.cfg') {

                $nextDir = <$fileHandle>;
                close $fileHandle;
            }
        }

        if (defined $nextDir) {
            chomp $nextDir;
        } else {
            $nextDir = $axmud::DATA_DIR;
        }

        # Show header
        $session->writeText('List of Axmud data directories (folders)');

        # Show list
        $session->writeText('   Directory used when ' . $axmud::SCRIPT . ' started:');
        $session->writeText('      ' . $axmud::DATA_DIR);
        $session->writeText('   Directory to be used when ' . $axmud::SCRIPT . ' next starts:');
        $session->writeText('      ' . $nextDir);
        $session->writeText('   Default directory:');
        $session->writeText('      ' . $axmud::DEFAULT_DATA_DIR);

        # Show footer
        return $self->complete($session, $standardCmd, 'End of list');
    }
}

{ package Games::Axmud::Cmd::SetDataDirectory;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);
    # Include module here, as well as in axmud.pl, so that .../t/00-compile.t won't fail
    use Archive::Tar;

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setdatadirectory', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sdd', 'setdatadir', 'setdatafolder', 'setdatadirectory'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the location of the ' . $axmud::SCRIPT . ' data directory';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $dirPath,
            $check,
        ) = @_;

        # Local variables
        my ($configPath, $fileHandle);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        $configPath = $axmud::TOP_DIR . '/datadir.cfg';

        # ;sdd
        if (! defined $dirPath) {

            # Prompt the user to choose a directory
            $dirPath = $session->mainWin->showFileChooser(
                'Set ' . $axmud::SCRIPT . ' data directory',
                'create-folder',
            );

            if (! defined $dirPath) {

                return $self->complete(
                    $session, $standardCmd,
                    'Set data directory operation cancelled',
                );

            } else {

                # Update datadir.cfg
                if (! open ($fileHandle, '>', $configPath)) {

                    return $self->error(
                        $session, $inputString,
                        'Unable to set the location of ' . $axmud::SCRIPT . '\'s data directory'
                        . ' (internal error)',
                    );

                } else {

                    print $fileHandle $dirPath;
                    close $fileHandle;

                    return $self->complete(
                        $session, $standardCmd,
                        'The location of ' . $axmud::SCRIPT . '\'s data directory has been set'
                        . ' to \'' . $dirPath . '\' (restart ' . $axmud::SCRIPT
                        . ' to load data files from this location)',
                    );
                }
            }

        # ;sdd -r
        } elsif ($dirPath eq '-r') {

            # Reset the data directory to the default one by emptying data.cfg
            if (! open ($fileHandle, '>', $configPath)) {

                return $self->error(
                    $session, $inputString,
                    'Unable to reset the location of ' . $axmud::SCRIPT . '\'s data directory'
                    . ' (internal error)',
                );

            } else {

                print $fileHandle "";
                close $fileHandle;

                return $self->complete(
                    $session, $standardCmd,
                    'The location of ' . $axmud::SCRIPT . '\'s data directory has been reset to \''
                    . $axmud::DEFAULT_DATA_DIR . '\' (restart ' . $axmud::SCRIPT . ' to load'
                    . ' data files from this location)',
                );
            }

        # ;sdd <path>
        } else {

            # For this option, the path must exist
            if (! -e $dirPath) {

                return $self->error(
                    $session, $inputString,
                    'The directory \'' . $dirPath . '\' doesn\'t exist (try using this command'
                    . ' without arguments instead)',
                );
            }

            # Update datadir.cfg
            if (! open ($fileHandle, '>', $configPath)) {

                return $self->error(
                    $session, $inputString,
                    'Unable to set the location of ' . $axmud::SCRIPT . '\'s data directory'
                    . ' (internal error)',
                );

            } else {

                print $fileHandle $dirPath;
                close $fileHandle;

                return $self->complete(
                    $session, $standardCmd,
                    'The location of ' . $axmud::SCRIPT . '\'s data directory has been set'
                    . ' to \'' . $dirPath . '\' (restart ' . $axmud::SCRIPT
                    . ' to load data files from this location)',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::BackupData;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);
    # Include module here, as well as in axmud.pl, so that .../t/00-compile.t won't fail
    use Archive::Tar;

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('backupdata', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['bud', 'backup', 'backupdata'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Creates a backup copy of the ' . $axmud::SCRIPT . ' data directory';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $dataDir, $ext, $fileName, $backupPath, $zipObj, $tarObj,
            @fileList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the Axmud data directory for use in regexes
        $dataDir = $axmud::DATA_DIR;

        # Check the switch is valid, if specified
        if (defined $switch && $switch ne '-z' && $switch ne '-t') {

            return $self->error(
                $session, $inputString,
                'Unrecognised switch \'' . $switch . '\' (try -t for .tgz, -z for .zip)',
            );
        }

        # Check that the data directory actually exists (no reason why it shouldn't, but still we'll
        #   still check)
        if (! -e $dataDir) {

            return $self->error(
                $session, $inputString,
                'Data backup failed, cannot find data directory \'' . $dataDir . '\'',
            );
        }

        # For convenience, archive to .zip on MS Windows, and to .tgz on Linux
        if (! defined $switch) {

            if ($^O eq 'MSWin32') {
                $ext = 'zip';
            } else {
                $ext = 'tgz';
            }

        } elsif ($switch eq '-z') {
            $ext = 'zip';
        } else {
            $ext = 'tgz';
        }

        # Set the filename, appending the time if required
        if (! $axmud::CLIENT->autoBackupAppendFlag) {

            $fileName = $axmud::NAME_FILE . '_backup_' . $axmud::CLIENT->localDateString() . '.'
                            . $ext;

        } else {

            $fileName = $axmud::NAME_FILE . '_backup_' . $axmud::CLIENT->localDateString() . '_'
                            . $axmud::CLIENT->localClockString() . '.' . $ext;
        }

        # If necessary, open a file chooser dialog to decide where to save the exported file
        if ($axmud::CLIENT->autoBackupDir && -e $axmud::CLIENT->autoBackupDir) {

            $backupPath = $axmud::CLIENT->autoBackupDir;

        } else {

            $backupPath = $session->mainWin->showFileChooser(
                'Backup ' . $axmud::SCRIPT . ' data',
                'save',
                $fileName,
            );
        }

        if (! $backupPath) {

            return $self->complete($session, $standardCmd, 'Data backup not completed');
        }

        # In case the data directory is large, display an initial message to explain the pause
        $session->writeText('Backing up ' . $axmud::SCRIPT . ' data directory...');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->do');

        # Get a list of files in the data directory, recursively searching sub-directories
        File::Find::find(
            sub { push (@fileList, $File::Find::name); },
            $dataDir . '/',
        );

        # Perform the backup
        if ($ext eq 'zip') {

            # Create a zip object
            $zipObj = Archive::Zip->new();

            foreach my $file (@fileList) {

                my $modFile;

                if ($file ne $dataDir) {

                    $modFile = substr($file, length($dataDir));

                    # 6 is the default compression level
                    $zipObj->addFileOrDirectory($file, $modFile, 6);
                }
            }

            # Save the .zip file. Successful operation returns 0
            if ($zipObj->writeToFileNamed($backupPath)) {

                return $self->complete(
                    $session, $standardCmd,
                    'Data backup failed (archive error)',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Backup of ' . $axmud::SCRIPT . ' data directory saved to ' . $backupPath,
                );
            }

        } else {

            # Create a tar object
            $tarObj = Archive::Tar->new();

            foreach my $file (@fileList) {

                if ($file ne $dataDir) {

                    $tarObj->add_files($file);
                    # Rename each file in the archive to remove the directory structure
                    $tarObj->rename($file, substr($file, length($dataDir)));
                }
            }

            # Save the .tgz file
            if (
                ! $tarObj->write(
                    $backupPath,
                    Archive::Tar::COMPRESS_GZIP,
                    $axmud::NAME_SHORT . '-data',
                )
            ) {
                return $self->complete(
                    $session, $standardCmd,
                    'Data backup failed (archive error)',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Backup of ' . $axmud::SCRIPT . ' data directory saved to ' . $backupPath,
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::RestoreData;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('restoredata', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rsd', 'restore', 'restoredata'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Restores ' . $axmud::SCRIPT . ' data directory from backup';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $backupPath,
            $check,
        ) = @_;

        # Local variables
        my ($choice, $oldDataDir, $extractObj, $zipFlag);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Always prompt the user for confirmation
        $choice = $session->mainWin->showMsgDialogue(
            'Restore data from backup',
            'question',
            'Are you sure you want to restore data from backup? (This operation will completely'
            . ' replace the current ' . $axmud::SCRIPT . ' data directory. In addition, you won\'t'
            . ' be able to save any data files until you restart ' . $axmud::SCRIPT
            . ', so any data currently in memory will be lost.)',
            'yes-no',
        );

        if (! defined $choice || $choice eq 'no') {

            return $self->complete(
                $session, $standardCmd,
                'Restore data from backup not completed',
            );
        }

        # If a file path was not specified, open a file chooser dialog to decide which file to
        #   import
        if (! $backupPath) {

            $backupPath = $session->mainWin->showFileChooser(
                'Restore data from backup',
                'open',
            );

            if (! $backupPath) {

                return $self->complete(
                    $session, $standardCmd,
                    'Restore data from backup not completed',
                );
            }
        }

        # ;backupdata only creates archives in .tgz or .zip formats, but this command can recognise
        #   the usual list of archive types
        if (
            ! ($backupPath =~ m/\.tar$/)
            && ! ($backupPath =~ m/\.tgz$/)
            && ! ($backupPath =~ m/\.gz$/)
            && ! ($backupPath =~ m/\.zip$/)
            && ! ($backupPath =~ m/\.bz2$/)
            && ! ($backupPath =~ m/\.tbz$/)
            && ! ($backupPath =~ m/\.lzma$/)
        ) {
            return $self->error(
                $session, $inputString,
                'Restore data from backup not completed (you specified something that doesn\'t'
                . ' appear to be a compressed archive, e.g. a .zip or .tgz file)',
            );
        }

        # For large files, we need to display an initial message to explain the pause
        $session->writeText('Restoring data from backup...');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->do');

        # The old directory is not replaced, but renamed. Cycle through a list of possible names
        #   until we find one that isn't in use (give up after a reasonable time)
        if (! -e $axmud::DATA_DIR . '_OLD') {

            $oldDataDir = $axmud::DATA_DIR . '_OLD';

        } else {

            OUTER: for (my $count = 2; $count <= 1024; $count++) {

                $oldDataDir = $axmud::DATA_DIR . '_OLD_' . $count;
                if (! -e $oldDataDir) {

                    last OUTER;
                }
            }
        }

        if (! $oldDataDir) {

           return $self->error(
                $session, $inputString,
                'Cannot restore data from backup - unable to find a new name for the existing'
                . ' data directory (try deleting a few of them first)',
            );

        } elsif (-e $axmud::DATA_DIR) {

            rename($axmud::DATA_DIR, $oldDataDir);
        }

        # Build an Archive::Extract object
        $extractObj = Archive::Extract->new(archive => $backupPath);
        if ($backupPath =~ m/\.zip$/) {
            $zipFlag = TRUE;
        } else {
            $zipFlag = FALSE;
        }

        if (
            ! $extractObj
            # (A .tar archive contains an 'axmud-data' directory, so we need to extract the archive
            #   into the parent directory)
            || (! $zipFlag && ! $extractObj->extract(to => $axmud::DATA_DIR . '/..'))
            || ($zipFlag && ! $extractObj->extract(to => $axmud::DATA_DIR))
        ) {
            # The data directory which was just renamed can be returned to its original name, as if
            #   nothing had happened
            rename($oldDataDir, $axmud::DATA_DIR);

            return $self->error(
                $session, $inputString,
                'Cannot restore data from backup (file decompression error)',
            );

        } else {

            # Operation successful. Disable load/save, forcing the user to restart Axmud
            $axmud::CLIENT->set_loadConfigFlag(FALSE);
            $axmud::CLIENT->set_saveConfigFlag(FALSE);
            $axmud::CLIENT->set_loadDataFlag(FALSE);
            $axmud::CLIENT->set_saveDataFlag(FALSE);

            return $self->complete(
                $session, $standardCmd,
                'Successfully restored data files from backup. No data has been loaded into memory'
                . ' and file load/save is now disabled, so you should restart ' . $axmud::SCRIPT
                . ' as soon as possible',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AutoBackup;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('autobackup', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['abu', 'autobu', 'autobackup'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Configures settings for auto-backups';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $modeCount, $typeCount, $noArgsFlag, $switch, $noBackupFlag, $allStartFlag,
            $allStopFlag, $intStartFlag, $intStopFlag, $dir, $dirFlag, $resetDirFlag, $number,
            $intervalFlag, $fileDefaultFlag, $fileTarFlag, $fileZipFlag, $appendFlag, $noAppendFlag,
            %modeHash, %typeHash,
        );

        # Extract switches
        $modeCount = 0;
        $typeCount = 0;
        if (! @args) {

            $noArgsFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-n', 0, @args);
        if (defined $switch) {

            $noBackupFlag = TRUE;
            $modeCount++;
        }

        ($switch, @args) = $self->extract('-s', 0, @args);
        if (defined $switch) {

            $allStartFlag = TRUE;
            $modeCount++;
        }

        ($switch, @args) = $self->extract('-p', 0, @args);
        if (defined $switch) {

            $allStopFlag = TRUE;
            $modeCount++;
        }

        ($switch, @args) = $self->extract('-x', 0, @args);
        if (defined $switch) {

            $intStartFlag = TRUE;
            $modeCount++;
        }

        ($switch, @args) = $self->extract('-y', 0, @args);
        if (defined $switch) {

            $intStopFlag = TRUE;
            $modeCount++;
        }

        ($switch, $dir, @args) = $self->extract('-f', 1, @args);
        if (defined $switch) {

            $dirFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-o', 0, @args);
        if (defined $switch) {

            $resetDirFlag = TRUE;
        }

        ($switch, $number, @args) = $self->extract('-i', 1, @args);
        if (defined $switch) {

            $intervalFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-d', 0, @args);
        if (defined $switch) {

            $fileDefaultFlag = TRUE;
            $typeCount++;
        }

        ($switch, @args) = $self->extract('-t', 0, @args);
        if (defined $switch) {

            $fileTarFlag = TRUE;
            $typeCount++;
        }

        ($switch, @args) = $self->extract('-z', 0, @args);
        if (defined $switch) {

            $fileZipFlag = TRUE;
            $typeCount++;
        }

        ($switch, @args) = $self->extract('-a', 0, @args);
        if (defined $switch) {

            $appendFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-e', 0, @args);
        if (defined $switch) {

            $noAppendFlag = TRUE;
        }

        # There should be 0 arguments left
        if (@args) {

            return $self->improper($session, $inputString);

        # Can't combine certain switches
        } elsif ($modeCount > 1) {

           return $self->error(
                $session, $inputString,
                'The switches -n, -s, -p, -x and -y can\'t be combined',
            );

        } elsif ($dirFlag && $resetDirFlag) {

           return $self->error(
                $session, $inputString,
                'The switches -f and -o can\'t be combined',
            );

        } elsif ($fileDefaultFlag && $resetDirFlag) {

           return $self->error(
                $session, $inputString,
                'The switches -f and -o can\'t be combined',
            );

        } elsif ($typeCount > 1) {

           return $self->error(
                $session, $inputString,
                'The switches -d, -t and -z can\'t be combined',
            );

        } elsif ($appendFlag && $noAppendFlag) {

           return $self->error(
                $session, $inputString,
                'The switches -a and -e can\'t be combined',
            );

        # The interval, if specified, must be an integer in the range 0-366
        } elsif ($intervalFlag && ! $axmud::CLIENT->intCheck($number, 0, 366)) {

           return $self->error(
                $session, $inputString,
                'Invalid interval \'' . $number . '\' - must be an integer in the range 0-366',
            );
        }

        # ;abu
        if ($noArgsFlag) {

            # Display header
            $session->writeText('Auto-backup settings');

            # Display list
            $session->writeText('   ' . $axmud::SCRIPT . ' base directory (folder)');
            $session->writeText('      ' . $axmud::TOP_DIR);
            $session->writeText('   ' . $axmud::SCRIPT . ' data directory (folder)');
            $session->writeText('      ' . $axmud::DATA_DIR);

            %modeHash = (
                'no_backup'         => 'Don\'t perform auto-backups',
                'all_start'         => 'Perform auto-backup when ' . $axmud::SCRIPT . ' starts',
                'all_stop'          => 'Perform auto-backup when ' . $axmud::SCRIPT . ' stops',
                'interval_start'    => 'Perform auto-backup when ' . $axmud::SCRIPT . ' starts,'
                                            . ' after certain interval',
                'interval_stop'     => 'Perform auto-backup when ' . $axmud::SCRIPT . ' stops,'
                                            . ' after certain interval',
            );

            $session->writeText('   Auto-backup mode');
            $session->writeText(
                sprintf(
                    '      %-16.16s : %-64.64s',
                    $axmud::CLIENT->autoBackupMode,
                    $modeHash{$axmud::CLIENT->autoBackupMode},
                ),
            );

            $session->writeText('   Directory where the backup file is stored');
            if (! $axmud::CLIENT->autoBackupDir) {
                $session->writeText('      <not set>');
            } else {
                $session->writeText('      ' . $axmud::CLIENT->autoBackupDir);
            }

            $session->writeText(
                '   Interval (in days) between backups (0 - no auto-backups at intervals)',
            );

            if (
                $axmud::CLIENT->autoBackupMode eq 'interval_start'
                || $axmud::CLIENT->autoBackupMode eq 'interval_stop'
            ) {

                $session->writeText('      ' . $axmud::CLIENT->autoBackupInterval);

            } else {

                $session->writeText(
                    '      ' . $axmud::CLIENT->autoBackupInterval . ' (not used in current'
                    . ' auto-backup mode)',
                );
            }

            $session->writeText('   Date of last successful auto-backup');
            if (! $axmud::CLIENT->autoBackupDate) {
                $session->writeText('      <not set>');
            } else {
                $session->writeText('      ' . $axmud::CLIENT->autoBackupDate);
            }

            %typeHash = (
                'default'   => 'Create .tgz file on Linux, .zip file on MS Windows',
                'tar'       => 'Create .tgz file (ideal for Linux)',
                'zip'       => 'Create .zip file (ideal for MS Windows)',
            );

            $session->writeText('   Auto-backup file type');
            $session->writeText(
                sprintf(
                    '      %-16.16s : %-64.64s',
                    $axmud::CLIENT->autoBackupFileType,
                    $typeHash{$axmud::CLIENT->autoBackupFileType},
                )
            );

            if (! $axmud::CLIENT->autoBackupAppendFlag) {
                $session->writeText('   Append time to back-up file - NO');
            } else {
                $session->writeText('   Append time to back-up file - YES');
            }

            # Display footer
            return $self->complete($session, $standardCmd, 'End of list');

        # ;abu <args>
        } else {

            if ($noBackupFlag) {
                $axmud::CLIENT->set_autoBackupMode('no_backup');
            } elsif ($allStartFlag) {
                $axmud::CLIENT->set_autoBackupMode('all_start');
            } elsif ($allStopFlag) {
                $axmud::CLIENT->set_autoBackupMode('all_stop');
            } elsif ($intStartFlag) {
                $axmud::CLIENT->set_autoBackupMode('interval_start');
            } elsif ($intStopFlag) {
                $axmud::CLIENT->set_autoBackupMode('interval_stop');
            }

            if ($dirFlag) {
                $axmud::CLIENT->set_autoBackupDir($dir);
            } elsif ($resetDirFlag) {
                $axmud::CLIENT->set_autoBackupDir();
            }

            if ($intervalFlag) {

                $axmud::CLIENT->set_autoBackupInterval($number);
            }

            if ($fileDefaultFlag) {
                $axmud::CLIENT->set_autoBackupFileType('default');
            } elsif ($fileTarFlag) {
                $axmud::CLIENT->set_autoBackupFileType('tar');
            } elsif ($fileZipFlag) {
                $axmud::CLIENT->set_autoBackupFileType('zip');
            }

            if ($appendFlag) {
                $axmud::CLIENT->set_autoBackupAppendFlag(TRUE);
            } elsif ($noAppendFlag) {
                $axmud::CLIENT->set_autoBackupAppendFlag(FALSE);
            }

            return $self->complete($session, $standardCmd, 'Auto-backup settings modified');
        }
    }
}

{ package Games::Axmud::Cmd::ImportPlugin;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('importplugin', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ipl', 'importplugin'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Imports ' . $axmud::NAME_ARTICLE . ' plugin';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $path,
            $check,
        ) = @_;

        # Local variables
        my ($file, $dir, $ext, $newPath, $choice);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If a file path was not specified, open a file chooser dialogue to decide which plugin file
        #   to import

        # ;ipl
        if (! defined $path) {

            $path = $session->mainWin->showFileChooser(
                'Import plugin',
                'open',
                $axmud::DATA_DIR . '/plugins',
            );

            if (! $path) {

                return $self->complete($session, $standardCmd, 'Plugin not imported');
            }
        }

        # Check the file exists
        if (! -e $path) {

            return $self->error(
                $session, $inputString,
                'Plugin file \''. $path . '\' doesn\'t exist',
            );
        }

        # Check that a plugin with the same name doesn't already exist
        ($file, $dir, $ext) = File::Basename::fileparse($path, qr/\.[^.]*/);

        if ($^O eq 'MSWin32') {
            $newPath = $axmud::DATA_DIR . '\\plugins\\' . $file . $ext;
        } else {
            $newPath = $axmud::DATA_DIR . '/plugins/' . $file . $ext;
        }

        if (-e $newPath) {


            $choice = $session->mainWin->showMsgDialogue(
                'Import plugin',
                'question',
                'The plugin \'' . $newPath . '\' already exists. Do you want to overwrite it?',
                'yes-no',
            );

            if (! defined $choice || $choice eq 'no') {

                return $self->complete($session, $standardCmd, 'Plugin not imported');
            }
        }

        # Import the plugin
        if (! File::Copy::copy($path, $newPath)) {

            return $self->error(
                $session, $inputString,
                'Failed to import the plugin file \''. $path . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Imported the plugin file \''. $path . '\' (load it using \';loadplugin\')',
            );
        }
    }
}

{ package Games::Axmud::Cmd::LoadPlugin;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('loadplugin', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lpl', 'loadplugin'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Loads ' . $axmud::NAME_ARTICLE . ' plugin';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my (
            $dirHandle, $count, $errorCount, $name,
            @fileList, @modList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If a file path was not specified, open a file chooser dialogue to decide which plugin file
        #   to load

        # ;lpl
        # ;lpl -s
        # ;lpl <path>
        if (! defined $arg || $arg ne '-a') {

            # ;lpl
            if (! $arg) {

                $arg = $session->mainWin->showFileChooser(
                    'Load plugin',
                    'open',
                    $axmud::DATA_DIR . '/plugins',
                );

                if (! $arg) {

                    return $self->complete($session, $standardCmd, 'Plugin not loaded');
                }

            # ;lpl -s
            } elsif ($arg eq '-s') {

                $arg = $session->mainWin->showFileChooser(
                    'Load plugin',
                    'open',
                    $axmud::SHARE_DIR . '/plugins',
                );

                if (! $arg) {

                    return $self->complete($session, $standardCmd, 'Plugin not loaded');
                }
            }

            $name = $axmud::CLIENT->loadPlugin($arg);
            if (! $name) {

                return $self->error($session, $inputString, 'Plugin not loaded');

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Plugin \'' . $name . '\' loaded',
                );
            }

        # ;lpl -a
        } else {

            # Get a list of standard plugins
            if (! opendir ($dirHandle, $axmud::SHARE_DIR . '/plugins')) {

                return $self->error($session, $inputString, 'No standard plugins found');

            } else {

                @fileList = readdir ($dirHandle);
                closedir $dirHandle;
            }

            # Eliminate non-plugin files (including those beginning with an underline, meaning
            #   they're a support file for a main plugin that doesn't begin with an underline)
            foreach my $file (@fileList) {

                if ($file =~ m/^[[:alpha:]].*\.pm/) {

                    push (@modList, $file);
                }
            }

            if (! @modList) {

                return $self->error($session, $inputString, 'No standard plugins found');
            }

            # Load each standard plugin in turn
            $count = 0;
            $errorCount = 0;

            foreach my $file (@modList) {

                if (! $axmud::CLIENT->loadPlugin($axmud::SHARE_DIR . '/plugins/' . $file)) {
                    $errorCount++;
                } else {
                    $count++;
                }
            }

            return $self->complete(
                $session, $standardCmd,
                'Standard plugins loaded: ' . $count . ', errors: ' . $errorCount,
            );
        }
    }
}

{ package Games::Axmud::Cmd::EnablePlugin;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('enableplugin', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['epl', 'enableplugin'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Enables ' . $axmud::NAME_ARTICLE . ' plugin';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the named plugin exists
        if (! $axmud::CLIENT->ivExists('pluginHash', $name)) {

            return $self->error($session, $inputString, 'Plugin \'' . $name . '\' not loaded');

        } else {

            $obj = $axmud::CLIENT->ivShow('pluginHash', $name);
        }

        # Check the plugin is not already enabled
        if ($obj->enabledFlag) {

            return $self->error(
                $session, $inputString,
                'Plugin \'' . $name . '\' is already enabled',
            );

        } else {

            # Enable the plugin
            if (! $axmud::CLIENT->enablePlugin($name)) {

                return $self->error(
                    $session, $inputString,
                    'Unable to enable the \'' . $name . '\' plugin',
                );

            } else {

                return $self->complete($session, $standardCmd, '\'' . $name . '\' plugin enabled');
            }
        }
    }
}

{ package Games::Axmud::Cmd::DisablePlugin;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('disableplugin', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dpl', 'disableplugin'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Disables ' . $axmud::NAME_ARTICLE . ' plugin';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the named plugin exists
        if (! $axmud::CLIENT->ivExists('pluginHash', $name)) {

            return $self->error($session, $inputString, 'Plugin \'' . $name . '\' not loaded');

        } else {

            $obj = $axmud::CLIENT->ivShow('pluginHash', $name);
        }

        # Check the plugin is not already enabled
        if (! $obj->enabledFlag) {

            return $self->error(
                $session, $inputString,
                'Plugin \'' . $name . '\' is already disabled',
            );

        } else {

            # Disable the plugin
            if (! $axmud::CLIENT->disablePlugin($name)) {

                return $self->error(
                    $session, $inputString,
                    'Unable to disable the \'' . $name . '\' plugin',
                );

            } else {

                return $self->complete($session, $standardCmd, '\'' . $name . '\' plugin disabled');
            }
        }
    }
}

{ package Games::Axmud::Cmd::TestPlugin;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('testplugin', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tpl', 'testplugin'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Tests ' . $axmud::NAME_ARTICLE . ' plugin';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $pluginPath,
            $check,
        ) = @_;

        # Local variables
        my $pluginName;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If a file path was not specified, open a file chooser dialogue to decide which plugin file
        #   to test
        if (! $pluginPath) {

            $pluginPath = $session->mainWin->showFileChooser(
                'Test plugin',
                'open',
                $axmud::DATA_DIR . '/plugins',
            );

            if (! $pluginPath) {

                return $self->complete($session, $standardCmd, 'Plugin not tested');
            }


        } elsif ($pluginPath eq '-s') {

            $pluginPath = $session->mainWin->showFileChooser(
                'Load plugin',
                'open',
                $axmud::SHARE_DIR . '/plugins',
            );

            if (! $pluginPath) {

                return $self->complete($session, $standardCmd, 'Plugin not tested');
            }
        }

        # Test the plugin
        $pluginName = $axmud::CLIENT->loadPlugin($pluginPath, TRUE);
        if (! $pluginName) {

            return $self->error($session, $inputString, 'Plugin test failed');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Plugin test for \'' . $pluginName . '\' passed',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListPlugin;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listplugin', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lpg', 'listplugin'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists existing ' . $axmud::SCRIPT . ' plugins';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        @list = sort {lc($a) cmp lc($b)} $axmud::CLIENT->ivKeys('pluginHash');
        if (! @list) {

            return $self->complete($session, $standardCmd, 'The loaded plugin list is empty');
        }

        # Display header
        $session->writeText('Loaded plugins (* - enabled)');
        $session->writeText('   Plugin name      Version          Author           Description');

        # Display list
        foreach my $plugin (@list) {

            my ($obj, $column, $author);

            $obj = $axmud::CLIENT->ivShow('pluginHash', $plugin);

            if ($obj->enabledFlag) {
                $column = ' * ';
            } else {
                $column = '   ';
            }

            if ($obj->author) {
                $author = $obj->author;
            } else {
                $author = '';
            }

            $session->writeText(
                $column
                . sprintf('%-16.16s %-16.16s %-16.16s', $plugin, $obj->version, $author)
                . ' ' . $obj->descrip,
            );
        }

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 plugin found');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @list . ' plugins found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddInitialPlugin;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addinitialplugin', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['aip', 'addplugin', 'addinitialplugin'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds an initial plugin';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $pluginPath,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If a file path was not specified, open a file chooser dialog to decide which plugin file
        #   to add
        if (! $pluginPath) {

            $pluginPath = $session->mainWin->showFileChooser(
                'Add initial plugin',
                'open',
                $axmud::DATA_DIR . '/plugins',
            );

            if (! $pluginPath) {

                return $self->complete($session, $standardCmd, 'Plugin not added');
            }

        } elsif ($pluginPath eq '-s') {

            $pluginPath = $session->mainWin->showFileChooser(
                'Add initial plugin',
                'open',
                $axmud::SHARE_DIR . '/plugins',
            );

            if (! $pluginPath) {

                return $self->complete($session, $standardCmd, 'Plugin not added');
            }
        }

        # Check the list of initial plugins doesn't already contain this plugin
        OUTER: foreach my $item ($axmud::CLIENT->initPluginList) {

            if ($item eq $pluginPath) {

                return $self->error(
                    $session, $inputString,
                    'The file \'' . $pluginPath . '\' has already been added to the initial plugin'
                    . 'list',
                );
            }
        }

        # Add the initial plugin
        $axmud::CLIENT->add_initPlugin($pluginPath);
        return $self->complete(
            $session, $standardCmd,
            'Add initial plugin \'' . $pluginPath . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::DeleteInitialPlugin;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteinitialplugin', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dip', 'delplugin', 'deleteinitialplugin'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes an initial plugin';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $pluginPath,
            $check,
        ) = @_;

        # Local variables
        my $count;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If a file path was not specified, open a file chooser dialog to decide which plugin file
        #   to delete as an initial plugin
        if (! $pluginPath) {

            $pluginPath = $session->mainWin->showFileChooser(
                'Delete initial plugin',
                'open',
                $axmud::DATA_DIR . '/plugins',
            );

            if (! $pluginPath) {

                return $self->complete($session, $standardCmd, 'Plugin not deleted');
            }

        } elsif ($pluginPath eq '-s') {

            $pluginPath = $session->mainWin->showFileChooser(
                'Delete initial plugin',
                'open',
                $axmud::SHARE_DIR . '/plugins',
            );

            if (! $pluginPath) {

                return $self->complete($session, $standardCmd, 'Plugin not deleted');
            }
        }

        # Check the list of initial plugins does contain this plugin
        $count = -1;
        OUTER: foreach my $item ($axmud::CLIENT->initPluginList) {

            $count++;
            if ($item eq $pluginPath) {

                # Delete this initial plugin
                $axmud::CLIENT->del_initPlugin($count);

                return $self->complete(
                    $session, $standardCmd,
                    'Deleted initial plugin \'' . $pluginPath . '\' (will not be loaded, the next'
                    . ' time ' . $axmud::SCRIPT . ' starts)',
                );
            }
        }

        return $self->error(
            $session, $inputString,
            'Initial plugin \'' . $pluginPath . '\' not found',
        );
    }
}

{ package Games::Axmud::Cmd::ListInitialPlugin;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listinitialplugin', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lip', 'listinitialplugin'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists initial plugins';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        @list = $axmud::CLIENT->initPluginList;
        if (! @list) {

            return $self->complete($session, $standardCmd, 'The initial plugin list is empty');
        }

        # Display header
        $session->writeText('Initial plugins');

        # Display list
        foreach my $pluginPath (@list) {

            $session->writeText('   ' . $pluginPath);
        }

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 initial plugin found');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @list . ' initial plugins found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetTelnetOption;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('settelnetoption', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sto', 'settelopt', 'settelnetoption'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Enables/disables telnet negotiation options';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;sto
        if (! defined $switch) {

            # Display header
            $session->writeText('Global telnet option settings');

            # Display list
            if ($axmud::CLIENT->useEchoFlag) {
                $session->writeText('   ECHO (hide passwords)                   - on');
            } else {
                $session->writeText('   ECHO (hide passwords)                   - off');
            }

            if ($axmud::CLIENT->useSgaFlag) {
                $session->writeText('   SGA (Suppress Go Ahead)                 - on');
            } else {
                $session->writeText('   SGA (Suppress Go Ahead)                 - off');
            }

            if ($axmud::CLIENT->useTTypeFlag) {
                $session->writeText('   TTYPE (detect Terminal Type)            - on');
            } else {
                $session->writeText('   TTYPE (detect Terminal Type)            - off');
            }

            if ($axmud::CLIENT->useEorFlag) {
                $session->writeText('   EOR (negotiate End Of Record)           - on');
            } else {
                $session->writeText('   EOR (negotiate End Of Record)           - off');
            }

            if ($axmud::CLIENT->useNawsFlag) {
                $session->writeText('   NAWS (Negotiate About Window Size)      - on');
            } else {
                $session->writeText('   NAWS (Negotiate About Window Size)      - off');
            }

            if ($axmud::CLIENT->useNewEnvironFlag) {
                $session->writeText('   NEW-ENVIRON (New Environment option)    - on');
            } else {
                $session->writeText('   NEW-ENVIRON (New Environment option)    - off');
            }

            if ($axmud::CLIENT->useCharSetFlag) {
                $session->writeText('   CHARSET (Character set and translation) - on');
            } else {
                $session->writeText('   CHARSET (Character set and translation) - off');
            }

            # Display footer. Use a message consistent with other client commands
            return $self->complete(
                $session, $standardCmd,
                'End of telnet option list (7 options found)',
            );

        # ;sto -l
        } elsif ($switch eq '-l') {

            # Display header
            $session->writeText('Session\'s telnet option status:');

            # Display list
            $session->writeText('   ECHO (hide passwords)');
            if ($session->echoMode eq 'no_invite') {

                $session->writeText('      Server has not suggested stopping ECHO yet');

            } elsif ($session->echoMode eq 'client_agree') {

                $session->writeText(
                    '      Server has suggested stopping ECHO and client has agreed',
                );

            } elsif ($session->echoMode eq 'client_refuse') {

                $session->writeText(
                    '      Server has suggested stopping ECHO and client has refused',
                );

            } elsif ($session->echoMode eq 'server_stop') {

                $session->writeText('      Server has resumed ECHO and client has agreeed');
            }

            $session->writeText('   SGA (Suppress Go Ahead)');
            if ($session->sgaMode eq 'no_invite') {
                $session->writeText('      Server has not suggested SGA yet');
            } elsif ($session->sgaMode eq 'client_agree') {
                $session->writeText('      Server has suggested SGa and client has agreed');
            } elsif ($session->sgaMode eq 'client_refuse') {
                $session->writeText('      Server has suggested SGA and client has refused');
            } elsif ($session->sgaMode eq 'server_stop') {
                $session->writeText('      Server has stopped SGA and client has agreeed');
            }

            $session->writeText('   TTYPE (detect Terminal Type)');
            if ($session->specifiedTType) {
                $session->writeText('      Preferred terminal: ' . $session->specifiedTType);
            } else {
                $session->writeText('      Preferred terminal: (not sent)');
            }

            $session->writeText('   EOR (negotiate End Of Record)');
            if ($session->eorMode eq 'no_invite') {

                $session->writeText('      Server has not negotiated EOR yet');

            } elsif ($session->eorMode eq 'client_agree') {

                $session->writeText(
                    '      Server has suggested EOR negotiation and client has agreed',
                );

            } elsif ($session->eorMode eq 'client_refuse') {

                $session->writeText(
                    '      Server has suggested EOR negotiation and client has refused',
                );
            }

            $session->writeText('   NAWS (Negotiate About Window Size)');
            if ($session->nawsMode eq 'no_invite') {
                $session->writeText('      Server has not suggested NAWS yet');
            } elsif ($session->nawsMode eq 'client_agree') {
                $session->writeText('      Server has suggested NAWS and client has agreed');
            } elsif ($session->nawsMode eq 'client_refuse') {
                $session->writeText('      Server has suggested NAWS and client has refused');
            }

            $session->writeText('   NEW-ENVIRON (New Environment option)');
            if ($session->nawsMode eq 'no_invite') {
                $session->writeText('      Server has not suggested NEW-ENVIRON yet');
            } elsif ($session->nawsMode eq 'client_agree') {
                $session->writeText('      Server has suggested NEW-ENVIRON and client has agreed');
            } elsif ($session->nawsMode eq 'client_refuse') {

                $session->writeText(
                    '      Server has suggested NEW-ENVIRON and client has refused',
                );
            }

            $session->writeText('   CHARSET (Character Set and translation)');
            if ($session->nawsMode eq 'no_invite') {
                $session->writeText('      Server has not suggested CHARSET yet');
            } elsif ($session->nawsMode eq 'client_agree') {
                $session->writeText('      Server has suggested CHARSET and client has agreed');
            } elsif ($session->nawsMode eq 'client_refuse') {
                $session->writeText('      Server has suggested CHARSET and client has refused');
            }

            # Display footer. Use a message consistent with other client commands
            return $self->complete(
                $session, $standardCmd,
                'End of telnet option list (7 options found)',
            );

        # ;sto -e
        } elsif ($switch eq '-e') {

            $axmud::CLIENT->toggle_telnetOption('echo');
            if ($axmud::CLIENT->useEchoFlag) {
                return $self->complete($session, $standardCmd, 'Telnet ECHO has been enabled');
            } else {
                return $self->complete($session, $standardCmd, 'Telnet ECHO has been disabled');
            }

        # ;sto -s
        } elsif ($switch eq '-s') {

            $axmud::CLIENT->toggle_telnetOption('sga');
            if ($axmud::CLIENT->useCharSetFlag) {
                return $self->complete($session, $standardCmd, 'Telnet SGA has been enabled');
            } else {
                return $self->complete($session, $standardCmd, 'Telnet SGA has been disabled');
            }

        # ;sto -t
        } elsif ($switch eq '-t') {

            $axmud::CLIENT->toggle_telnetOption('ttype');
            if ($axmud::CLIENT->useTTypeFlag) {
                return $self->complete($session, $standardCmd, 'Telnet TTYPE has been enabled');
            } else {
                return $self->complete($session, $standardCmd, 'Telnet TTYPE has been disabled');
            }

        # ;sto -r
        } elsif ($switch eq '-r') {

            $axmud::CLIENT->toggle_telnetOption('eor');
            if ($axmud::CLIENT->useEorFlag) {
                return $self->complete($session, $standardCmd, 'Telnet EOR has been enabled');
            } else {
                return $self->complete($session, $standardCmd, 'Telnet EOR has been disabled');
            }

        # ;sto -n
        } elsif ($switch eq '-n') {

            $axmud::CLIENT->toggle_telnetOption('naws');
            if ($axmud::CLIENT->useNawsFlag) {
                return $self->complete($session, $standardCmd, 'Telnet NAWS has been enabled');
            } else {
                return $self->complete($session, $standardCmd, 'Telnet NAWS has been disabled');
            }

        # ;sto -v
        } elsif ($switch eq '-v') {

            $axmud::CLIENT->toggle_telnetOption('new_environ');
            if ($axmud::CLIENT->useNawsFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Telnet NEW-ENVIRON has been enabled',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Telnet NEW-ENVIRON has been disabled',
                );
            }

        # ;sto -c
        } elsif ($switch eq '-c') {

            $axmud::CLIENT->toggle_telnetOption('charset');
            if ($axmud::CLIENT->useNawsFlag) {
                return $self->complete($session, $standardCmd, 'Telnet CHARSET has been enabled');
            } else {
                return $self->complete($session, $standardCmd, 'Telnet CHARSET has been disabled');
            }

        } else {

            return $self->error(
                $session, $inputString,
                'Unrecognised switch \'' . $switch . '\' - try -l, -e, -s, -t, -r, -n, -v or -c',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetMUDProtocol;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setmudprotocol', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['spt', 'setprotocol', 'setmudprotocol'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Enables/disables MUD protocols';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;spt
        if (! defined $switch) {

            # Display header
            $session->writeText('Global MUD protocol settings');

            # Display list
            if ($axmud::CLIENT->useMsdpFlag) {
                $session->writeText('   MSDP (Mud Server Data Protocol)           - ON');
            } else {
                $session->writeText('   MSDP (Mud Server Data Protocol)           - OFF');
            }

            if ($axmud::CLIENT->useMsspFlag) {
                $session->writeText('   MSSP (Mud Server Status Protocol)         - ON');
            } else {
                $session->writeText('   MSSP (Mud Server Status Protocol)         - OFF');
            }

            if ($axmud::CLIENT->useMccpFlag) {
                $session->writeText('   MCCP (Mud Client Compression Protocol)    - ON');
            } else {
                $session->writeText('   MCCP (Mud Client Compression Protocol)    - OFF');
            }

            if ($axmud::CLIENT->useMspFlag) {
                $session->writeText('   MSP (Mud Sound Protocol)                  - ON');
            } else {
                $session->writeText('   MSP (Mud Sound Protocol)                  - OFF');
            }

            if ($axmud::CLIENT->useMxpFlag) {
                $session->writeText('   MXP (Mud Extension Protocol)              - ON');
            } else {
                $session->writeText('   MXP (Mud Extension Protocol)              - OFF');
            }

            if ($axmud::CLIENT->usePuebloFlag) {
                $session->writeText('   Pueblo                                    - ON');
            } else {
                $session->writeText('   Pueblo                                    - OFF');
            }

            if ($axmud::CLIENT->useZmpFlag) {
                $session->writeText('   ZMP (Zenith Mud Protocol)                 - ON');
            } else {
                $session->writeText('   ZMP (Zenith Mud Protocol)                 - OFF');
            }

            if ($axmud::CLIENT->useAard102Flag) {
                $session->writeText('   AARDW102 (Aardwolf 102 channel)           - ON');
            } else {
                $session->writeText('   AARD102 (Aardwolf 102 channel)            - OFF');
            }

            if ($axmud::CLIENT->useAtcpFlag) {
                $session->writeText('   ATCP (Achaea Telnet Client Protocol)      - ON');
            } else {
                $session->writeText('   ATCP (Achaea Telnet Client Protocol)      - OFF');
            }

            if ($axmud::CLIENT->useGmcpFlag) {
                $session->writeText('   GMCP (Generic Mud Communication Protocol) - ON');
            } else {
                $session->writeText('   GMCP (Generic Mud Communication Protocol) - OFF');
            }

            if ($axmud::CLIENT->useMttsFlag) {
                $session->writeText('   MTTS (Mud Terminal Type Standard)         - ON');
            } else {
                $session->writeText('   MTTS (Mud Terminal Type Standard)         - OFF');
            }

            if ($axmud::CLIENT->useMnesFlag) {
                $session->writeText('   MNES (MUD NEW-ENVIRON Standard)           - ON');
            } else {
                $session->writeText('   MNES (MUD NEW-ENVIRON Standard)           - OFF');
            }

            if ($axmud::CLIENT->useMcpFlag) {
                $session->writeText('   MCP (Mud Client Protocol)                 - ON');
            } else {
                $session->writeText('   MCP (Mud Client Protocol)                 - OFF');
            }

            # Display footer. Use a message consistent with other client commands
            return $self->complete(
                $session, $standardCmd,
                'End of mud protocol list (12 protocols found)',
            );

        # ;spt -l
        } elsif ($switch eq '-l') {

            # Display header
            $session->writeText('Session\'s mud protocol status:');

            # Display list
            $session->writeText('   MSDP (Mud Server Data Protocol)');
            if ($session->msdpMode eq 'no_invite') {
                $session->writeText('      Server has not suggested MSDP yet');
            } elsif ($session->msdpMode eq 'client_agree') {
                $session->writeText('      Server has suggested MSDP and client has agreed');
            } elsif ($session->msdpMode eq 'client_refuse') {
                $session->writeText('      Server has suggested MSDP and client has refused');
            }

            $session->writeText('   MSSP (Mud Server Status Protocol)');
            if ($session->msspMode eq 'no_invite') {
                $session->writeText('      Server has not suggested MSSP yet');
            } elsif ($session->msspMode eq 'client_agree') {
                $session->writeText('      Server has suggested MSSP and client has agreed');
            } elsif ($session->msspMode eq 'client_refuse') {
                $session->writeText('      Server has suggested MSSP and client has refused');
            }

            $session->writeText('   MCCP (Mud Client Compression Protocol)');
            if ($session->mccpMode eq 'no_invite') {
                $session->writeText('      Server has not suggested MCCP yet');
            } elsif ($session->mccpMode eq 'client_agree') {
                $session->writeText('      Server has suggested MCCP and client has agreed');
            } elsif ($session->mccpMode eq 'client_refuse') {
                $session->writeText('      Server has suggested MCCP and client has refused');
            } elsif ($session->mccpMode eq 'compress_start') {
                $session->writeText('      Server has signalled MCCP compression has begun');
            } elsif ($session->mccpMode eq 'compress_error') {
                $session->writeText('      MCCP has stopped after a compression error');
            } elsif ($session->mccpMode eq 'compress_stop') {
                $session->writeText('      Server has terminated MCCP compression');
            }

            $session->writeText('   MSP (Mud Sound Protocol)');
            if ($session->mspMode eq 'no_invite') {
                $session->writeText('      Server has not suggested MSP yet');
            } elsif ($session->mspMode eq 'client_agree') {
                $session->writeText('      Server has suggested MSP and client has agreed');
            } elsif ($session->mspMode eq 'client_refuse') {
                $session->writeText('      Server has suggested MSP and client has refused');
            } elsif ($session->mspMode eq 'client_simulate') {

                $session->writeText(
                    '      Server did not suggest MSP, but ' . $axmud::SCRIPT
                    . ' is responding to MSP sound/music triggers',
                );
            }

            $session->writeText('   MXP (Mud Extension Protocol)');
            if ($session->mxpMode eq 'no_invite') {
                $session->writeText('      Server has not suggested MXP yet');
            } elsif ($session->mxpMode eq 'client_agree') {
                $session->writeText('      Server has suggested MXP and client has agreed');
            } elsif ($session->mxpMode eq 'client_refuse') {
                $session->writeText('      Server has suggested MXP and client has refused');
            }

            $session->writeText('   Pueblo');
            if ($session->puebloMode eq 'no_invite') {
                $session->writeText('      Server has not suggested Pueblo yet');
            } elsif ($session->puebloMode eq 'client_agree') {
                $session->writeText('      Server has suggested Pueblo and client has agreed');
            } elsif ($session->puebloMode eq 'client_refuse') {
                $session->writeText('      Server has suggested Pueblo and client has refused');
            }

            $session->writeText('   ZMP (Zenith Mud Protocol)');
            if ($session->zmpMode eq 'no_invite') {
                $session->writeText('      Server has not suggested ZMP yet');
            } elsif ($session->zmpMode eq 'client_agree') {
                $session->writeText('      Server has suggested ZMP and client has agreed');
            } elsif ($session->zmpMode eq 'client_refuse') {
                $session->writeText('      Server has suggested ZMP and client has refused');
            }

            $session->writeText('   AARD102 (Aardwolf 102 channel)');
            if ($session->aard102Mode eq 'no_invite') {
                $session->writeText('      Server has not suggested AARD102 yet');
            } elsif ($session->aard102Mode eq 'client_agree') {
                $session->writeText('      Server has suggested AARD102 and client has agreed');
            } elsif ($session->aard102Mode eq 'client_refuse') {
                $session->writeText('      Server has suggested AARD102 and client has refused');
            }

            $session->writeText('   ATCP (Achaea Telnet Client Protocol)');
            if ($session->atcpMode eq 'no_invite') {
                $session->writeText('      Server has not suggested ATCP yet');
            } elsif ($session->atcpMode eq 'client_agree') {
                $session->writeText('      Server has suggested ATCP and client has agreed');
            } elsif ($session->atcpMode eq 'client_refuse') {
                $session->writeText('      Server has suggested ATCP and client has refused');
            }

            $session->writeText('   GMCP (Generic MUD Communication Protocol)');
            if ($session->gmcpMode eq 'no_invite') {
                $session->writeText('      Server has not suggested GMCP yet');
            } elsif ($session->gmcpMode eq 'client_agree') {
                $session->writeText('      Server has suggested GMCP and client has agreed');
            } elsif ($session->gmcpMode eq 'client_refuse') {
                $session->writeText('      Server has suggested GMCP and client has refused');
            }

            $session->writeText('   MTTS (Mud Terminal Type Standard)');
            if ($session->specifiedTType) {
                $session->writeText('      Preferred terminal: ' . $session->specifiedTType);
            } else {
                $session->writeText('      Preferred terminal: (not sent)');
            }

            $session->writeText('   MNES (MUD NEW-ENVIRON Standard)');
            if ($axmud::CLIENT->allowMnesSendIPFlag) {
                $session->writeText('      Sending user\'s IP address: YES');
            } else {
                $session->writeText('      Sending user\'s IP address: NO');
            }

            $session->writeText('   MCP (Mud Client Protocol)');
            if ($session->mcpMode eq 'no_invite') {
                $session->writeText('      Server has not suggested MCP yet');
            } elsif ($session->mcpMode eq 'client_agree') {
                $session->writeText('      Server has suggested MCP and client has agreed');
            } elsif ($session->mcpMode eq 'client_refuse') {
                $session->writeText('      Server has suggested MCP and client has refused');
            }

            # Display footer. Use a message consistent with other client commands
            return $self->complete(
                $session, $standardCmd,
                'End of mud protocol list (12 protocols found)',
            );

        # ;spt -d
        } elsif ($switch eq '-d') {

            $axmud::CLIENT->toggle_mudProtocol('msdp');

            if ($axmud::CLIENT->useMsdpFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'The MSDP protocol has been enabled across all sessions',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'The MSDP protocol has been disabled across all sessions',
                );
            }

        # ;spt -s
        } elsif ($switch eq '-s') {

            $axmud::CLIENT->toggle_mudProtocol('mssp');

            if ($axmud::CLIENT->useMsspFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'The MSSP protocol has been enabled across all sessions',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'The MSSP protocol has been disabled across all sessions',
                );
            }

        # ;spt -c
        } elsif ($switch eq '-c') {

            $axmud::CLIENT->toggle_mudProtocol('mccp');

            if ($axmud::CLIENT->useMccpFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'The MCCP protocol has been enabled across all sessions',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'The MCCP protocol has been disabled across all sessions',
                );
            }

        # ;spt -y
        } elsif ($switch eq '-y') {

            $axmud::CLIENT->toggle_mudProtocol('msp');

            if ($axmud::CLIENT->useMspFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'The MSP protocol has been enabled across all sessions',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'The MSP protocol has been disabled across all sessions',
                );
            }

        # ;spt -x
        } elsif ($switch eq '-x') {

            $axmud::CLIENT->toggle_mudProtocol('mxp');

            if ($axmud::CLIENT->useMxpFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'The MXP protocol has been enabled across all sessions',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'The MXP protocol has been disabled across all sessions',
                );
            }

        # ;spt -p
        } elsif ($switch eq '-p') {

            $axmud::CLIENT->toggle_mudProtocol('pueblo');

            if ($axmud::CLIENT->usePuebloFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'The Pueblo protocol has been enabled across all sessions',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'The Pueblo protocol has been disabled across all sessions',
                );
            }

        # ;spt -z
        } elsif ($switch eq '-z') {

            $axmud::CLIENT->toggle_mudProtocol('zmp');

            if ($axmud::CLIENT->useZmpFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'The ZMP protocol has been enabled across all sessions',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'The ZMP protocol has been disabled across all sessions',
                );
            }

        # ;spt -r
        } elsif ($switch eq '-r') {

            $axmud::CLIENT->toggle_mudProtocol('aard102');

            if ($axmud::CLIENT->useAard102Flag) {

                return $self->complete(
                    $session, $standardCmd,
                    'The Aardwolf 102 channel has been enabled across all sessions',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'The Aardwolf 102 channel has been disabled across all sessions',
                );
            }

        # ;spt -a
        } elsif ($switch eq '-a') {

            $axmud::CLIENT->toggle_mudProtocol('atcp');

            if ($axmud::CLIENT->useAtcpFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'The ATCP protocol has been enabled across all sessions',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'The ATCP protocol has been disabled across all sessions',
                );
            }

        # ;spt -g
        } elsif ($switch eq '-g') {

            $axmud::CLIENT->toggle_mudProtocol('gmcp');

            if ($axmud::CLIENT->useGmcpFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'The GMCP protocol has been enabled across all sessions',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'The GMCP protocol has been disabled across all sessions',
                );
            }

        # ;spt -t
        } elsif ($switch eq '-t') {

            $axmud::CLIENT->toggle_mudProtocol('mtts');

            if ($axmud::CLIENT->useMttsFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'The MTTS protocol has been enabled across all sessions',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'The MTTS protocol has been disabled across all sessions',
                );
            }

        # ;spt -n
        } elsif ($switch eq '-n') {

            $axmud::CLIENT->toggle_mudProtocol('mnes');

            if ($axmud::CLIENT->useMttsFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'The MNES protocol has been enabled across all sessions',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'The MNES protocol has been disabled across all sessions',
                );
            }

        # ;spt -m
        } elsif ($switch eq '-m') {

            $axmud::CLIENT->toggle_mudProtocol('mcp');

            if ($axmud::CLIENT->useMcpFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'The MCP protocol has been enabled across all sessions',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'The MCP protocol has been disabled across all sessions',
                );
            }

        } else {

            return $self->error(
                $session, $inputString,
                'Unrecognised switch \'' . $switch . '\' - try \';help setmudprotocol\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetTermType;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('settermtype', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['stt', 'setterm', 'settermtype'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the data sent during TTYPE/MTTS negotiations';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $string,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;stt
        if (! defined $switch) {

            # Display header
            $session->writeText('List of data sent during TTYPE/MTTS negotiations');

            # Display list
            if ($axmud::CLIENT->termTypeMode eq 'send_nothing') {

                $session->writeText('   \'send_nothing\' - Nothing is sent');

            } elsif ($axmud::CLIENT->termTypeMode eq 'send_client') {

                $session->writeText(
                    '   \'send_client\' - Send client name, followed by usual termtype list',
                );

            } elsif ($axmud::CLIENT->termTypeMode eq 'send_client_version') {

                $session->writeText(
                    '   \'send_client_version\' - Send client name and version, followed by usual'
                    . ' termtype list',
                );

            } elsif ($axmud::CLIENT->termTypeMode eq 'send_custom_client') {

                $session->writeText(
                    '   \'send_custom_client\' - Send custom client name/version, followed by usual'
                    . ' termtype list',
                );

            } elsif ($axmud::CLIENT->termTypeMode eq 'send_default') {

                $session->writeText(
                    '   \'send_default\' - Send the usual termtype list',
                );

            } else {

                $session->writeText(
                    '   \'send_unknown\' - Send the termtype \'unknown\'',
                );
            }

            if (! $axmud::CLIENT->customClientName) {

                $session->writeText('      Custom client name    : (not set, and not used)');

            } else {

                $session->writeText(
                    '      Custom client name    : \'' .  $axmud::CLIENT->customClientName,
                );
            }

            if (! $axmud::CLIENT->customClientVersion) {

                $session->writeText('      Custom client version : (not set, and not used)');

            } else {

                $session->writeText(
                    '      Custom client version : \'' .  $axmud::CLIENT->customClientVersion,
                );
            }

            $session->writeText(
                '      Usual termtype list   : ' . join(' ', $axmud::CLIENT->constTermTypeList),
            );

            # Display footer
            return $self->complete($session, $standardCmd, 'End of list');

        # ;stt -s
        } elsif ($switch eq '-s') {

            $axmud::CLIENT->set_termTypeMode('send_nothing');

            return $self->complete(
                $session, $standardCmd,
                'Set send nothing during termptype negotiations',
            );

        # ;stt -a
        } elsif ($switch eq '-a') {

            $axmud::CLIENT->set_termTypeMode('send_client');

            return $self->complete(
                $session, $standardCmd,
                'Set send client name, followed by usual termtype list',
            );

        # ;stt -x
        } elsif ($switch eq '-x') {

            $axmud::CLIENT->set_termTypeMode('send_client_version');

            return $self->complete(
                $session, $standardCmd,
                'Set send client name and version, followed by usual termtype list',
            );

        # ;stt -c
        } elsif ($switch eq '-c') {

            $axmud::CLIENT->set_termTypeMode('send_custom_client');

            return $self->complete(
                $session, $standardCmd,
                'Set send custom client name/version, followed by usual termtype list',
            );

        # ;stt -d
        } elsif ($switch eq '-d') {

            $axmud::CLIENT->set_termTypeMode('send_default');

            return $self->complete(
                $session, $standardCmd,
                'Set send the usual termtype list during termptype negotiations',
            );

        # ;stt -u
        } elsif ($switch eq '-u') {

            $axmud::CLIENT->set_termTypeMode('send_unknown');

            return $self->complete(
                $session, $standardCmd,
                'Set send \'unknown\' during termptype negotiations',
            );

        # ;stt -n <name>
        # ;stt -n
        } elsif ($switch eq '-n') {

            if (! defined $string) {

                $axmud::CLIENT->set_customClientName('');

                return $self->complete(
                    $session, $standardCmd,
                    'Termtype negotiation custom client name reset',
                );

            } else {

                $axmud::CLIENT->set_customClientName($string);

                return $self->complete(
                    $session, $standardCmd,
                    'Termtype negotiation custom client name set to \'' . $string . '\'',
                );
            }

        # ;stt -v <version>
        # ;stt -v
        } elsif ($switch eq '-v') {

            if (! defined $string) {

                $axmud::CLIENT->set_customClientVersion('');

                return $self->complete(
                    $session, $standardCmd,
                    'Termtype negotiation custom client version reset',
                );

            } else {

                $axmud::CLIENT->set_customClientVersion($string);

                return $self->complete(
                    $session, $standardCmd,
                    'Termtype negotiation custom client version set to \'' . $string . '\'',
                );
            }

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid switch (try \';help settermtype\')',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ConfigureTerminal;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('configureterminal', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ctl', 'configterm', 'configureterminal'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Configures terminal settings';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;ctl
        if (! defined $switch) {

            # Display header
            $session->writeText('Global terminal settings');

            # Display list
            if ($axmud::CLIENT->useCtrlSeqFlag) {
                $session->writeText('   Use VT100 control sequences             - ON');
            } else {
                $session->writeText('   Use VT100 control sequences             - OFF');
            }

            if ($axmud::CLIENT->useVisibleCursorFlag) {
                $session->writeText('   Show visible cursor in default textview - ON');
            } else {
                $session->writeText('   Show visible cursor in default textview - OFF');
            }

            if ($axmud::CLIENT->useFastCursorFlag) {
                $session->writeText('   Use a rapidly-blinking cursor           - ON');
            } else {
                $session->writeText('   Use a rapidly-blinking cursor           - OFF');
            }

            if ($axmud::CLIENT->useDirectKeysFlag) {
                $session->writeText('   Use direct keyboard input in terminal   - ON');
            } else {
                $session->writeText('   Use direct keyboard input in terminal   - OFF');
            }

            # Display footer. Use a message consistent with other client commands
            return $self->complete(
                $session, $standardCmd,
                'End of terminal settings list (3 settings found)',
            );

        # ;ctl -s
        } elsif ($switch eq '-s') {

            $axmud::CLIENT->toggle_termSetting('use_ctrl_seq');
            if ($axmud::CLIENT->useCtrlSeqFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Use of VT100 control sequences has been enabled',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Use of VT100 control sequences has been disabled',
                );
            }

        # ;ctl -c
        } elsif ($switch eq '-c') {

            $axmud::CLIENT->toggle_termSetting('show_cursor');
            if ($axmud::CLIENT->useVisibleCursorFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Show visible cursor in default textview has been enabled',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Show visible cursor in default textview has been disabled',
                );
            }

        # ;ctl -f
        } elsif ($switch eq '-f') {

            $axmud::CLIENT->toggle_termSetting('fast_cursor');
            if ($axmud::CLIENT->useVisibleCursorFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Use of a rapidly-blinking cursor has been enabled',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Use of a rapidly-blinking cursor has been disabled',
                );
            }

        # ;ctl -d
        } elsif ($switch eq '-d') {

            $axmud::CLIENT->toggle_termSetting('direct_keys');
            if ($axmud::CLIENT->useDirectKeysFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Use direct keyboard input in terminal has been enabled',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Use direct keyboard input in terminal has been disabled',
                );
            }

        } else {

            return $self->error(
                $session, $inputString,
                'Unrecognised switch \'' . $switch . '\' - try -s, -c or -d',
            );
        }
    }
}

{ package Games::Axmud::Cmd::MSDP;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('msdp', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['msdp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows MSDP data reported by the current world';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            %genericCmdHash, %customCmdHash, %genericListHash, %customListHash,
            %genericConfigFlagHash, %customConfigFlagHash, %genericConfigValHash,
            %customConfigValHash, %genericReportableFlagHash, %customReportableFlagHash,
            %genericReportedFlagHash, %customReportedFlagHash, %genericSendableFlagHash,
            %customSendableFlagHash, %genericValueHash, %customValueHash, %combGenericHash,
            %combCustomHash,
        );

        # Check for improper arguments
        if ((defined $switch && $switch ne '-e' && $switch ne '-f') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;msdp -e
        if (defined $switch && $switch eq '-e') {

            $session->resetMsdpData();

            return $self->complete(
                $session, $standardCmd,
                'MSDP data reported by the current world has been emptied',
            );

        # ;msdp
        } else {

            # Import collected MSDP data (for convenience)
            %genericCmdHash = $session->msdpGenericCmdHash;
            %customCmdHash = $session->msdpCustomCmdHash;
            %genericListHash = $session->msdpGenericListHash;
            %customListHash = $session->msdpCustomListHash;
            %genericConfigFlagHash = $session->msdpGenericConfigFlagHash;
            %customConfigFlagHash = $session->msdpCustomConfigFlagHash;
            %genericConfigValHash = $session->msdpGenericConfigValHash;
            %customConfigValHash = $session->msdpCustomConfigValHash;
            %genericReportableFlagHash = $session->msdpGenericReportableFlagHash;
            %customReportableFlagHash = $session->msdpCustomReportableFlagHash;
            %genericReportedFlagHash = $session->msdpGenericReportedFlagHash;
            %customReportedFlagHash = $session->msdpCustomReportedFlagHash;
            %genericSendableFlagHash = $session->msdpGenericSendableFlagHash;
            %customSendableFlagHash = $session->msdpCustomSendableFlagHash;
            %genericValueHash = $session->msdpGenericValueHash;
            %customValueHash = $session->msdpCustomValueHash;

            # Display header
            $session->writeText('MSDP data reported by \'' . $session->currentWorld->name . '\'');

            # Display list
            if (defined $switch && $switch eq '-f') {

                # Display generic/custom commands
                $session->writeText('   Generic commands (* - supported)');
                foreach my $key (sort {lc($a) cmp lc($b)} (keys %genericCmdHash)) {

                    if ($genericCmdHash{$key}) {
                        $session->writeText('      * ' . $key);
                    } else {
                        $session->writeText('        ' . $key);
                    }
                }
                $session->writeText('   Custom commands (* - supported)');
                if (! %customCmdHash) {

                    $session->writeText('        <none>');

                } else {

                    foreach my $key (sort {lc($a) cmp lc($b)} (keys %customCmdHash)) {

                        if ($customCmdHash{$key}) {
                            $session->writeText('      * ' . $key);
                        } else {
                            $session->writeText('        ' . $key);
                        }
                    }
                }

                # Display generic/custom lists
                $session->writeText('   Generic lists (* - supported)');
                foreach my $key (sort {lc($a) cmp lc($b)} (keys %genericListHash)) {

                    if ($genericListHash{$key}) {
                        $session->writeText('      * ' . $key);
                    } else {
                        $session->writeText('        ' . $key);
                    }
                }
                $session->writeText('   Custom lists (* - supported)');
                if (! %customListHash) {

                    $session->writeText('        <none>');

                } else {

                    foreach my $key (sort {lc($a) cmp lc($b)} (keys %customListHash)) {

                        if ($customListHash{$key}) {
                            $session->writeText('      * ' . $key);
                        } else {
                            $session->writeText('        ' . $key);
                        }
                    }
                }

                # Display configurable variables
                $session->writeText('   Generic configurable variables (* - supported)');
                foreach my $key (sort {lc($a) cmp lc($b)} (keys %genericConfigFlagHash)) {

                    my ($flag, $val, $string);

                    $flag = $genericConfigFlagHash{$key};
                    $val = $genericConfigValHash{$key};

                    if ($flag) {
                        $string = '      * ';
                    } else {
                        $string = '        ';
                    }

                    $string .= sprintf('%-32.32s', $key);
                    if (defined $val) {

                        $string .= ' ' . $val;
                    }

                    $session->writeText($string);
                }

                $session->writeText('   Custom configurable variables (* - supported)');
                if (! %customConfigFlagHash) {

                    $session->writeText('        <none>');

                } else {

                    foreach my $key (sort {lc($a) cmp lc($b)} (keys %customConfigFlagHash)) {

                        my ($flag, $val, $string);

                        $flag = $customConfigFlagHash{$key};
                        $val = $customConfigValHash{$key};

                        if ($flag) {
                            $string = '      * ';
                        } else {
                            $string = '        ';
                        }

                        $string .= sprintf('%-32.32s', $key);
                        if (defined $val) {

                            $string .= ' ' . $val;
                        }

                        $session->writeText($string);
                    }
                }
            }

            # Display reportable/reported variables
            $session->writeText(
                '   Generic reportable variables (* - reportable # - reported = - sendable)',
            );

            # (Compile a single hash of keys which exist in all three flag hashes)
            foreach my $key (keys %genericReportableFlagHash) {

                $combGenericHash{$key} = undef;
            }

            foreach my $key (keys %genericReportedFlagHash) {

                $combGenericHash{$key} = undef;
            }

            foreach my $key (keys %genericSendableFlagHash) {

                $combGenericHash{$key} = undef;
            }

            # (Display them)
            foreach my $key (sort {lc($a) cmp lc($b)} (keys %combGenericHash)) {

                my (
                    $reportFlag, $reportedFlag, $sendFlag, $val, $string,
                    @lineList,
                );

                $reportFlag = $genericReportableFlagHash{$key};
                $reportedFlag = $genericReportedFlagHash{$key};
                $sendFlag = $genericSendableFlagHash{$key};
                $val = $genericValueHash{$key};

                if ($reportFlag) {
                    $string = '    *';
                } else {
                    $string = '     ';
                }

                if ($reportedFlag) {
                    $string .= '#';
                } else {
                    $string .= ' ';
                }

                if ($sendFlag) {
                    $string .= '= ';
                } else {
                    $string .= '  ';
                }

                $string .= sprintf('%-32.32s', $key);
                if (defined $val) {

                    @lineList = $self->parseMsdpScalar($val, 0);
                    $string .= ' ' . shift @lineList;
                }

                $session->writeText($string);
                foreach my $line (@lineList) {

                    $session->writeText('                                         ' . $line);
                }
            }

            $session->writeText(
                '   Custom reportable variables (* - reportable # - reported = - sendable)',
            );
            if (
                ! %customReportableFlagHash
                && ! %customReportedFlagHash
                && ! %customSendableFlagHash
            ) {
                $session->writeText('        <none>');

            } else {

                # (Compile a single hash of keys which exist in all three flag hashes)
                foreach my $key (keys %customReportableFlagHash) {

                    $combCustomHash{$key} = undef;
                }

                foreach my $key (keys %customReportedFlagHash) {

                    $combCustomHash{$key} = undef;
                }

                foreach my $key (keys %customSendableFlagHash) {

                    $combCustomHash{$key} = undef;
                }

                # (Display them)
                foreach my $key (sort {lc($a) cmp lc($b)} (keys %combCustomHash)) {

                    my (
                        $reportFlag, $reportedFlag, $sendFlag, $val, $string,
                        @lineList,
                    );

                    $reportFlag = $customReportableFlagHash{$key};
                    $reportedFlag = $customReportedFlagHash{$key};
                    $sendFlag = $customSendableFlagHash{$key};
                    $val = $customValueHash{$key};

                    if ($reportFlag) {
                        $string = '    *';
                    } else {
                        $string = '     ';
                    }

                    if ($reportedFlag) {
                        $string .= '#';
                    } else {
                        $string .= ' ';
                    }

                    if ($sendFlag) {
                        $string .= '= ';
                    } else {
                        $string .= '  ';
                    }

                    $string .= sprintf('%-32.32s', $key);
                    if (defined $val) {

                        @lineList = $self->parseMsdpScalar($val, 0);
                        $string .= ' ' . shift @lineList;
                    }

                    $session->writeText($string);
                    foreach my $line (@lineList) {

                        $session->writeText('                                         ' . $line);
                    }
                }
            }

            # Display footer
            return $self->complete($session, $standardCmd, 'End of MSDP list');
        }
    }

    sub parseMsdpScalar {

        # Called by $self->do and recursively by ->parseMsdpScalar, ->parseMsdpArray and
        #   ->parseMsdpHash
        # The value of an MSDP variable can be a scalar, or a list/hash reference representing an
        #   embedded array/table. Call these functions recursively to reduce them all to a list
        #   of indented lines, with each indentation representing an embedded array/table
        #
        # Expected arguments
        #   $arg        - A scalar, or a list/hash reference
        #   $columns    - The size of the indentation, 0 or a positive integer
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise returns the modified list of indented lines

        my ($self, $arg, $columns, $check) = @_;

        # Local variables
        my (@emptyList, @lineList);

        # Check for improper arguments
        if (! defined $arg || ! defined $columns || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->parseMsdpScalar', @_);
            return @emptyList;
        }

        if (ref $arg eq 'HASH') {
            push (@lineList, $self->parseMsdpTable($arg, ($columns + 1)));
        } elsif (ref $arg eq 'ARRAY') {
            push (@lineList, $self->parseMsdpArray($arg, ($columns + 1)));
        } else {
            push (@lineList, (' ' x $columns) . $arg);
        }

        return @lineList;
    }

    sub parseMsdpArray {

        # Called by $self->do and recursively by ->parseMsdpScalar, ->parseMsdpArray and
        #   ->parseMsdpHash
        # The value of an MSDP variable can be a scalar, or a list/hash reference representing an
        #   embedded array/table. Call these functions recursively to reduce them all to a list
        #   of indented lines, with each indentation representing an embedded array/table
        #
        # Expected arguments
        #   $arg        - A list reference
        #   $columns    - The size of the indentation, 0 or a positive integer
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise returns the modified list of indented lines

        my ($self, $arg, $columns, $check) = @_;

        # Local variables
        my (@emptyList, @lineList);

        # Check for improper arguments
        if (! defined $arg || ! defined $columns || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->parseMsdpArray', @_);
            return @emptyList;
        }

        foreach my $item (@$arg) {

            if (ref $item eq 'HASH') {
                push (@lineList, $self->parseMsdpTable($item, ($columns + 1)));
            } elsif (ref $item eq 'ARRAY') {
                push (@lineList, $self->parseMsdpArray($item, ($columns + 1)));
            } else {
                push (@lineList, (' ' x $columns) . $item);
            }
        }

        return @lineList;
    }

    sub parseMsdpHash {

        # Called by $self->do and recursively by ->parseMsdpScalar, ->parseMsdpArray and
        #   ->parseMsdpHash
        # The value of an MSDP variable can be a scalar, or a list/hash reference representing an
        #   embedded array/table. Call these functions recursively to reduce them all to a list
        #   of indented lines, with each indentation representing an embedded array/table
        #
        # Expected arguments
        #   $arg        - A hash reference
        #   $columns    - The size of the indentation, 0 or a positive integer
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise returns the modified list of indented lines

        my ($self, $arg, $columns, $check) = @_;

        # Local variables
        my (@emptyList, @lineList);

        # Check for improper arguments
        if (! defined $arg || ! defined $columns || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->parseMsdpHash', @_);
            return @emptyList;
        }

        foreach my $key (sort {lc($a) cmp lc($b)} (keys %$arg)) {

            my $value = $$arg{$key};

            if (ref $value eq 'HASH') {
                push (@lineList, $self->parseMsdpTable($value, ($columns + 1)));
            } elsif (ref $value eq 'ARRAY') {
                push (@lineList, $self->parseMsdpArray($value, ($columns + 1)));
            } else {
                push (@lineList, (' ' x $columns) . $key . ' = ' . $value);
            }
        }

        return @lineList;
    }
}

{ package Games::Axmud::Cmd::MSSP;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('mssp', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mssp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows MSSP data collected from the current world';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            @sortedList,
            %hash, %otherHash,
        );

        # Check for improper arguments
        if ((defined $switch && $switch ne '-e') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;mssp -e
        if ($switch) {

            $session->currentWorld->ivEmpty('msspGenericValueHash');
            $session->currentWorld->ivEmpty('msspCustomValueHash');

            return $self->complete(
                $session, $standardCmd,
                'MSSP data for the \'' . $session->currentWorld->name . '\' world profile has'
                . ' been emptied',
            );

        # ;mssp
        } else {

            # Import the collected MSSP data (for convenience)
            %hash = $session->currentWorld->msspGenericValueHash;
            %otherHash = $session->currentWorld->msspCustomValueHash;
            if (! %hash && ! %otherHash) {

                return $self->complete(
                    $session, $standardCmd,
                    'No MSSP data has been collected for the \'' . $session->currentWorld->name
                    . '\' world profile',
                );
            }

            # Display header
            $session->writeText('MSSP data collected for \'' . $session->currentWorld->name . '\'');

            # Display list

            # Display official variables. Items beginning with a '#' character are group headings
            foreach my $item ($axmud::CLIENT->constMsspVarList) {

                if (substr($item, 0, 1) eq '#') {
                    $session->writeText('   ' . $item);
                } elsif (exists $hash{$item}) {
                    $session->writeText(sprintf('      %-20.20s', $item) . ' ' . $hash{$item});
                } else {
                    $session->writeText(sprintf('      %-20.20s', $item));
                }
            }

            # Display unofficial variables (if any)
            if (%otherHash) {

                $session->writeText('   Unofficial variables');

                @sortedList = sort {lc($a) cmp lc($b)} (keys %otherHash);
                foreach my $item (@sortedList) {

                    $session->writeText(sprintf('      %-20.20s', $item) . ' ' . $otherHash{$item});
                }
            }

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of list (variables: ' . (scalar (keys %otherHash)) . ', unofficial variables:'
                . (scalar (keys %otherHash)) . ')',
            );
        }
    }
}

{ package Games::Axmud::Cmd::MXP;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('mxp', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mxp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Manages the Mud Xtension Protocol (MXP)';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my $string;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;mxp
        if (! $switch) {

            # Display header
            $session->writeText('Mud Xtension Protocol (MXP)');

            # Display list
            $string = '   Allow MXP in general                          - ';
            if ($axmud::CLIENT->useMxpFlag) {
                $session->writeText($string . ' yes');
            } else {
                $session->writeText($string . ' no');
            }

            $string = '   Allow MXP to change fonts                     - ';
            if ($axmud::CLIENT->allowMxpFontFlag) {
                $session->writeText($string . ' yes');
            } else {
                $session->writeText($string . ' no');
            }

            $string = '   Allow MXP to display images                   - ';
            if ($axmud::CLIENT->allowMxpImageFlag) {
                $session->writeText($string . ' yes');
            } else {
                $session->writeText($string . ' no');
            }

            $string = '   Allow MXP to download image files             - ';
            if ($axmud::CLIENT->allowMxpLoadImageFlag) {
                $session->writeText($string . ' yes');
            } else {
                $session->writeText($string . ' no');
            }

            $string = '   Allow MXP to use world\'s own graphics formats - ';
            if ($axmud::CLIENT->allowMxpFilterImageFlag) {
                $session->writeText($string . ' yes');
            } else {
                $session->writeText($string . ' no');
            }

            $string = '   Allow MXP to play sound/music files           - ';
            if ($axmud::CLIENT->allowMxpSoundFlag) {
                $session->writeText($string . ' yes');
            } else {
                $session->writeText($string . ' no');
            }

            $string = '   Allow MXP to download sound/music files       - ';
            if ($axmud::CLIENT->allowMxpLoadSoundFlag) {
                $session->writeText($string . ' yes');
            } else {
                $session->writeText($string . ' no');
            }

            $string = '   Allow MXP to display gauges/status bars       - ';
            if ($axmud::CLIENT->allowMxpGaugeFlag) {
                $session->writeText($string . ' yes');
            } else {
                $session->writeText($string . ' no');
            }

            $string = '   Allow MXP to use frames                       - ';
            if ($axmud::CLIENT->allowMxpFrameFlag) {
                $session->writeText($string . ' yes');
            } else {
                $session->writeText($string . ' no');
            }

            $string = '   Allow MXP to use frames inside \'main\' windows - ';
            if ($axmud::CLIENT->allowMxpInteriorFlag) {
                $session->writeText($string . ' yes');
            } else {
                $session->writeText($string . ' no');
            }

            $string = '   Allow MXP to crosslink to new servers         - ';
            if ($axmud::CLIENT->allowMxpCrosslinkFlag) {
                $session->writeText($string . ' yes');
            } else {
                $session->writeText($string . ' no');
            }

            $string = '   Allow Locator task to rely on MXP room data   - ';
            if ($axmud::CLIENT->allowMxpRoomFlag) {
                $session->writeText($string . ' yes');
            } else {
                $session->writeText($string . ' no');
            }

            $string = '   Allow MXP to use (some) illegal keywords      - ';
            if ($axmud::CLIENT->allowMxpFlexibleFlag) {
                $session->writeText($string . ' yes');
            } else {
                $session->writeText($string . ' no');
            }

            $string = '   Assume world has enabled MXP                  - ';
            if ($axmud::CLIENT->allowMxpPermFlag) {
                $session->writeText($string . ' yes');
            } else {
                $session->writeText($string . ' no');
            }

            # Display footer
            return $self->complete($session, $standardCmd, 'End of list');

        # ;mxp -f
        } elsif ($switch eq '-f') {

            $axmud::CLIENT->set_allowMxpFlag('font', ! $axmud::CLIENT->allowMxpFontFlag);
            if (! $axmud::CLIENT->allowMxpFontFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Allow MXP to change fonts set to ' . $string,
            );

        # ;mxp -i
        } elsif ($switch eq '-i') {

            $axmud::CLIENT->set_allowMxpFlag('image', ! $axmud::CLIENT->allowMxpImageFlag);
            if (! $axmud::CLIENT->allowMxpImageFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Allow MXP to display images set to ' . $string,
            );

        # ;mxp -l
        } elsif ($switch eq '-l') {

            $axmud::CLIENT->set_allowMxpFlag('load_image', ! $axmud::CLIENT->allowMxpLoadImageFlag);
            if (! $axmud::CLIENT->allowMxpLoadImageFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Allow MXP to download image files set to ' . $string,
            );

        # ;mxp -t
        } elsif ($switch eq '-t') {

            $axmud::CLIENT->set_allowMxpFlag(
                'filter_image',
                ! $axmud::CLIENT->allowMxpFilterImageFlag,
            );

            if (! $axmud::CLIENT->allowMxpFilterImageFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Allow MXP to use world\'s own graphics formats set to ' . $string,
            );

        # ;mxp -s
        } elsif ($switch eq '-s') {

            $axmud::CLIENT->set_allowMxpFlag('sound', ! $axmud::CLIENT->allowMxpSoundFlag);
            if (! $axmud::CLIENT->allowMxpSoundFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Allow MXP to play sound/music files set to ' . $string,
            );

        # ;mxp -o
        } elsif ($switch eq '-o') {

            $axmud::CLIENT->set_allowMxpFlag('load_sound', ! $axmud::CLIENT->allowMxpLoadSoundFlag);
            if (! $axmud::CLIENT->allowMxpFontFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Allow MXP to download sound/music files set to ' . $string,
            );

        # ;mxp -g
        } elsif ($switch eq '-g') {

            $axmud::CLIENT->set_allowMxpFlag('gauge', ! $axmud::CLIENT->allowMxpGaugeFlag);
            if (! $axmud::CLIENT->allowMxpGaugeFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Allow MXP to display gauges/status bars set to ' . $string,
            );

        # ;mxp -a
        } elsif ($switch eq '-a') {

            $axmud::CLIENT->set_allowMxpFlag('frame', ! $axmud::CLIENT->allowMxpFrameFlag);
            if (! $axmud::CLIENT->allowMxpFrameFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Allow MXP to use frames set to ' . $string,
            );

        # ;mxp -n
        } elsif ($switch eq '-n') {

            $axmud::CLIENT->set_allowMxpFlag('interior', ! $axmud::CLIENT->allowMxpInteriorFlag);
            if (! $axmud::CLIENT->allowMxpInteriorFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Allow MXP to frames inside \'main\' windows set to ' . $string,
            );

        # ;mxp -c
        } elsif ($switch eq '-c') {

            $axmud::CLIENT->set_allowMxpFlag('crosslink', ! $axmud::CLIENT->allowMxpCrosslinkFlag);
            if (! $axmud::CLIENT->allowMxpCrosslinkFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Allow MXP to crosslink to new servers set to ' . $string,
            );

        # ;mxp -r
        } elsif ($switch eq '-r') {

            $axmud::CLIENT->set_allowMxpFlag('room', ! $axmud::CLIENT->allowMxpRoomFlag);
            if (! $axmud::CLIENT->allowMxpRoomFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Allow Locator task to rely on MXP room data set to ' . $string,
            );

        # ;mxp -k
        } elsif ($switch eq '-k') {

            $axmud::CLIENT->set_allowMxpFlag('flexible', ! $axmud::CLIENT->allowMxpFlexibleFlag);
            if (! $axmud::CLIENT->allowMxpFlexibleFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Allow MXP to use (some) illegal keywords set to ' . $string,
            );

        # ;mxp -p
        } elsif ($switch eq '-p') {

            $axmud::CLIENT->set_allowMxpFlag('perm', ! $axmud::CLIENT->allowMxpPermFlag);
            if (! $axmud::CLIENT->allowMxpPermFlag) {
                $string = 'OFF';
            } else {
                $string = 'ON';
            }

            return $self->complete(
                $session, $standardCmd,
                'Assume world has enabled MXP set to ' . $string,
            );

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid options (try \';help mxp\')',
            );
        }
    }
}

{ package Games::Axmud::Cmd::MSP;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('msp', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['msp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Manages the Mud Sound Protocol (MSP)';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $soundDir, $count, $switch, $testFlag, $listFlag, $dlFlag, $multFlag, $autoFlag,
            $flexFlag, $string, $path, $urlRegex, $tempDir, $targetDir, $errorCount, $fetchObj,
            $dlPath, $extractObj,
            @fileList,
            %extHash,
        );

        # Several parts of this function need the directory in which MSP sounds are stored for the
        #   current world
        $soundDir = $axmud::DATA_DIR . '/msp/' . $session->currentWorld->name . '/';

        # Extract switches
        $count = 0;

        ($switch, @args) = $self->extract('-t', 0, @args);
        if (defined $switch) {

            $testFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-l', 0, @args);
        if (defined $switch) {

            $listFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-d', 0, @args);
        if (defined $switch) {

            $dlFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-m', 0, @args);
        if (defined $switch) {

            $multFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-a', 0, @args);
        if (defined $switch) {

            $autoFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-f', 0, @args);
        if (defined $switch) {

            $flexFlag = TRUE;
            $count++;
        }

        # There should be 0 or 1 arguments left
        $string = shift @args;
        if (@args) {

            return $self->improper($session, $inputString);

        # Can't combine switches
        } elsif ($count > 1) {

           return $self->error(
                $session, $inputString,
                'The switches -t, -l, -d, -m, -a and -f can\'t be combined',
            );
        }

        # msp
        if (! $count && ! $string) {

            # Display header
            $session->writeText('Mud Sound Protocol (MSP)');

            # Display list
            $session->writeText('   Allow MSP in general');
            if ($axmud::CLIENT->useMspFlag) {
                $session->writeText('      yes');
            } else {
                $session->writeText('      no');
            }

            $session->writeText('   MSP mode for this session');

            if ($session->mspMode eq 'no_invite') {

                $session->writeText(
                    '      Server has not suggested MSP, but client is willing',
                );

            } elsif ($session->mspMode eq 'client_agree') {

                $session->writeText(
                    '      Server has suggested MSP, and client has agreed',
                );

            } elsif ($session->mspMode eq 'client_refuse') {

                $session->writeText(
                    '      Server has suggested MSP, and client has refused',
                );

            } elsif ($session->mspMode eq 'client_simulate') {

                $session->writeText(
                    '      Server has not suggested MSP, but client is responding to MSP',
                );
            }

            $session->writeText('   Allow multiple sound files to play concurrently');
            if ($axmud::CLIENT->allowMspMultipleFlag) {
                $session->writeText('      yes');
            } else {
                $session->writeText('      no');
            }

            $session->writeText(
                '   Allow ' . $axmud::SCRIPT . ' to automatically download new sound files',
            );

            if ($axmud::CLIENT->allowMspLoadSoundFlag) {
                $session->writeText('      yes');
            } else {
                $session->writeText('      no');
            }

            $session->writeText('   ' . $axmud::SCRIPT . ' supported audio formats');
            $session->writeText(
                '      ' . join(
                    ' ',
                    sort {lc($a) cmp lc($b)} ($axmud::CLIENT->ivKeys('constSoundFormatHash')),
                ),
            );

            $session->writeText('   Download MSP sound files into this directory (folder)');
            $session->writeText('      ' . $soundDir);

            $session->writeText('   MSP sound/music triggers playing');
            if (! $session->soundHarnessHash) {

                $session->writeText('      (none)');

            } else {

                $session->writeText('      Number   Type  File path');

                foreach my $soundObj (
                    sort {$a->number <=> $b->number} ($session->ivValues('soundHarnessHash'))
                ) {
                    $session->writeText(
                        sprintf('      %-8.8s %-5.5s', $soundObj->number, $soundObj->type)
                        . ' ' . $soundObj->path,
                    );
                }
            }

            $session->writeText(
                '   Flexible MSP tag placement (officially discouraged)',
            );

            if ($axmud::CLIENT->allowMspFlexibleFlag) {
                $session->writeText('      yes');
            } else {
                $session->writeText('      no');
            }

            # Display footer
            return $self->complete($session, $standardCmd, 'End of list');

        # ;msp on
        } elsif (! $count && $string eq 'on') {

            # (Enable pseudo-MSP recognition for this session, even if GA::Client->useMspFlag is
            #   FALSE, because some worlds can't negotiate MSP telnet options, but still send MSP
            #   sound/music tags
            if ($session->mspMode eq 'client_agree' || $session->mspMode eq 'client_simulate') {

                return $self->error(
                    $session, $inputString,
                    'MSP is already enabled for this session',
                );

            } else {

                if (! $session->setPseudoMSP(TRUE)) {

                    return $self->error(
                        $session, $inputString,
                        'Unable to enable MSP for this session',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'MSP enabled for this session (use \';setmudprotocol -y\' to enable'
                        . '/disable MSP generally)',
                    );
                }
            }

        # ;msp off
        } elsif (! $count && $string eq 'off') {

            # (Disable pseudo-MSP recognition for this session only)
            if ($session->mspMode eq 'no_invite' || $session->mspMode eq 'client_refuse') {

                return $self->error(
                    $session, $inputString,
                    'MSP is already disabled for this session',
                );

            } else {

                if (! $session->setPseudoMSP(FALSE)) {

                    return $self->error(
                        $session, $inputString,
                        'Unable to disable MSP for this session',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'MSP disabled for this session (use \';setmudprotocol -y\' to enable'
                        . '/disable MSP generally)',
                    );
                }
            }

        # ;msp -t <sound>
        } elsif ($testFlag) {

            if (! $string) {

                return $self->error(
                    $session, $inputString,
                    'Test which MSP sound?',
                );

            } else {

                # Process a fake MSP sound/music trigger
                $session->processMspSoundTrigger('!!SOUND(' . $string . ')');
                return $self->complete(
                    $session, $standardCmd,
                    'Testing MSP sound file \'' . $string . '\' (if nothing is audible, check'
                    . ' that sound is on, and that a matching file exists in ' . $axmud::SCRIPT
                    . '\'s MSP directory, .../' . $axmud::NAME_SHORT . '-data/msp/'
                    . $session->currentWorld->name . '/)',
                );
            }

        # ;msp -l
        } elsif ($listFlag) {

            # (Don't bother checking whether $string was specified, or not - just ignore it)
            if (! -e $soundDir) {

                return $self->complete($session, $standardCmd, 'No files found in ' . $soundDir);
            }

            # Get a list of files in the MSP directory for the current world, and its subdirectories
            File::Find::find(
                sub { push (@fileList, $File::Find::name); },
                $soundDir,
            );

            if (! @fileList) {

                return $self->complete($session, $standardCmd, 'No files found in ' . $soundDir);

            } else {

                # Display header
                $session->writeText('MSP sound files downloaded to ' . $soundDir);

                # Display list

                @fileList = sort {lc($a) cmp lc($b)} (@fileList);
                $count = 0;

                foreach my $file (@fileList) {

                    # Ignore directories
                    if (-f $file) {

                        $count++;
                        $file =~ s/$soundDir//;
                        $session->writeText('   ' . $file);
                    }
                }

                # Display footer
                if ($count == 1) {

                    return $self->complete($session, $standardCmd, 'End of list (1 file found)');

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'End of list (' . $count . ' files found)',
                    );
                }
            }

        # ;msp -d
        } elsif ($dlFlag) {

            # Initialise some variables
            $urlRegex = $axmud::CLIENT->constUrlRegex;
            $tempDir = $axmud::DATA_DIR . '/data/temp/msp-extract';
            $targetDir = $axmud::DATA_DIR . '/msp/' . $session->currentWorld->name . '/';
            %extHash = $axmud::CLIENT->constSoundFormatHash;
            $count = 0;
            $errorCount = 0;

            # If no URL was specified, prompt the user for one
            if (! defined $string) {

                $string = $session->mainWin->showEntryDialogue(
                    'Download MSP sound pack',
                    'Enter the link for the \'' . $session->currentWorld->longName
                    . '\' sound pack',
                );

                if (! defined $string) {

                    return $self->complete($session, $standardCmd, 'Download operation cancelled');
                }
            }

            # Check the URL is valid
            if (! ($string =~ m/$urlRegex/)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid download link \'' . $string . '\'',
                );
            }

            # Attempt to download the file
            $session->writeText('Downloading sound pack \'' . $string . '\'...');
            # It might be a long wait, so make sure the message is visible right away
            $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->do');

            $fetchObj = File::Fetch->new(uri => $string);
            $dlPath = $fetchObj->fetch(to => $axmud::DATA_DIR . '/data/temp');
            if (! $dlPath) {

                return $self->error(
                    $session, $inputString,
                    'Sound pack download failed; check the link and try again',
                );
            }

            # If it's an archive file, extract it
            if (
                $dlPath =~ m/\.tar$/
                || $dlPath =~ m/\.tgz$/
                || $dlPath =~ m/\.gz$/
                || $dlPath =~ m/\.zip$/
                || $dlPath =~ m/\.bz2$/
                || $dlPath =~ m/\.tbz$/
                || $dlPath =~ m/\.lzma$/
            ) {
                # Attempt to extract the file
                $session->writeText('Sound pack downloaded, extracting...');
                $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->do');

                # Build an Archive::Extract object
                $extractObj = Archive::Extract->new(archive => $dlPath);
                if (! $extractObj) {

                    return $self->error(
                        $session, $inputString,
                        'No files extracted (file decompression error)',
                    );
                }

                # Extract the archive
                if (! $extractObj->extract(to => $tempDir)) {

                    return $self->error(
                        $session, $inputString,
                        'No files extracted (file decompression error)',
                    );
                }

                # Get a list of paths, relative to $tempDir, of all the extracted files
                @fileList = @{$extractObj->files};  # e.g. export/tasks.axm
                OUTER: foreach my $file (@fileList) {

                    my $matchFlag;

                    # Convert all the paths into absolute paths
                    $file = $axmud::DATA_DIR . '/data/temp/msp-extract/' . $file;

                    # Any file that ends in a valid sound file extension (one of those specified by
                    #   GA::Client->constSoundFormatHash) should be copied into the current world's
                    #   MSP directory
                    INNER: foreach my $ext (keys %extHash) {

                        if ($file =~ m/\.$ext$/) {

                            $matchFlag = TRUE;
                            last INNER;
                        }
                    }

                    if ($matchFlag) {

                        File::Copy::move($file, $targetDir);
                        $count++;

                    } else {

                        # Invalid sound file
                        unlink $file;
                        $errorCount++;
                    }
                }

                if (! $count) {

                    return $self->error(
                        $session, $inputString,
                        'Didn\'t extract any valid sound files (invalid files: ' . $errorCount
                        . ')',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'Extraction complete (valid sound files: ' . $count
                        . ', invalid sound files: ' . $errorCount . ')',
                    );
                }

            # If it's a (single) valid sound file, use it
            } else {

                foreach my $ext (keys %extHash) {

                    if ($dlPath =~ m/\.$ext$/) {

                        File::Copy::move($dlPath, $targetDir);

                        return $self->complete(
                            $session, $standardCmd,
                            'Sound pack (consisting of one file) downloaded',
                        );
                    }
                }

                return $self->error(
                    $session, $inputString,
                    'The downloaded file isn\'t a valid archive (e.g. ending .zip) or a valid'
                    . ' sound file (e.g. ending .wav)',
                );
            }

        # ;msp -m
        } elsif ($multFlag) {

            $axmud::CLIENT->toggle_mspFlag('multiple');

            if (! $axmud::CLIENT->allowMspMultipleFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Playing multiple MSP sounds concurrently turned off',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Playing multiple MSP sounds concurrently turned on',
                );
            }

        # ;msp -a
        } elsif ($autoFlag) {

            $axmud::CLIENT->toggle_mspFlag('load');

            if (! $axmud::CLIENT->allowMspLoadSoundFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Permission to automatically download MSP sounds turned off',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Permission to automatically download MSP sounds turned on',
                );
            }

        # ;msp -f
        } elsif ($flexFlag) {

            $axmud::CLIENT->toggle_mspFlag('flexible');

            if (! $axmud::CLIENT->allowMspLoadSoundFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Flexible MSP tag placement turned off',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Flexible MSP tag placement turned on',
                );
            }

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid options (try \';help msp\')',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ZMP;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('zmp', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['zmp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows supported ZMP packages/commands';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $package,
            $check,
        ) = @_;

        # Local variables
        my (
            $dotPackage,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Compile an ordered list of matching ZMP packages
        if (! $package) {

            @list = sort {lc($a->packageName) cmp lc($b->packageName)}
                ($axmud::CLIENT->ivValues('zmpPackageHash'));

        } else {

            foreach my $obj ($axmud::CLIENT->ivValues('zmpPackageHash')) {

                if ($obj->packageName eq $package) {

                    push (@list, $obj);
                }
            }

            @list = sort {lc($a->packageName) cmp lc($b->packageName)} (@list);
        }

        if (! @list) {

            return $self->error($session, $inputString, 'No matching ZMP packages found');
        }

        # Display header
        $session->writeText('List of supported ZMP packages');

        # Display list
        foreach my $obj (@list) {

            $session->writeText('   ' . $obj->packageName);

            if ($obj->world) {
                $session->writeText('      World    : ' . $obj->world);
            } else {
                $session->writeText('      World    : <supported in all worlds>');
            }

            if ($obj->cmdHash) {

                $session->writeText(
                    '      Commands : '
                    . join(' / ', sort {lc($a) cmp lc($b)} ($obj->ivKeys('cmdHash'))),
                );

            } else {

                $session->writeText(
                    '      Commands : <none supported>',
                );
            }
        }

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 package found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @list . ' packages found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SendZMP;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('sendzmp', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['szmp', 'sendzmp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends a ZMP command to the world';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $cmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! defined $cmd) {

            return $self->improper($session, $inputString);
        }

        # Check ZMP is enabled in the current session
        if ($session->zmpMode ne 'client_agree') {

            return $self->error(
                $session, $inputString,
                'ZMP is not enabled in the current session',
            );
        }

        # Send the ZMP command and any specified parameters
        if (! $session->optSendZmp($cmd, @args)) {

            return $self->error(
                $session, $inputString,
                'Unabled to send ZMP command \'' . $cmd . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'ZMP command \'' . $cmd . '\' sent',
            );
        }
    }
}

{ package Games::Axmud::Cmd::InputZMP;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('inputzmp', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['izmp', 'inputzmp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends pre-formatted text via ZMP';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Check ZMP is enabled in the current session
        if ($session->zmpMode ne 'client_agree') {

            return $self->error(
                $session, $inputString,
                'ZMP is not enabled in the current session',
            );
        }

        # Send the ZMP command and any specified parameters
        if (! $session->optSendZmp('zmp.input', join("\n", @args))) {

            return $self->error(
                $session, $inputString,
                'Unabled to send ZMP command \'zmp.input\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'ZMP input command sent',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Aardwolf;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('aardwolf', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['aard', 'aard102', 'aardwolf'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Handles AARD102 (Aardwolf 102 channel) data';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $option,
            $setting,
            $check,
        ) = @_;

        # Local variables
        my ($diff, $flag);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;aard
        if (! defined $option) {

            # Display header
            $session->writeText('Current AARD102 (Aardwolf 102 channel) status');

            # Display list
            if ($session->aard102Mode eq 'no_invite') {

                $session->writeText('    AARD102 mode : Server has not suggested AARD102 yet');

            } elsif ($session->aard102Mode eq 'client_agree') {

                $session->writeText(
                    '    AARD102 mode : Server has suggested AARD102 and client has agreed',
                );

            } elsif ($session->aard102Mode eq 'client_refuse') {

                $session->writeText(
                    '    AARD102 mode : Server has suggested AARD102 and client has refused',
                );
            }

            if ($session->aard102Mode eq 'client_agree') {

                if (! defined $session->aard102Status) {
                    $session->writeText('    Status       : <not received>');
                } else {
                    $session->writeText('    Status       : ' . $session->aard102Status);
                }

                if (! defined $session->aard102TickTime) {

                    $session->writeText('    Tick time    : <not received>');

                } else {

                    $diff = int($session->sessionTime - $session->aard102TickTime) + 1;

                    if ($diff == 1) {
                        $session->writeText('    Tick time    : 1 second ago');
                    } else {
                        $session->writeText('    Tick time    : ' . $diff . ' seconds ago');
                    }
                }
            }

            # Display footer
            return $self->complete($session, $standardCmd, 'End of status list');

        # ;aard <option>
        # ;aard <option> <setting>
        } else {

            # Check AARD102 is enabled in the current session
            if ($session->aard102Mode ne 'client_agree') {

                return $self->error(
                    $session, $inputString,
                    'AARD102 is not enabled in the current session',
                );

            # <option> must be a value in the range 1-254
            } elsif (! $axmud::CLIENT->intCheck($option, 1, 254)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid AARD102 option \'' . $option . '\' - must be a value in the range'
                    . ' 1-254',
                );
            }

            # If <setting> was specified, convert it to TRUE or FALSE
            if (defined $setting) {

                if (lc($setting) eq 'on') {
                    $flag = TRUE;
                } elsif (lc($setting) eq 'off') {
                    $flag = FALSE;
                } else {

                    $flag = $self->convertTrueFalse($setting);
                    if (! defined $flag) {

                        # Emergency fallback to prevent yet more improper args messages
                        $flag = FALSE;
                    }
                }

            } else {

                # Default is 'turn on'
                $flag = TRUE;
            }

            # Apply the setting
            if (! $session->optSendAard102($option, $flag)) {

                return $self->error(
                    $session, $inputString,
                    'Could not apply the AARD102 setting \'' . $option . '\'',
                );

            } elsif (! $flag) {

                return $self->complete(
                    $session, $standardCmd,
                    'AARD102 option \'' . $option . '\' turned OFF',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'AARD102 option \'' . $option . '\' turned ON',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ATCP;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('atcp', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['atcp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows ATCP data reported by the current world';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $package,
            $check,
        ) = @_;

        # Local variables
        my (
            $dotPackage,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check ATCP is enabled in the current session
        if ($session->atcpMode ne 'client_agree') {

            return $self->error(
                $session, $inputString,
                'ATCP is not enabled in the current session',
            );
        }

        # Compile an ordered list of matching ATCP packages
        if (! $package) {

            if (! $session->atcpDataHash) {

                return $self->error(
                    $session, $inputString,
                    'No ATCP data has been reported by the world',
                );

            } else {

                @list = sort {lc($a->name) cmp lc($b->name)} ($session->ivValues('atcpDataHash'));
            }

        } else {

            $dotPackage = $package . '.';
            foreach my $obj (
                sort {lc($a->name) cmp lc($b->name)} ($session->ivValues('atcpDataHash'))
            ) {
                if ($obj->name eq $package || $obj->name =~ m/^$dotPackage/) {

                    push (@list, $obj);
                }
            }
        }

        if (! @list) {

            return $self->error($session, $inputString, 'No matching ATCP packages found');
        }

        # Display header
        $session->writeText('List of reported ATCP packages');

        # Display list
        foreach my $obj (@list) {

            $session->writeText('   ' . $obj->name);
            $session->writeText('      ' . $axmud::CLIENT->encodeJson($obj->data));
        }

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 package found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @list . ' packages found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SendATCP;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('sendatcp', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['satcp', 'sendatcp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends encoded JSON data to the world via ATCP';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($string, $name, $data);

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Check ATCP is enabled in the current session
        if ($session->atcpMode ne 'client_agree') {

            return $self->error(
                $session, $inputString,
                'ATCP is not enabled in the current session',
            );
        }

        # The ATCP packet expects a payload in the form 'Package[.SubPackages].Message <data>'
        # Split @args into a name and data component, if possible; otherwise submit the whole
        #   argument list as a single string
        $string = join(' ', @args);
        if ($string =~ m/^([[:alpha:]\_][[:word:]\-\.]*)\s(.*)/) {

            $name = lc($1);
            $data = $2;

        } else {

            $name = $string;
        }

        # Send the ATCP packet
        if (! $session->optSendAtcp($name, $data)) {

            return $self->error(
                $session, $inputString,
                'Unabled to send ATCP package \'' . $name . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'ATCP package \'' . $name . '\' sent',
            );
        }
    }
}

{ package Games::Axmud::Cmd::GMCP;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('gmcp', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['gmcp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows GMCP data reported by the current world';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $package,
            $check,
        ) = @_;

        # Local variables
        my (
            $dotPackage,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check GMCP is enabled in the current session
        if ($session->gmcpMode ne 'client_agree') {

            return $self->error(
                $session, $inputString,
                'GMCP is not enabled in the current session',
            );
        }

        # Compile an ordered list of matching GMCP packages
        if (! $package) {

            if (! $session->gmcpDataHash) {

                return $self->error(
                    $session, $inputString,
                    'No GMCP data has been reported by the world',
                );

            } else {

                @list = sort {lc($a->name) cmp lc($b->name)} ($session->ivValues('gmcpDataHash'));
            }

        } else {

            $dotPackage = $package . '.';
            foreach my $obj (
                sort {lc($a->name) cmp lc($b->name)} ($session->ivValues('gmcpDataHash'))
            ) {
                if (
                    $obj->name eq $package
                    || $obj->name =~ m/^$dotPackage/
                ) {
                    push (@list, $obj);
                }
            }
        }

        if (! @list) {

            return $self->error($session, $inputString, 'No matching GMCP packages found');
        }

        # Display header
        $session->writeText('List of reported GMCP packages');

        # Display list
        foreach my $obj (@list) {

            $session->writeText('   ' . $obj->name);
            $session->writeText('      ' . $axmud::CLIENT->encodeJson($obj->data));
        }

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 package found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @list . ' packages found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SendGMCP;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('sendgmcp', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sgmcp', 'sendgmcp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends encoded JSON data to the world via GMCP';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($string, $name, $data);

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Check GMCP is enabled in the current session
        if ($session->gmcpMode ne 'client_agree') {

            return $self->error(
                $session, $inputString,
                'GMCP is not enabled in the current session',
            );
        }

        # The GMCP packet expects a payload in the form 'Package[.SubPackages].Message <data>'
        # Split @args into a name and data component, if possible; otherwise submit the whole
        #   argument list as a single string
        $string = join(' ', @args);
        if ($string =~ m/^([[:alpha:]\_][[:word:]\-\.]*)\s(.*)/) {

            $name = lc($1);
            $data = $2;

        } else {

            $name = $string;
        }

        # Send the GMCP packet
        if (! $session->optSendGmcp($name, $data)) {

            return $self->error(
                $session, $inputString,
                'Unabled to send GMCP package \'' . $name . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'GMCP package \'' . $name . '\' sent',
            );
        }
    }
}

{ package Games::Axmud::Cmd::MNES;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('mnes', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mnes'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Manages the MUD NEW-ENVIRON Standard (MNES)';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # mnes
        if (! defined $switch) {

            # Display header
            $session->writeText('MUD NEW-ENVIRON Standard (MNES)');

            # Display list
            if ($axmud::CLIENT->useMnesFlag) {
                $session->writeText('   Allow MSP in general            - YES');
            } else {
                $session->writeText('   Allow MSP in general            - NO');
            }

            if ($axmud::CLIENT->allowMnesSendIPFlag) {
                $session->writeText('   Send real IP address to world   - YES');
            } else {
                $session->writeText('   Send real IP address to world   - NO');
            }

            # Display footer
            return $self->complete($session, $standardCmd, 'End of list');

        # ;mnes -s
        } elsif ($switch ne '-s') {

            return $self->error(
                $session, $inputString,
                'Invalid switch (try \'-s\' to toggle sending the user\'s IP address)',
            );

        } else {

            $axmud::CLIENT->toggle_mnesFlag('send_ip');
            if ($axmud::CLIENT->allowMnesSendIPFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'MNES will now send the user\'s IP address to the world',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'MNES will no longer send the user\'s IP address to the world',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::MCP;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('mcp', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mcp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows supported MCP packages';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Display header
        $session->writeText('List of supported MCP packages');

        # Display list
        @list = sort {lc($a->name) cmp lc($b->name)} ($axmud::CLIENT->ivValues('mcpPackageHash'));
        foreach my $obj (@list) {

            $session->writeText(
                '   ' . sprintf('%-64.64s', $obj->name) . ' v' . $obj->minVersion . ' - v'
                . $obj->maxVersion,
            );

            if ($obj->supplantList) {

                $session->writeText('      Supplants packages: ' . join(' ', $obj->supplantList));
            }
        }

        # Display header
        $session->writeText('List of MCP packages used in this session');

        # Display list
        if (! $session->mcpPackageHash) {

            $session->writeText('   (No MCP packages are in use)');

        } else {

            foreach my $obj (
                sort {lc($a->name) cmp lc($b->name)} ($session->ivValues('mcpPackageHash'))
            ) {
                my $version;

                if (defined $obj->useVersion) {
                    $version = 'v' . $obj->useVersion;
                } else {
                    $version = '<package disabled>';
                }

                $session->writeText('   ' . sprintf('%-64.64s', $obj->name) . ' ' . $version);
            }
        }

        # Display footer
        return $self->complete(
            $session, $standardCmd,
            'End of list (' . scalar @list . ' supported packages found)',
        );
    }
}

{ package Games::Axmud::Cmd::Log;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('log', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['log'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles logfile settings on/off';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my (
            $string, $msg,
            %hash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;log
        if (! $arg) {

            # Display header
            $session->writeText('Current logfile settings (logging is enabled)');

            # Display list
            $session->writeText('   Switch       Setting  Description');

            if ($axmud::CLIENT->allowLogsFlag) {
                $string = 'enabled';
            } else {
                $string = 'disabled';
            }

            $session->writeText(
                sprintf('   %-12.12s %-8.8s ', '-l', $string) . 'Logging in general',
            );

            if ($axmud::CLIENT->deleteStandardLogsFlag) {
                $string = 'on';
            } else {
                $string = 'off';
            }

            $session->writeText(
                sprintf('   %-12.12s %-8.8s ', '-d', $string) . 'Deletion of standard logfiles',
            );

            if ($axmud::CLIENT->deleteWorldLogsFlag) {
                $string = 'on';
            } else {
                $string = 'off';
            }

            $session->writeText(
                sprintf('   %-12.12s %-8.8s ', '-m', $string) . 'Deletion of world logfiles',
            );

            if ($axmud::CLIENT->logDayFlag) {
                $string = 'on';
            } else {
                $string = 'off';
            }

            $session->writeText(
                sprintf('   %-12.12s %-8.8s ', '-y', $string) . 'New logfiles every day',
            );

            if ($axmud::CLIENT->logClientFlag) {
                $string = 'on';
            } else {
                $string = 'off';
            }

            $session->writeText(
                sprintf('   %-12.12s %-8.8s ', '-s', $string) . 'New logfiles when client starts',
            );

            if ($axmud::CLIENT->logPrefixDateFlag) {
                $string = 'on';
            } else {
                $string = 'off';
            }

            $session->writeText(
                sprintf('   %-12.12s %-8.8s ', '-a', $string) . 'Lines prefixed with date',
            );

            if ($axmud::CLIENT->logPrefixTimeFlag) {
                $string = 'on';
            } else {
                $string = 'off';
            }

            $session->writeText(
                sprintf('   %-12.12s %-8.8s ', '-t', $string) . 'Lines prefixed with time',
            );

            if ($axmud::CLIENT->logImageFlag) {
                $string = 'on';
            } else {
                $string = 'off';
            }

            $session->writeText(
                sprintf('   %-12.12s %-8.8s ', '-t', $string) . 'Logfiles show image filenames',
            );

            $session->writeText(' ');
            $session->writeText('Client logfiles');
            $session->writeText('   Logfile      Setting');

            %hash = $axmud::CLIENT->logPrefHash;
            foreach my $logFile (sort {lc($a) cmp lc($b)} (keys %hash)) {

                if ($hash{$logFile}) {
                    $string = 'on';
                } else {
                    $string = 'off';
                }

                $session->writeText(sprintf('   %-12.12s %-3.3s', $logFile, $string));
            }

            $session->writeText(' ');
            $session->writeText('World (session) logfiles');
            $session->writeText('   Logfile      Setting');

            %hash = $session->currentWorld->logPrefHash;
            foreach my $logFile (sort {lc($a) cmp lc($b)} (keys %hash)) {

                if ($hash{$logFile}) {
                    $string = 'on';
                } else {
                    $string = 'off';
                }

                $session->writeText(sprintf('   %-12.12s %-3.3s', $logFile, $string));
            }

            # Display footer
            return $self->complete($session, $standardCmd, 'End of logging preferences');

        # ;log <switch>
        } elsif ($arg eq '-l') {

            $msg = 'Logging turned ';
            $axmud::CLIENT->toggle_logFlag('allow');

            if (! $axmud::CLIENT->allowLogsFlag) {
                return $self->complete($session, $standardCmd, $msg . 'off');
            } else {
                return $self->complete($session, $standardCmd, $msg . 'on');
            }

        } elsif ($arg eq '-d') {

            $msg = 'Deletion of standard logfiles turned ';
            $axmud::CLIENT->toggle_logFlag('del_standard');

            if (! $axmud::CLIENT->deleteStandardLogsFlag) {
                return $self->complete($session, $standardCmd, $msg . 'off');
            } else {
                return $self->complete($session, $standardCmd, $msg . 'on');
            }

        } elsif ($arg eq '-w') {

            $msg = 'Deletion of world (session) logfiles turned ';
            $axmud::CLIENT->toggle_logFlag('del_world');

            if (! $axmud::CLIENT->deleteWorldLogsFlag) {
                return $self->complete($session, $standardCmd, $msg . 'off');
            } else {
                return $self->complete($session, $standardCmd, $msg . 'on');
            }

        } elsif ($arg eq '-y') {

            $msg = 'Creation of new logfiles every day turned ';
            $axmud::CLIENT->toggle_logFlag('new_day');

            if (! $axmud::CLIENT->logDayFlag) {
                return $self->complete($session, $standardCmd, $msg . 'off');
            } else {
                return $self->complete($session, $standardCmd, $msg . 'on');
            }

        } elsif ($arg eq '-s') {

            $msg = 'Creation of new logfiles when client starts turned ';
            $axmud::CLIENT->toggle_logFlag('new_client');

            if (! $axmud::CLIENT->logClientFlag) {
                return $self->complete($session, $standardCmd, $msg . 'off');
            } else {
                return $self->complete($session, $standardCmd, $msg . 'on');
            }

        } elsif ($arg eq '-a') {

            $msg = 'Logfile lines prefixed with date turned ';
            $axmud::CLIENT->toggle_logFlag('prefix_date');

            if (! $axmud::CLIENT->logPrefixDateFlag) {
                return $self->complete($session, $standardCmd, $msg . 'off');
            } else {
                return $self->complete($session, $standardCmd, $msg . 'on');
            }

        } elsif ($arg eq '-t') {

            $msg = 'Logfile lines prefixed with time turned ';
            $axmud::CLIENT->toggle_logFlag('prefix_time');

            if (! $axmud::CLIENT->logPrefixTimeFlag) {
                return $self->complete($session, $standardCmd, $msg . 'off');
            } else {
                return $self->complete($session, $standardCmd, $msg . 'on');
            }

        } elsif ($arg eq '-i') {

            $msg = 'Logfiles show image filenames ';
            $axmud::CLIENT->toggle_logFlag('image');

            if (! $axmud::CLIENT->logImageFlag) {
                return $self->complete($session, $standardCmd, $msg . 'off');
            } else {
                return $self->complete($session, $standardCmd, $msg . 'on');
            }

        # ;log <logfile>
        } else {

            $msg = 'Logging to the file \'' . $arg . '\' turned ';

            if ($axmud::CLIENT->ivExists('logPrefHash', $arg)) {

                if ($axmud::CLIENT->ivShow('logPrefHash', $arg)) {

                    $axmud::CLIENT->set_logPref($arg, FALSE);
                    return $self->complete($session, $standardCmd, $msg . 'off');

                } else {

                    $axmud::CLIENT->set_logPref($arg, TRUE);
                    return $self->complete($session, $standardCmd, $msg . 'on');
                }

            } elsif ($session->currentWorld->ivExists('logPrefHash', $arg)) {

                if ($session->currentWorld->ivShow('logPrefHash', $arg)) {

                    $session->currentWorld->ivAdd('logPrefHash', $arg, FALSE);
                    return $self->complete($session, $standardCmd, $msg . 'off');

                } else {

                    $session->currentWorld->ivAdd('logPrefHash', $arg, TRUE);
                    return $self->complete($session, $standardCmd, $msg . 'on');
                }

            } else {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised logfile \'' . $arg . '\'',
                );
            }
        }
    }
}

# Sound and text-to-speech

{ package Games::Axmud::Cmd::Sound;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('sound', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['snd', 'sound'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Turns sound on/off';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;snd
        if (! defined $arg) {

            if ($axmud::CLIENT->allowSoundFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Sound effects are currently turned on',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Sound effects are currently turned off',
                );
            }

        # ;snd on
        } elsif ($arg eq 'on') {

            if ($axmud::CLIENT->allowSoundFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Sound effects are already turned on',
                );

            } elsif (! $axmud::CLIENT->audioCmd) {

                return $self->error(
                    $session, $inputString,
                    'Sound effects can\'t be turned on because no external audio player has been'
                    . ' set (with the \';setexternalprogramme\' command)',
                );

            } else {

                $axmud::CLIENT->set_allowSoundFlag(TRUE);

                return $self->complete($session, $standardCmd, 'Sound effects have been turned on');
            }

        # ;sound off
        } elsif ($arg eq 'off') {

            if (! $axmud::CLIENT->allowSoundFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Sound effects are already turned off',
                );

            } else {

                $axmud::CLIENT->set_allowSoundFlag(FALSE);

                return $self->complete(
                    $session, $standardCmd,
                    'Sound effects have been turned off',
                );
            }

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid setting - try \';sound on\' or \';sound off\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ASCIIBell;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('asciibell', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['asb', 'bell', 'asciibell'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Turns ASCII bells on/off';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;asb
        if (! defined $arg) {

            if ($axmud::CLIENT->allowAsciiBellFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'ASCII bells are currently turned on',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'ASCII bells are currently turned off',
                );
            }

        # ;asb on
        } elsif ($arg eq 'on') {

            if ($axmud::CLIENT->allowAsciiBellFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'ASCII bells are already turned on',
                );

            } else {

                $axmud::CLIENT->set_allowAsciiBellFlag(TRUE);

                return $self->complete($session, $standardCmd, 'ASCII bells have been turned on');
            }

        # asb off
        } elsif ($arg eq 'off') {

            if (! $axmud::CLIENT->allowAsciiBellFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'ASCII bells are already turned off',
                );

            } else {

                $axmud::CLIENT->set_allowAsciiBellFlag(FALSE);

                return $self->complete(
                    $session, $standardCmd,
                    'ASCII bells have been turned off',
                );
            }

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid setting - try \';bell on\' or \';bell off\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddSoundEffect;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addsoundeffect', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ase', 'addse', 'addsound', 'addsoundeffect'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a new sound effect';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($name, $switch, $oldFile, $path, $msg);

        # Extract the -d switch, if present
        ($switch, @args) = $self->extract('-d', 0, @args);
        # Only one argument should be left
        $name = shift @args;
        if (! defined $name || @args) {

            return $self->improper($session, $inputString);
        }

        # See if the sound effect exists, and if a file has been specified for it
        if (
            # Sound effect exists...
            $axmud::CLIENT->ivExists('customSoundHash', $name)
            # ...and a file is specified for it
            && $axmud::CLIENT->ivShow('customSoundHash', $name)
        ) {
            $oldFile = $axmud::CLIENT->ivShow('customSoundHash', $name);
        }

        # Check that <name> is valid
        if (! $axmud::CLIENT->nameCheck($name, 16)) {

            return $self->error(
                $session, $inputString,
                'Invalid name for sound effect \'' . $name . '\'',
            );
        }

        # ;ase <name>
        if (! $switch) {

            # Open a 'dialogue' window to select a file
            $path = $session->mainWin->showFileChooser(
                'Choose file',
                'open',
            );

            if (! $path) {

                return $self->complete($session, $standardCmd, 'Sound effect not added');
            }

        # ;ase <name> -d
        # ;ase -d <name>
        } else {

            $path = '';     # Empty file path disables the sound effect
        }

        # Add the sound effect
        $axmud::CLIENT->add_soundEffect($name, $path);
        if ($oldFile) {

            if ($switch) {
                $msg = 'Sound effect \'' . $name . '\' replaced (and disabled)',
            } else {
                $msg = 'Sound effect \'' . $name . '\' replaced with \'' . $path . '\'',
            }

        } else {

            if ($switch) {
                $msg = 'Sound effect \'' . $name . '\' added (but disabled)',
            } else {
                $msg = 'Sound effect \'' . $name . '\' added using \'' . $path . '\'',
            }
        }

        return $self->complete($session, $standardCmd, $msg);
    }
}

{ package Games::Axmud::Cmd::PlaySoundEffect;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('playsoundeffect', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['pse', 'playse', 'playsound', 'playsoundeffect'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Plays a sound effect and shows a system message';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If no sound effect was specified, chose a random one
        if (! defined $name) {

            @list = $axmud::CLIENT->ivKeys('customSoundHash');
            if (@list) {

                $name = $list[int(rand(scalar @list))];
            }
        }

        # Check the sound effect exists
        if (! $axmud::CLIENT->ivExists('customSoundHash', $name)) {

            return $self->error(
                $session, $inputString,
                '\'' . $name . '\' doesn\'t exist in the sound effects bank',
            );

        # Check that sound effects are allowed to be played
        } elsif (! $axmud::CLIENT->allowSoundFlag) {

            return $self->error(
                $session, $inputString,
                'Sound effects are turned off (try \';sound on\')',
            );

        } else {

            # Attempt to play the sound effect
            if (! $axmud::CLIENT->playSound($name)) {

                return $self->error(
                    $session, $inputString,
                    'Failed to play \'' . $name . '\' sound effect',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Sound effect \'' . $name . '\' played'
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::QuickSoundEffect;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('quicksoundeffect', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['qse', 'quickse', 'quicksound', 'quicksoundeffect'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Plays a sound effect without a system message';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($switch, $flashFlag, $effect);

        # Extract the optional switch
        ($switch, @args) = $self->extract('-f', 0, @args);
        if (defined $switch) {

            $flashFlag = TRUE;
        }

        # The sound effect name is also optional (nothing happens if it's not specified)
        $effect = shift @args;

        # There should be nothing left in @args
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # Flash the session's 'main' window, if specified
        if ($flashFlag) {

            $session->mainWin->setUrgent(TRUE);
        }

        # Play a sound effect, if one was specified
        if (defined $effect) {

            # Attempt to play the sound effect
            $axmud::CLIENT->playSound($effect);
        }

        # (No confirmation message with this client command)
        return 1;
    }
}

{ package Games::Axmud::Cmd::Beep;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('beep', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['beep'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Plays the \'beep\' sound effect';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Attempt to play the sound effect
        $axmud::CLIENT->playSound('beep');

        # (No confirmation message with this client command)
        return 1;
    }
}

{ package Games::Axmud::Cmd::DeleteSoundEffect;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletesoundeffect', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dse', 'delse', 'delsound', 'deletesoundeffect'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a sound effect';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my $count;

        # Check for improper arguments
        if (! defined $arg || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;dse -a
        if ($arg eq '-a') {

            if (! $axmud::CLIENT->customSoundHash) {

                return $self->error(
                    $session, $inputString,
                    'The bank of sound effects is already empty',
                );

            } else {

                # Delete all sound effects
                $count = $axmud::CLIENT->ivPairs('customSoundHash');
                $axmud::CLIENT->ivEmpty('customSoundHash');

                if ($count == 1) {

                    return $self->complete(
                        $session, $standardCmd,
                        '1 sound effect deleted from the sound effects bank');

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        $count . ' sound effects deleted from the sound effects bank',
                    );
                }
            }

        # ;dse <name>
        } else {

            if (! $axmud::CLIENT->ivExists('customSoundHash', $arg)) {

                return $self->error(
                    $session, $inputString,
                    '\'' . $arg . '\' not found in the sound effects bank');

            } else {

                # Delete the sound effect
                $axmud::CLIENT->del_soundEffect($arg);

                return $self->complete(
                    $session, $standardCmd,
                    '\'' . $arg . '\' deleted from the sound effects bank',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ResetSoundEffect;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resetsoundeffect', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rse', 'resetse', 'resetsound', 'resetsoundeffect'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets sound effects to defaults';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        $axmud::CLIENT->reset_customSoundHash();
        return $self->complete($session, $standardCmd, 'Sound effects bank reset');
    }
}

{ package Games::Axmud::Cmd::ListSoundEffect;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listsoundeffect', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lse', 'listse', 'listsound', 'listsoundeffect'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows the sound effects bank';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            @list,
            %constSoundHash, %customSoundHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the sound effects bank isn't empty
        if (! $axmud::CLIENT->customSoundHash) {

            return $self->complete($session, $standardCmd, 'The sound effects bank is empty');
        }

        # Import the sound effects banks
        %constSoundHash = $axmud::CLIENT->constStandardSoundHash;
        %customSoundHash = $axmud::CLIENT->customSoundHash;
        # Compile a list of sound effect names in in alphabetical order
        @list = sort {lc($a) cmp lc($b)} (keys %customSoundHash);
        if (! @list) {

            return $self->complete($session, $standardCmd, 'The sound effects list is empty');
        }

        # Display header
        if ($axmud::CLIENT->allowSoundFlag) {
            $session->writeText('List of sound effects (turned on) (* = standard effect)');
        } else {
            $session->writeText('List of sound effects (turned off) (* = standard effect)');
        }

        # Display list
        foreach my $effect (@list) {

            my $column;

            if (exists $constSoundHash{$effect}) {
                $column = ' * ';
            } else {
                $column = '   ';
            }

            if ($customSoundHash{$effect}) {

                $session->writeText(
                    $column . sprintf('%-16.16s', $effect) . ' ' . $customSoundHash{$effect},
                );

            } else {

                $session->writeText($column . sprintf('%-16.16s <no file specified>', $effect));
            }
        }

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 sound effect found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . @list . ' sound effects found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Speech;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('speech', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tts', 'speech'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies text-to-speech (TTS) general settings';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $string, $speed, $pitch, $choice, $port,
            @list,
        );

        # (For the benefit of visually-impaired users, don't check for improper arguments; ignore
        #   everything after the expected arguments)

        # ;tts
        if (! @args) {

            # Display header
            $session->writeText('Text-to-speech (TTS) settings');

            # Display list
            if ($axmud::CLIENT->customAllowTTSFlag) {
                $string = 'yes';
            } else {
                $string = 'no';
            }

            $session->writeText('   TTS enabled for all users:  ' . $string);

            if ($axmud::CLIENT->systemAllowTTSFlag) {
                $string = 'yes';
            } else {
                $string = 'no';
            }

            $session->writeText('   TTS enabled at the moment:  ' . $string);

            $session->writeText(
                '   Supported TTS engines:      ' . join(', ', $axmud::CLIENT->constTTSList),
            );

            $session->writeText(
                '   OS-compatible TTS engines:  ' . join(', ', $axmud::CLIENT->constTTSCompatList),
            );

            $session->writeText('   Available TTS configurations:');
            $session->writeText(
                '      '
                . join(', ', sort {lc($a) cmp lc($b)} ($axmud::CLIENT->ivKeys('ttsObjHash'))),
            );

            $session->writeText(
                '   Convert received text:      '
                . $self->convertFlag($axmud::CLIENT->ttsReceiveFlag),
            );

            $session->writeText(
                '   Don\'t convert pre-login:    '
                . $self->convertFlag($axmud::CLIENT->ttsLoginFlag),
            );

            $session->writeText(
                '   Don\'t convert prompts:      '
                . $self->convertFlag($axmud::CLIENT->ttsPromptFlag),
            );

            $session->writeText(
                '   Convert system messages:    '
                . $self->convertFlag($axmud::CLIENT->ttsSystemFlag),
            );

            $session->writeText(
                '   Convert system errors:      '
                . $self->convertFlag($axmud::CLIENT->ttsSystemErrorFlag),
            );

            $session->writeText(
                '   Convert world commands:     '
                . $self->convertFlag($axmud::CLIENT->ttsWorldCmdFlag),
            );

            $session->writeText(
                '   Convert \'dialogue\' windows: '
                . $self->convertFlag($axmud::CLIENT->ttsDialogueFlag),
            );

            $session->writeText(
                '   Convert (some) task text:   ' . $self->convertFlag($axmud::CLIENT->ttsTaskFlag),
            );

            # Display footer
            return $self->complete($session, $standardCmd, 'End of TTS settings');

        # ;tts on
        # ;tts -o
        } elsif ($args[0] eq 'on' || $args[0] eq '-o') {

            if (! $axmud::BLIND_MODE_FLAG) {

                if ($axmud::CLIENT->customAllowTTSFlag) {

                    return $self->complete(
                        $session, $standardCmd,
                        'Text-to-speech is already turned on',
                    );

                } else {

                    $axmud::CLIENT->set_customAllowTTSFlag(TRUE);

                    return $self->complete(
                        $session, $standardCmd,
                        'Text-to-speech has been turned on',
                    );
                }

            } else {

                if ($axmud::CLIENT->customAllowTTSFlag) {

                    return $self->complete(
                        $session, $standardCmd,
                        'Text-to-speech is already turned on for all users',
                    );

                } else {

                    $axmud::CLIENT->set_customAllowTTSFlag(TRUE);

                    return $self->complete(
                        $session, $standardCmd,
                        'Text-to-speech has been turned on for all users',
                    );
                }
            }

        # ;tts off
        # ;tts -f
        } elsif ($args[0] eq 'off' || $args[0] eq '-f') {

            if (! $axmud::BLIND_MODE_FLAG) {

                if (! $axmud::CLIENT->customAllowTTSFlag) {

                    return $self->complete(
                        $session, $standardCmd,
                        'Text-to-speech is already turned off',
                    );

                } else {

                    $axmud::CLIENT->set_customAllowTTSFlag(FALSE);

                    return $self->complete(
                        $session, $standardCmd,
                        'Text-to-speech has been turned off',
                    );
                }

            } else {

                if (! $axmud::CLIENT->customAllowTTSFlag) {

                    return $self->complete(
                        $session, $standardCmd,
                        'Text-to-speech for all users is already turned off, but text-to-speech'
                        . ' is still available because ' . $axmud::SCRIPT . ' is running in'
                        . ' \'blind\' mode',
                    );

                } else {

                    $axmud::CLIENT->set_customAllowTTSFlag(FALSE);

                    return $self->complete(
                        $session, $standardCmd,
                        'Text-to-speech for all users has been turned off, but text-to-speech'
                        . ' is still available because ' . $axmud::SCRIPT . ' is running in'
                        . ' \'blind\' mode',
                    );
                }
            }

        # ;tts toggle
        # ;tts -g
        } elsif ($args[0] eq 'toggle' || $args[0] eq '-g') {

            # (Used by the 'main' window's toolbar icon)

            if (! $axmud::BLIND_MODE_FLAG) {

                if (! $axmud::CLIENT->customAllowTTSFlag) {

                    $axmud::CLIENT->set_customAllowTTSFlag(TRUE);

                    return $self->complete(
                        $session, $standardCmd,
                        'Text-to-speech has been turned on',
                    );

                } else {

                    $axmud::CLIENT->set_customAllowTTSFlag(FALSE);

                    return $self->complete(
                        $session, $standardCmd,
                        'Text-to-speech has been turned off',
                    );
                }

            } else {

                if (! $axmud::CLIENT->customAllowTTSFlag) {

                    $axmud::CLIENT->set_customAllowTTSFlag(TRUE);

                    return $self->complete(
                        $session, $standardCmd,
                        'Text-to-speech has been turned on for all users',
                    );

                } else {

                    $axmud::CLIENT->set_customAllowTTSFlag(FALSE);

                    return $self->complete(
                        $session, $standardCmd,
                        'Text-to-speech for all users has been turned off, but text-to-speech'
                        . ' is still available because ' . $axmud::SCRIPT . ' is running in'
                        . ' \'blind\' mode',
                    );
                }
            }

        # ;tts receive/login/prompt/system/error/command/dialogue/task/smooth
        # ;tts receive/login/prompt/system/error/command/dialogue/task/smooth
        # ;tts -r/-l/-x/-s/-e/-c/-d/-t/-m on
        # ;tts -r/-l/-x/-s/-e/-c/-d/-t/-m off
        } elsif (
            $args[0] eq 'receive' || $args[0] eq '-r'
            || $args[0] eq 'login' || $args[0] eq '-l'
            || $args[0] eq 'prompt' || $args[0] eq '-x'
            || $args[0] eq 'system' || $args[0] eq '-s'
            || $args[0] eq 'error' || $args[0] eq '-e'
            || $args[0] eq 'command' || $args[0] eq 'cmd' || $args[0] eq '-c'
            || $args[0] eq 'dialogue' || $args[0] eq '-d'
            || $args[0] eq 'task' || $args[0] eq '-t'
            || $args[0] eq 'smooth' || $args[0] eq '-m'
            || $args[0] eq 'auto' || $args[0] eq '-a'
        ) {
            if (! $args[1]) {

                return $self->error(
                    $session, $inputString,
                    'Turn which text-to-speech setting on/off?',
                );

            } elsif (
                $args[1] ne 'on' && $args[1] ne '-o'
                && $args[1] ne 'off' && $args[1] ne '-f'
            ) {
                return $self->error(
                    $session, $inputString,
                    'Format: \';speech ' . $args[0] . ' on / off\'',
                );
            }

            if ($args[0] eq 'receive' || $args[0] eq '-m') {

                $string = 'received text';

                if ($args[1] eq 'on' || $args[1] eq '-o') {
                    $axmud::CLIENT->set_ttsFlag('receive', TRUE);
                } else {
                    $axmud::CLIENT->set_ttsFlag('receive', FALSE);
                }

            } elsif ($args[0] eq 'login' || $args[0] eq '-l') {

                $string = 'optimised login';

                if ($args[1] eq 'on' || $args[1] eq '-o') {
                    $axmud::CLIENT->set_ttsFlag('login', TRUE);
                } else {
                    $axmud::CLIENT->set_ttsFlag('login', FALSE);
                }

            } elsif ($args[0] eq 'prompt' || $args[0] eq '-x') {

                $string = 'recognised prompts';

                if ($args[1] eq 'on' || $args[1] eq '-o') {
                    $axmud::CLIENT->set_ttsFlag('prompt', TRUE);
                } else {
                    $axmud::CLIENT->set_ttsFlag('prompt', FALSE);
                }

            } elsif ($args[0] eq 'system' || $args[0] eq '-y') {

                $string = 'system messages';

                if ($args[1] eq 'on' || $args[1] eq '-o') {
                    $axmud::CLIENT->set_ttsFlag('system', TRUE);
                } else {
                    $axmud::CLIENT->set_ttsFlag('system', FALSE);
                }

            } elsif ($args[0] eq 'error' || $args[0] eq '-z') {

                $string = 'system error messages';

                if ($args[1] eq 'on' || $args[1] eq '-o') {
                    $axmud::CLIENT->set_ttsFlag('error', TRUE);
                } else {
                    $axmud::CLIENT->set_ttsFlag('error', FALSE);
                }

            } elsif ($args[0] eq 'command' || $args[0] eq 'cmd' || $args[0] eq '-c') {

                $string = 'world commands';

                if ($args[1] eq 'on' || $args[1] eq '-o') {
                    $axmud::CLIENT->set_ttsFlag('command', TRUE);
                } else {
                    $axmud::CLIENT->set_ttsFlag('command', FALSE);
                }

            } elsif ($args[0] eq 'dialogue' || $args[0] eq '-d') {

                $string = '\'dialogue\' windows';

                if ($args[1] eq 'on' || $args[1] eq '-o') {
                    $axmud::CLIENT->set_ttsFlag('dialogue', TRUE);
                } else {
                    $axmud::CLIENT->set_ttsFlag('dialogue', FALSE);
                }

            } elsif ($args[0] eq 'task' || $args[0] eq '-t') {

                $string = '(some) task text';

                if ($args[1] eq 'on' || $args[1] eq '-o') {
                    $axmud::CLIENT->set_ttsFlag('task', TRUE);
                } else {
                    $axmud::CLIENT->set_ttsFlag('task', FALSE);
                }

            } elsif ($args[0] eq 'smooth' || $args[0] eq '-h') {

                $string = 'smoothing';

                if ($args[1] eq 'on' || $args[1] eq '-o') {
                    $axmud::CLIENT->set_ttsFlag('smooth', TRUE);
                } else {
                    $axmud::CLIENT->set_ttsFlag('smooth', FALSE);
                }
            }

            if ($args[1] eq 'on' || $args[1] eq '-o') {

                return $self->complete(
                    $session, $standardCmd,
                    'Conversion of ' . $string . ' to speech turned on',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Conversion of ' . $string . ' to speech turned off',
                );
            }

        # ;tts port <port>
        # ;tts port default
        # ;tts port none
        } elsif ($args[0] eq 'port' || $args[0] eq '-p') {

            $port = $args[1];

            if (! defined $port) {

                return $self->error(
                    $session, $inputString,
                    'Invalid Festival server port (must be in the range 0 to 65535, or use'
                    . ' the words \'default\' or \'none\')',
                );

            } elsif ($port eq 'default') {

                $axmud::CLIENT->set_ttsFestivalServerPort(
                    $axmud::CLIENT->constTtsFestivalServerPort
                );

                return $self->complete(
                    $session, $standardCmd,
                    'Festival server port set to default value of \''
                    . $axmud::CLIENT->constTtsFestivalServerPort . '\'',
                );

            } elsif ($port eq 'none') {

                $axmud::CLIENT->set_ttsFestivalServerPort(undef);

                return $self->complete(
                    $session, $standardCmd,
                    'Festival server disabled (' . $axmud::SCRIPT . ' will use the command-line'
                    . ' server instead)',
                );

            } elsif (! $axmud::CLIENT->intCheck($port, 0, 65535)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid Festival server port (must be in the range 0 to 65535, or use'
                    . ' the words \'default\' or \'none\')',
                );

            } else {

                $axmud::CLIENT->set_ttsFestivalServerPort($port);

                return $self->complete(
                    $session, $standardCmd,
                    'Festival server port set to \'' . $axmud::CLIENT->ttsFestivalServerPort . '\'',
                );
            }

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid setting - try \';speech on\' or \';speech off\'',
            );
        }
    }

    sub convertFlag {

        # Converts a TRUE/FALSE flag into an 'on/off' string, and returns the string
        #
        # Expected arguments
        #   $flag   - The flag to convert
        #
        # Return values
        #   'undef' on improper arguments
        #   The string 'on' or 'off' otherwise

        my ($self, $flag, $check) = @_;

        # Check for improper arguments
        if (! defined $flag || defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->convertFlag', @_);
        }

        if (! $flag) {
            return 'off';
        } else {
            return 'on';
        }
    }
}

{ package Games::Axmud::Cmd::Speak;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('speak', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['spk', 'speak'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Uses a text-to-speech engine to read out a message';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $configFlag, $configuration, $engineFlag, $engine, $voiceFlag, $voice,
            $speedFlag, $speed, $rateFlag, $rate, $pitchFlag, $pitch, $volumeFlag, $volume, $text,
        );

        # ;speak <config>
        if (@args == 1 && $axmud::CLIENT->ivExists('ttsObjHash', $args[0])) {

            # Use a sample sentence
            $text = 'Hello, my name is ' . $axmud::SCRIPT . ' and I am testing the configuration'
                        . ' called \''. $args[0] . '\'.';

            # Convert the text to speech
            if (! $axmud::CLIENT->tts($text, 'other', $args[0], $session)) {

                return $self->error(
                    $session, $inputString,
                    'Unable to test the configuration \'' . $args[0] .'\'',
                );

            } elsif (! $axmud::CLIENT->systemAllowTTSFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Attempted to read out a test message for the configuration \'' . $args[0]
                    . '\'',
                );

            } else {

                # Show no confirmation message, so the user doesn't hear the text twice
                return 1;
            }
        }

        # Otherwise, extract switches
        ($switch, $configuration, @args) = $self->extract('-n', 1, @args);
        if (defined $switch) {

            $configFlag = TRUE;
        }

        ($switch, $engine, @args) = $self->extract('-e', 1, @args);
        if (defined $switch) {

            $engineFlag = TRUE;
        }

        ($switch, $voice, @args) = $self->extract('-v', 1, @args);
        if (defined $switch) {

            $voiceFlag = TRUE;
        }

        ($switch, $speed, @args) = $self->extract('-s', 1, @args);
        if (defined $switch) {

            $speedFlag = TRUE;
        }

        ($switch, $rate, @args) = $self->extract('-r', 1, @args);
        if (defined $switch) {

            $rateFlag = TRUE;
        }

        ($switch, $pitch, @args) = $self->extract('-p', 1, @args);
        if (defined $switch) {

            $pitchFlag = TRUE;
        }

        ($switch, $volume, @args) = $self->extract('-l', 1, @args);
        if (defined $switch) {

            $volumeFlag = TRUE;
        }

        # Anything left in @args is the text to convert
        if (! @args) {

            # Use a sample sentence
            $text = 'Hello, my name is ' . $axmud::SCRIPT . ' and I am your mud client.';

        } else {

            # For convenience, combine any remaining arguments into a single string
            $text = join(' ', @args);
        }

        # Check the validity of any supplied options
        if ($configFlag && ! $axmud::CLIENT->ivExists('ttsObjHash', $configuration)) {

            return $self->error(
                $session, $inputString,
                'Unrecognised text-to-speech configuration \'' . $configuration . '\'',
            );

        } elsif ($engineFlag && ! defined $axmud::CLIENT->ivFind('constTTSList', $engine)) {

            return $self->error(
                $session, $inputString,
                'Unsupported text-to-speech engine \'' . $engine . '\'',
            );
        }

        # Convert the text to speech
        if (
            ! $axmud::CLIENT->ttsAddUrgentJob(
                $text,
                $configuration,
                FALSE,          # Don't empty the urgent job list
                $engine,
                $voice,
                $speed,
                $rate,
                $pitch,
                $volume,
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Unable to read out \'' . $text . '\'',
            );

        } elsif (! $axmud::CLIENT->systemAllowTTSFlag) {

            return $self->complete(
                $session, $standardCmd,
                'Attempted to read out \'' . $text . '\'',
            );

        } else {

            # Show no confirmation message, so the user doesn't hear the text twice
            return 1;
        }
    }
}

{ package Games::Axmud::Cmd::Split;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('split', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['spl', 'split'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Split lines, sentences or words in TTS';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my $msg;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (! $arg) {

            if ($axmud::CLIENT->ttsJobMode eq 'default') {
                $axmud::CLIENT->set_ttsJobMode('sentence');
            } elsif ($axmud::CLIENT->ttsJobMode eq 'sentence') {
                $axmud::CLIENT->set_ttsJobMode('word');
            } elsif ($axmud::CLIENT->ttsJobMode eq 'word') {
                $axmud::CLIENT->set_ttsJobMode('default');
            }

        } elsif ($arg eq 'line') {
            $axmud::CLIENT->set_ttsJobMode('default');
        } elsif ($arg eq 'sentence' || $arg eq 'word') {
            $axmud::CLIENT->set_ttsJobMode($arg);
        } else {

            return $self->error(
                $session, $inputString,
                'Split text how? (Try using \'line\', \'sentence\' or \'word\', or just'
                . ' use the \';split\' command on its own to switch between them',
            );
        }

        if ($axmud::CLIENT->ttsJobMode eq 'sentence') {
            $msg = 'Sentence mode';
        } elsif ($axmud::CLIENT->ttsJobMode eq 'word') {
            $msg = 'Word mode';
        } else {
            $msg = 'Line mode';
        }

        if (! $axmud::CLIENT->systemAllowTTSFlag) {

            return $self->complete(
                $session, $standardCmd,
                'Text-to-speech switching to ' . lc($msg),
            );

        } else {

            # Show no further confirmation message, so that this command can be used as a macro
            #   without disrupting the user experience
            # However, do attempt to read aloud a confirmation. The call to GA::Client->ttsAddJob
            #   produces a job which is performed immediately, before any other jobs
            $axmud::CLIENT->ttsAddUrgentJob($msg);

            return 1;
        }
    }
}

{ package Games::Axmud::Cmd::Skip;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('skip', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['skp', 'skip'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Reads aloud the next piece of text';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # If a TTS is reading out something, tell it to stop. If there is more text waiting to be
        #   read aloud, then read it
        if (defined $args[0]) {

            # Exception: if a number of jobs (or the word 'more') is specified, then resume
            #   performing jobs, one line at a time, but skip that many jobs
            if ($args[0] eq 'more') {
                $axmud::CLIENT->ttsSkipJob(10);
            } elsif ($axmud::CLIENT->intCheck($args[0], 1)) {
                $axmud::CLIENT->ttsSkipJob($args[0]);
            } else {
                $axmud::CLIENT->ttsSkipJob();
            }

        } else {

            $axmud::CLIENT->ttsSkipJob();
        }

        # This command shows no confirmation message, so that it can be used as a macro without
        #   disrupting the user experience
        return 1;
    }
}

{ package Games::Axmud::Cmd::Unskip;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('unskip', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['usk', 'unskip'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Reads aloud the previous piece of text';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # If a TTS is reading out something, tell it to stop. If we're at the beginning of the list,
        #   re-read the first thing in the list. Otherwise, re-read the previous thing in the list.
        # In all cases, stop automatically reading things in the list (until the skips back to the
        #   end of the list)
        if (defined $args[0]) {

            # Exception: if a number of jobs (or the word 'more') is specified, then resume
            #   performing jobs, one line at a time, but unskip that many jobs
            if ($args[0] eq 'more') {
                $axmud::CLIENT->ttsUnskipJob(10);
            } elsif ($axmud::CLIENT->intCheck($args[0], 1)) {
                $axmud::CLIENT->ttsUnskipJob($args[0]);
            } else {
                $axmud::CLIENT->ttsUnskipJob();
            }

        } else {

            $axmud::CLIENT->ttsUnskipJob();
        }

        # This command shows no confirmation message, so that it can be used as a macro without
        #   disrupting the user experience
        return 1;
    }
}

{ package Games::Axmud::Cmd::First;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('first', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['fst', 'first'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Reads aloud the first piece of text';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # If a TTS is reading out something, tell it to stop. Then read out the first text remaining
        #   in memory
        $axmud::CLIENT->ttsFirstJob();

        # This command shows no confirmation message, so that it can be used as a macro without
        #   disrupting the user experience
        return 1;
    }
}

{ package Games::Axmud::Cmd::Last;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('last', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['last'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Reads aloud the last piece of text';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # If a TTS is reading out something, tell it to stop. Then read out the last text remaining
        #   in memory
        $axmud::CLIENT->ttsLastJob();

        # This command shows no confirmation message, so that it can be used as a macro without
        #   disrupting the user experience
        return 1;
    }
}

{ package Games::Axmud::Cmd::Resume;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resume', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['resume'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Reads aloud the first unread line';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # If a TTS is reading out something, tell it to stop
        # Then, switch to reading out whole lines, and perform the first job that has never been
        #   performed (the first line that has never been read out)
        # If already reading lines continuously, move to the end of the job list
        $axmud::CLIENT->ttsResumeJob();

        # This command shows no confirmation message, so that it can be used as a macro without
        #   disrupting the user experience
        return 1;
    }
}

{ package Games::Axmud::Cmd::Shutup;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('shutup', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sup', 'stfu', 'shutup'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Skips all text waiting to be read out';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # If a TTS is reading out something, tell it to stop. No more text is read aloud until the
        #   user types a command like ;skip, ;unskip or ;resume
        $axmud::CLIENT->ttsSilenceJob();

        # This command shows no confirmation message, so that it can be used as a macro without
        #   disrupting the user experience
        return 1;
    }
}

{ package Games::Axmud::Cmd::Prompt;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('prompt', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['pro', 'prompt'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Replays the most recent prompt ignored by TTS';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Use an urgent job, so that the natural flow of TTS jobs is not affected
        if (! defined $axmud::CLIENT->ttsLastPrompt) {

            $axmud::CLIENT->ttsAddUrgentJob('No prompt has been ignored', 'system');

        } else {

            $axmud::CLIENT->ttsAddUrgentJob($axmud::CLIENT->ttsLastPrompt, 'receive');
        }
    }
}

{ package Games::Axmud::Cmd::FreeKeys;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('freekeys', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['frk', 'free', 'freekeys'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles cursor (etc) keys for replaying TTS';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Toggle the flag
        if (! $axmud::BLIND_MODE_FLAG) {

            return $self->error(
                $session, $inputString,
                'This setting can only be changed in blind mode (try \';hijackkeys\' instead)',
            );

        } elsif (! $axmud::CLIENT->ttsHijackFlag) {

            $axmud::CLIENT->set_ttsHijackFlag(TRUE);

            return $self->complete(
                $session, $standardCmd,
                'Use cursor (etc) keys to replay text-to-speech turned ON',
            );

        } else {

            $axmud::CLIENT->set_ttsHijackFlag(FALSE);

            return $self->complete(
                $session, $standardCmd,
                'Use cursor (etc) keys to replay text-to-speech turned OFF',
            );
        }
    }
}

{ package Games::Axmud::Cmd::HijackKeys;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('hijackkeys', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['hjk', 'hijack', 'hijackkeys'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles cursor (etc) keys for replaying TTS';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Toggle the flag
        if ($axmud::BLIND_MODE_FLAG) {

            return $self->error(
                $session, $inputString,
                'This setting can not be changed in blind mode (try \';freekeys\' instead)',
            );

        } elsif (! $axmud::CLIENT->customAllowTTSFlag) {

            return $self->error(
                $session, $inputString,
                'Text to speech is not enabled (try \';speech on\' instead)',
            );

        } elsif (! $axmud::CLIENT->ttsForceHijackFlag) {

            $axmud::CLIENT->set_ttsForceHijackFlag(TRUE);

            return $self->complete(
                $session, $standardCmd,
                'Use cursor (etc) keys to replay text-to-speech turned ON',
            );

        } else {

            $axmud::CLIENT->set_ttsForceHijackFlag(FALSE);

            return $self->complete(
                $session, $standardCmd,
                'Use cursor (etc) keys to replay text-to-speech turned OFF',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Read;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('read', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rd', 'read'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Tells a task to read something aloud';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $attrib, $item, $value, $taskName, $taskObj,
            @taskList,
        );

        # (No improper arguments to check)

        if (! @args) {

            return $self->complete(
                $session, $standardCmd,
                'Available text-to-speech attributes: '
                . $self->sortAttributes('ttsAttribHash'),
            );
        }

        # Otherwise, the command is in the form ';read <attribute> <value>'. <attribute> is usually
        #   a single word like 'health', but occasionally something like 'healthup'. For the benefit
        #   of visually-impaired users, it's possible to type that as 'health up' (or, indeed, any
        #   combination of letters and spaces, e.g. 'hea lth up')
        # Work our way through @args, finding the longest possible TTS <attribute> that actually
        #   exists
        # (NB TTS attributes are case-insensitive)
        $attrib = '';
        do {

            $item = shift @args;

            if (! $axmud::CLIENT->ivExists('ttsAttribHash', lc($attrib . $item))) {

                # Only set the optional <value> if this is the last argument
                if ($attrib && ! @args) {
                    $value = $item;
                } else {
                    $attrib .= lc($item);
                }

            } else {

                $attrib .= lc($item);
            }

        } until (! @args);

        # Get the task that uses this attribute
        $taskName = $axmud::CLIENT->ivShow('ttsAttribHash', $attrib);
        if (! $taskName) {

            # (This message should never be seen)
            return $self->error(
                $session, $inputString,
                'Unrecognised text-to-speech attribute \'' . $attrib . '\'',
            );
        }

        # Find the matching task from the current tasklist
        @taskList = $self->findTask($session, $taskName);
        if (! @taskList) {

            return $self->error(
                $session, $inputString,
                'The text-to-speech attribute requires a \'' . $taskName . '\' task, but this task'
                . ' is not currently running',
            );

        } else {

            # In the unlikely event of there being two copies of a task which uses TTS attributes,
            #   direct the request to just the first one found
            $taskObj = $taskList[0];
        }

        # Check that the task recognises this attribute
        if (! $taskObj->ivExists('ttsAttribHash', $attrib)) {

            return $self->error(
                $session, $inputString,
                'The \'' . $taskObj->prettyName . '\' task doesn\'t seem to know about attributes'
                . ' called \'' . $attrib . '\'',
            );
        }

        # Pass the attribute to the task; it's up to the task to decide whether to attempt to read
        #   something aloud (in which case, it returns 1) or not (returns 'undef')
        # In either case, we don't use the usual system message generated by $self->complete and/or
        #   $self->error, since the visually-impaired user probably doesn't want to hear it
        return $taskObj->ttsReadAttrib($attrib, $value);
    }
}

{ package Games::Axmud::Cmd::PermRead;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('permread', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['prd', 'permread'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Tells an initial task to read something aloud';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $attrib, $item, $value, $taskName, $firstTaskObj, $recogniseFlag, $count, $errorCount,
            @taskList, @permTaskList, @activeList, @passiveList,
        );

        # (No improper arguments to check)

        if (! @args) {

            return $self->complete(
                $session, $standardCmd,
                'Available text-to-speech attributes: '
                . $self->sortAttributes('ttsAttribHash'),
            );
        }

        # Otherwise, the command is in the form ';read <attribute> <value>'. <attribute> is usually
        #   a single word like 'health', but occasionally something like 'healthup'. For the benefit
        #   of visually-impaired users, it's possible to type that as 'health up' (or, indeed, any
        #   combination of letters and spaces, e.g. 'hea lth up')
        # Work our way through @args, finding the longest possible TTS <attribute> that actually
        #   exists
        # (NB TTS attributes are case-insensitive)
        $attrib = '';
        do {

            $item = shift @args;

            if (! $axmud::CLIENT->ivExists('ttsAttribHash', lc($attrib . $item))) {

                # Only set the optional <value> if this is the last argument
                if ($attrib && ! @args) {
                    $value = $item;
                } else {
                    $attrib .= lc($item);
                }

            } else {

                $attrib .= lc($item);
            }

        } until (! @args);

        # Work out which kind of task uses this attribute
        $taskName = $axmud::CLIENT->ivShow('ttsAttribHash', $attrib);
        if (! $taskName) {

            # (This message should never be seen)
            return $self->error(
                $session, $inputString,
                'Unrecognised text-to-speech attribute \'' . $attrib . '\'',
            );
        }

        # Get all tasks of this kind from the current tasklist...
        push (@taskList, $self->findTask($session, $taskName));
        # ...and also from the global initial tasklist
        push (@permTaskList, $self->findGlobalInitialTask($taskName));

        if (! @taskList && ! @permTaskList) {

            return $self->error(
                $session, $inputString,
                'The text-to-speech attribute requires a \'' . $taskName . '\' task, but this task'
                . ' was not found in the current tasklist or the global initial tasklist',
            );
        }

        # Only the first task in @taskList (if any) is actually told to read something; all other
        #   tasks in @taskList and @permTaskList merely have their ->ttsAttribHash updated
        push (@activeList, shift @taskList);
        @passiveList = (@taskList, @permTaskList);

        # Check that at least one task (in either list) recognises this attribute
        OUTER: foreach my $taskObj (@activeList, @passiveList) {

            if (! defined $firstTaskObj) {

                # (We need at least one task object just below, any one will do)
                $firstTaskObj = $taskObj;
            }

            if ($taskObj->ivExists('ttsAttribHash', $attrib)) {

                $recogniseFlag = TRUE;
                last OUTER;
            }
        }

        if (! $recogniseFlag) {

            return $self->error(
                $session, $inputString,
                'The \'' . $firstTaskObj->prettyName . '\' task doesn\'t seem to know about'
                . ' attributes called \'' . $attrib . '\'',
            );
        }

        # Pass the attribute to the task(s)
        $count = 0;
        $errorCount = 0;
        foreach my $taskObj (@activeList) {

            # Pass the attribute to the task; it's up to the task to decide whether to attempt to
            #   read something aloud (in which case, it returns 1) or not (returns 'undef')
            if (! $taskObj->ttsReadAttrib($attrib, $value)) {
                $errorCount++;
            } else {
                $count++;
            }
        }

        foreach my $taskObj (@passiveList) {

            # Pass the attribute to the task; the TRUE flag means 'don't read out anything, just
            #   update the task's ->ttsAttribHash)
            if (! $taskObj->ttsReadAttrib($attrib, $value, TRUE)) {
                $errorCount++;
            } else {
                $count++;
            }
        }

        # If we found at least one task from the current tasklist, don't display a confirmation;
        #   otherwise, nothing has been read out, and we need to display a confirmation
        if (@activeList) {

            return 1;

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Modified the text-to-speech attribute. (Found tasks: ' . ($count + $errorCount)
                . ', errors: ' . $errorCount . ').',
            )
        }
    }
}

{ package Games::Axmud::Cmd::Switch;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('switch', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['swi', 'switch'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Tells a task to automatically read something aloud';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $flagAttrib, $item, $taskName, $taskObj, $msg,
            @taskList,
        );

        # (No improper arguments to check)

        if (! @args) {

            return $self->complete(
                $session, $standardCmd,
                'Available text-to-speech flag attributes: '
                . $self->sortAttributes('ttsFlagAttribHash'),
            );
        }

        # Otherwise, the command is in the form ';switch <flag_attribute>'. <flag_attribute> is
        #   usually a single word like 'health', but occasionally something like 'healthup'. For the
        #   benefit of visually-impaired users, it's possible to type that as 'health up' (or,
        #   indeed, any combination of letters and spaces, e.g. 'hea lth up')
        # (NB TTS attributes are case-insensitive)
        $flagAttrib = lc(join('', @args));

        # Get the task that uses this flag attribute
        $taskName = $axmud::CLIENT->ivShow('ttsFlagAttribHash', $flagAttrib);
        if (! $taskName) {

            return $self->error(
                $session, $inputString,
                'Unrecognised text-to-speech flag attribute \'' . $flagAttrib . '\'',
            );
        }

        # Find the matching task from the current tasklist
        @taskList = $self->findTask($session, $taskName);
        if (! @taskList) {

            return $self->error(
                $session, $inputString,
                'The text-to-speech flag attribute requires a \'' . $taskName . '\' task, but this'
                . ' task is not currently running',
            );

        } else {

            # In the unlikely event of there being two copies of a task which uses flag attributes,
            #   direct the request to just the first one found
            $taskObj = $taskList[0];
        }

        # Check that the task recognises this flag attribute
        if (! $taskObj->ivExists('ttsFlagAttribHash', $flagAttrib)) {

            return $self->error(
                $session, $inputString,
                'The \'' . $taskObj->prettyName . '\' task doesn\'t seem to know about flag'
                . ' attributes called \'' . $flagAttrib . '\'',
            );
        }

        # Pass the flag attribute to the task
        $msg = $taskObj->ttsSwitchFlagAttrib($flagAttrib);
        if (! $msg) {

            return $self->error(
                $session, $inputString,
                'General error switching the flag attribute \'' . $flagAttrib . '\'',
            );

        } else {

            return $self->complete($session, $standardCmd, $msg);
        }
    }
}

{ package Games::Axmud::Cmd::PermSwitch;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('permswitch', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['pswi', 'permswitch'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Tells an initial task to automatically read aloud';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $flagAttrib, $item, $taskName, $firstTaskObj, $recogniseFlag, $count, $errorCount,
            @taskList, @permTaskList,
        );

        # (No improper arguments to check)

        if (! @args) {

            return $self->complete(
                $session, $standardCmd,
                'Available text-to-speech flag attributes: '
                . $self->sortAttributes('ttsFlagAttribHash'),
            );
        }

        # Otherwise, the command is in the form ';switch <flag_attribute>'. <flag_attribute> is
        #   usually a single word like 'health', but occasionally something like 'healthup'. For the
        #   benefit of visually-impaired users, it's possible to type that as 'health up' (or,
        #   indeed, any combination of letters and spaces, e.g. 'hea lth up')
        # (NB TTS attributes are case-insensitive)
        $flagAttrib = lc(join('', @args));

        # Work out which kind of task uses this flag attribute
        $taskName = $axmud::CLIENT->ivShow('ttsFlagAttribHash', $flagAttrib);
        if (! $taskName) {

            # (This message should never be seen)
            return $self->error(
                $session, $inputString,
                'Unrecognised text-to-speech flag attribute \'' . $flagAttrib . '\'',
            );
        }

        # Get all tasks of this kind from the current tasklist...
        push (@taskList, $self->findTask($session, $taskName));
        # ...and also from the global initial tasklist
        push (@permTaskList, $self->findGlobalInitialTask($taskName));

        if (! @taskList && ! @permTaskList) {

            return $self->error(
                $session, $inputString,
                'The text-to-speech flag attribute requires a \'' . $taskName . '\' task, but this'
                . ' task was not found in the current tasklist or the global initial tasklist',
            );
        }

        # Check that at least one task (in either list) recognises this flag attribute
        OUTER: foreach my $taskObj (@taskList, @permTaskList) {

            if (! defined $firstTaskObj) {

                # (We need at least one task object just below, any one will do)
                $firstTaskObj = $taskObj;
            }

            if ($taskObj->ivExists('ttsFlagAttribHash', $flagAttrib)) {

                $recogniseFlag = TRUE;
                last OUTER;
            }
        }

        if (! $recogniseFlag) {

            return $self->error(
                $session, $inputString,
                'The \'' . $firstTaskObj->prettyName . '\' task doesn\'t seem to know about'
                . ' flag attributes called \'' . $flagAttrib . '\'',
            );
        }

        # Pass the flag attribute to the task(s)
        $count = 0;
        $errorCount = 0;
        foreach my $taskObj (@taskList) {

            # Pass the flag attribute to the task; it's up to the task to decide whether to attempt
            #   to switch the flag attribute or not
            my $msg = $taskObj->ttsSwitchFlagAttrib($flagAttrib);

            if (! $msg) {

                $errorCount++;

            } else {

                $count++;
                $session->writeText('Current tasklist: ' . $msg);
            }
        }

        foreach my $taskObj (@permTaskList) {

            # Pass the flag attribute to the task; the TRUE flag means 'don't switch anything, just
            #   update the task's ->ttsFlagAttribHash)
            my $msg = $taskObj->ttsSwitchFlagAttrib($flagAttrib, TRUE);

            if (! $msg) {

                $errorCount++;

            } else {

                $count++;
                $session->writeText('Global initial tasklist: ' . $msg);
            }
        }

        # Display a confirmation
        return $self->complete(
            $session, $standardCmd,
            'Modified the text-to-speech flag attribute. (Found tasks: ' . ($count + $errorCount)
            . ', errors: ' . $errorCount . ').',
        )
    }
}

{ package Games::Axmud::Cmd::Alert;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('alert', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['alt', 'alert'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Tells a task to automatically read aloud alerts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $alertAttrib, $item, $value, $taskName, $taskObj, $msg,
            @taskList,
        );

        # (No improper arguments to check)

        if (! @args) {

            return $self->complete(
                $session, $standardCmd,
                'Available text-to-speech alert attributes: '
                . $self->sortAttributes('ttsAlertAttribHash'),
            );
        }

        # Command is in the form ';alert <alert_attribute> <value>'. <alert_attribute> is usually a
        #   single word like 'health', but occasionally something like 'healthup'. For the benefit
        #   of visually-impaired users, it's possible to type that as 'health up' (or, indeed, any
        #   combination of letters and spaces, e.g. 'hea lth up')
        # Work our way through @args, finding the longest possible TTS <alert_attribute> that
        #   actually exists
        # (NB TTS attributes are case-insensitive)
        $alertAttrib = '';
        do {

            $item = shift @args;

            if (! $axmud::CLIENT->ivExists('ttsAlertAttribHash', lc($alertAttrib . $item))) {

                # Only set the optional <value> if this is the last argument
                if ($alertAttrib && ! @args) {
                    $value = $item;
                } else {
                    $alertAttrib .= lc($item);
                }

            } else {

                $alertAttrib .= lc($item);
            }

        } until (! @args);

        # Get the task that uses this alert attribute
        $taskName = $axmud::CLIENT->ivShow('ttsAlertAttribHash', $alertAttrib);
        if (! $taskName) {

            # (This message should never be seen)
            return $self->error(
                $session, $inputString,
                'Unrecognised text-to-speech alert attribute \'' . $alertAttrib . '\'',
            );
        }

        # Find the matching task from the current tasklist
        @taskList = $self->findTask($session, $taskName);
        if (! @taskList) {

            return $self->error(
                $session, $inputString,
                'The text-to-speech alert attribute requires a \'' . $taskName . '\' task, but this'
                . ' task is not currently running',
            );

        } else {

            # In the unlikely event of there being two copies of a task which uses alert attributes,
            #   direct the request to just the first one found
            $taskObj = $taskList[0];
        }

        # Check that the task recognises this alert attribute
        if (! $taskObj->ivExists('ttsAlertAttribHash', $alertAttrib)) {

            return $self->error(
                $session, $inputString,
                'The \'' . $taskObj->prettyName . '\' task doesn\'t seem to know about alert'
                . ' attributes called \'' . $alertAttrib . '\'',
            );
        }

        # Pass the alert attribute to the task
        $msg = $taskObj->ttsSetAlertAttrib($alertAttrib, $value);
        if (! $msg) {

            return $self->error(
                $session, $inputString,
                'General error setting the alert attribute \'' . $alertAttrib . '\'',
            );

        } else {

            return $self->complete($session, $standardCmd, $msg);
        }
    }
}

{ package Games::Axmud::Cmd::PermAlert;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('permalert', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['palt', 'permalert'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Tells an initial task to automatically read alerts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $alertAttrib, $item, $value, $taskName, $firstTaskObj, $recogniseFlag, $count,
            $errorCount,
            @taskList, @permTaskList,
        );

        # (No improper arguments to check)

        if (! @args) {

            return $self->complete(
                $session, $standardCmd,
                'Available text-to-speech alert attributes: '
                . $self->sortAttributes('ttsAlertAttribHash'),
            );
        }

        # Command is in the form ';alert <alert_attribute> <value>'. <alert_attribute> is usually a
        #   single word like 'health', but occasionally something like 'healthup'. For the benefit
        #   of visually-impaired users, it's possible to type that as 'health up' (or, indeed, any
        #   combination of letters and spaces, e.g. 'hea lth up')
        # Work our way through @args, finding the longest possible TTS <alert_attribute> that
        #   actually exists
        # (NB TTS attributes are case-insensitive)
        $alertAttrib = '';
        do {

            $item = shift @args;

            if (! $axmud::CLIENT->ivExists('ttsAlertAttribHash', lc($alertAttrib . $item))) {

                # Only set the optional <value> if this is the last argument
                if ($alertAttrib && ! @args) {
                    $value = $item;
                } else {
                    $alertAttrib .= lc($item);
                }

            } else {

                $alertAttrib .= lc($item);
            }

        } until (! @args);

        # Work out which kind of task uses this alert attribute
        $taskName = $axmud::CLIENT->ivShow('ttsAlertAttribHash', $alertAttrib);
        if (! $taskName) {

            # (This message should never be seen)
            return $self->error(
                $session, $inputString,
                'Unrecognised text-to-speech alert attribute \'' . $alertAttrib . '\'',
            );
        }

        # Get all tasks of this kind from the current tasklist...
        push (@taskList, $self->findTask($session, $taskName));
        # ...and also from the global initial tasklist
        push (@permTaskList, $self->findGlobalInitialTask($taskName));

        if (! @taskList && ! @permTaskList) {

            return $self->error(
                $session, $inputString,
                'The text-to-speech alert attribute requires a \'' . $taskName . '\' task, but this'
                . ' task was not found in the current tasklist or the global initial tasklist',
            );
        }

        # Check that at least one task (in either list) recognises this alert attribute
        OUTER: foreach my $taskObj (@taskList, @permTaskList) {

            if (! defined $firstTaskObj) {

                # (We need at least one task object just below, any one will do)
                $firstTaskObj = $taskObj;
            }

            if ($taskObj->ivExists('ttsAlertAttribHash', $alertAttrib)) {

                $recogniseFlag = TRUE;
                last OUTER;
            }
        }

        if (! $recogniseFlag) {

            return $self->error(
                $session, $inputString,
                'The \'' . $firstTaskObj->prettyName . '\' task doesn\'t seem to know about'
                . ' alert attributes called \'' . $alertAttrib . '\'',
            );
        }

        # Pass the alert attribute to the task(s)
        $count = 0;
        $errorCount = 0;
        foreach my $taskObj (@taskList) {

            # Pass the alert attribute to the task; it's up to the task to decide whether to attempt
            #   to set an alert (in which case, it returns 1) or not (returns 'undef')
            my $msg = $taskObj->ttsSetAlertAttrib($alertAttrib, $value);

            if (! $msg) {

                $errorCount++;

            } else {

                $count++;
                $session->writeText('Current tasklist: ' . $msg);
            }
        }

        foreach my $taskObj (@permTaskList) {

            # Pass the alert attribute to the task; the TRUE flag means 'don't set an alert just
            #   update the task's ->ttsAlertAttribHash)
            my $msg = $taskObj->ttsSetAlertAttrib($alertAttrib, $value, TRUE);

            if (! $msg) {

                $errorCount++;

            } else {

                $count++;
                $session->writeText('Global initial tasklist: ' . $msg);
            }
        }

        # Display a confirmation
        return $self->complete(
            $session, $standardCmd,
            'Modified the text-to-speech alert attribute. (Found tasks: ' . ($count + $errorCount)
            . ', errors: ' . $errorCount . ').',
        )
    }
}

{ package Games::Axmud::Cmd::ListAttribute;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listattribute', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lat', 'listattrib', 'listattribute'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists text-to-speech attributes';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # (Three lists to display - attributes, flag attributes and alert attributes)

        # Display header
        $session->writeText('Text-to-speech attributes (* - built-in task)');
        $session->writeText('   Attribute        Task');

        # Display lists
        foreach my $attrib (sort {lc($a) cmp lc($b)} ($axmud::CLIENT->ivKeys('ttsAttribHash'))) {

            my ($task, $column);

            $task = $axmud::CLIENT->ivShow('ttsAttribHash', $attrib);

            if (
                # It's in the default list of attributes...
                $axmud::CLIENT->ivExists('constTtsAttribHash', $attrib)
                # ...and still pointing at the original task
                && $task eq $axmud::CLIENT->ivShow('constTtsAttribHash', $attrib)
            ) {
                $column = ' * ';
            } else {
                $column = '   ';
            }

            $session->writeText($column . sprintf('%-16.16s %-16.16s', $attrib, $task));
        }

        # Display header
        $session->writeText('Text-to-speech flag attributes (* - built-in task)');
        $session->writeText('   Flag attribute   Task');

        # Display lists
        foreach my $attrib (
            sort {lc($a) cmp lc($b)} ($axmud::CLIENT->ivKeys('ttsFlagAttribHash'))
        ) {
            my ($task, $column);

            $task = $axmud::CLIENT->ivShow('ttsFlagAttribHash', $attrib);

            if (
                # It's in the default list of attributes...
                $axmud::CLIENT->ivExists('constTtsFlagAttribHash', $attrib)
                # ...and still pointing at the original task
                && $task eq $axmud::CLIENT->ivShow('constTtsFlagAttribHash', $attrib)
            ) {
                $column = ' * ';
            } else {
                $column = '   ';
            }

            $session->writeText($column . sprintf('%-16.16s %-16.16s', $attrib, $task));
        }

        # Display header
        $session->writeText('Text-to-speech alert attributes (* - built-in task)');
        $session->writeText('   Alert attribute  Task');

        # Display lists
        foreach my $attrib (
            sort {lc($a) cmp lc($b)} ($axmud::CLIENT->ivKeys('ttsAlertAttribHash'))
        ) {
            my ($task, $column);

            $task = $axmud::CLIENT->ivShow('ttsAlertAttribHash', $attrib);

            if (
                # It's in the default list of attributes...
                $axmud::CLIENT->ivExists('constTtsAlertAttribHash', $attrib)
                # ...and still pointing at the original task
                && $task eq $axmud::CLIENT->ivShow('constTtsAlertAttribHash', $attrib)
            ) {
                $column = ' * ';
            } else {
                $column = '   ';
            }

            $session->writeText($column . sprintf('%-16.16s %-16.16s', $attrib, $task));
        }

        # Display footer
        return $self->complete($session, $standardCmd, 'End of attribute lists');
    }
}

{ package Games::Axmud::Cmd::AddConfig;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addconfig', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['acf', 'addcf', 'addconfig'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a new text-to-speech configuration';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $configuration, $engine,
            $check,
        ) = @_;

        # Local variables
        my $ttsObj;

        # For the benefit of visually-impaired users, don't check for improper arguments (ignore
        #   anything after <name> and <engine>)
        if (! $configuration) {

            return $self->error(
                $session, $inputString,
                'Add which text-to-speech configuration? (Try \'addconfig <name>\')',
            );
        }

        # Check the configuration doesn't already exist
        if ($axmud::CLIENT->ivExists('ttsObjHash', $configuration)) {

            return $self->error(
                $session, $inputString,
                'There is already a text-to-speech configuration called \'' . $configuration . '\'',
            );

        # Again for visually-impaired user benefit, check the name is valid (reserved names are
        #   allowed) before creating the new configuration object
        } elsif (! ($configuration =~ m/^[[:alpha:]\_]{1}[[:word:]]{0,15}$/)) {

            return $self->error(
                $session, $inputString,
                '\'' . $configuration . '\' is an invalid text-to-speech configuration name'
                . ' (maximum 16 alphanumeric characters)',
            );
        }

        # If <engine> was specified, check it's valid
        if ($engine) {

            $engine = lc($engine);

            if (! defined $axmud::CLIENT->ivFind('constTTSList', $engine)) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised text-to-speech engine: try \'espeak\', \'esng\', \'flite\','
                    . ' \'festival\', \'swift\', \'none\' (or don\'t specify an engine at all)',
                );
            }

        } else {

            # Default engine
            $engine = 'espeak';
        }

        # Create the TTS configuration object
        $ttsObj = Games::Axmud::Obj::Tts->new($configuration, $engine);
        if (! $ttsObj) {

            return $self->error(
                $session, $inputString,
                'General error creating the text-to-speech configuration \'' . $configuration
                . '\'',
            );

        } else {

            # Add the object to the registry
            $axmud::CLIENT->add_ttsObj($ttsObj);

            return $self->complete(
                $session, $standardCmd,
                'Added text-to-speech configuration \'' . $configuration . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::CloneConfig;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('cloneconfig', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ccf', 'clonecf', 'cloneconfig'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Clones an existing text-to-speech configuration';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $original, $copy,
            $check,
        ) = @_;

        # Local variables
        my ($originalObj, $copyObj);

        # For the benefit of visually-impaired users, don't check for improper arguments (ignore
        #   anything after <original> and <copy>)
        if (! $original || ! $copy) {

            return $self->error(
                $session, $inputString,
                'Clone which text-to-speech configuration? (Try \'cloneconfig <original> <copy>\')',
            );
        }

        # Check that <original> exists, and <copy> doesn't
        if (! $axmud::CLIENT->ivExists('ttsObjHash', $original)) {

            return $self->error(
                $session, $inputString,
                'The text-to-speech configuration \'' . $original . '\' doesn\'t exist',
            );

        } elsif ($axmud::CLIENT->ivExists('ttsObjHash', $copy)) {

            return $self->error(
                $session, $inputString,
                'The text-to-speech configuration \'' . $copy . '\' already exists',
            );

        } else {

            $originalObj = $axmud::CLIENT->ivShow('ttsObjHash', $original);
        }

        # Again for visually-impaired user benefit, check for reserved words (etc) before creating
        #   the cloned configuration object
        if (! $axmud::CLIENT->nameCheck($copy, 16)) {

            return $self->error(
                $session, $inputString,
                '\'' . $copy . '\' is an invalid text-to-speech configuration name (maximum 16'
                . ' alphanumeric characters)',
            );
        }

        # Create the TTS configuration object
        $copyObj = $originalObj->clone($copy);
        if (! $copyObj) {

            return $self->error(
                $session, $inputString,
                'Could not clone the text-to-speech configuration \'' . $original . '\'',
            );

        } else {

            # Add the object to the registry
            $axmud::CLIENT->add_ttsObj($copyObj);

            return $self->complete(
                $session, $standardCmd,
                'Cloned the text-to-speech configuration \'' . $original . '\' into one named \''
                . $copy . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditConfig;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editconfig', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ecf', 'editcf', 'editconfig'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Edits a text-to-speech configuration';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $configuration,
            $check,
        ) = @_;

        # Local variables
        my $ttsObj;

        # For the benefit of visually-impaired users, don't check for improper arguments (ignore
        #   anything after <configuration>)
        if (! $configuration) {

            return $self->error(
                $session, $inputString,
                'Edit which text-to-speech configuration? (Try \'editconfig <original> <name>\')',
            );
        }

        # Check that configuration exists
        if (! $axmud::CLIENT->ivExists('ttsObjHash', $configuration)) {

            return $self->error(
                $session, $inputString,
                'The text-to-speech configuration \'' . $configuration . '\' doesn\'t exist',
            );

        } else {

            $ttsObj = $axmud::CLIENT->ivShow('ttsObjHash', $configuration);
        }

        # Open an 'edit' window for the configuration
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::TTS',
                $session->mainWin,
                $session,
                'Edit text-to-speech configuration \'' . $configuration . '\'',
                $ttsObj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the \'' . $configuration . '\' text-to-speech configuration',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the \'' . $configuration . '\' text-to-speech'
                . ' configuration',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ModifyConfig;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('modifyconfig', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mcf', 'config', 'modconfig', 'modifyconfig'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies text-to-speech configurations';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $configuration,
            @args,
        ) = @_;

        # Local variables
        my ($ttsObj, $count, $otherObj, $var, $var2, $string, $choice);

        # (For the benefit of visually-impaired users, don't check improper arguments and ignore
        #   everything after the expected arguments)

        # There is one command format which doesn't match the others, so we'll deal with it first:
        # ;mcf all engine <string>
        # ;mcf all -e <string>
        if ($configuration && $configuration eq 'all') {

            if (! $args[0] || ! $args[1] || $args[0] ne 'engine' && $args[0] ne '-e') {

                return $self->error(
                    $session, $inputString,
                    'You must specify an engine, for example \';engine espeak\'',
                );

            } elsif (! defined $axmud::CLIENT->ivFind('constTTSList', $args[1])) {

                return $self->error(
                    $session, $inputString,
                    'You must specify one of ' . $axmud::SCRIPT . '\'s recognised speech engines: '
                    . join(', ', $axmud::CLIENT->constTTSList),
                );

            } else {

                $ttsObj = $axmud::CLIENT->ivShow('ttsObjHash', $args[1]);
            }

            $count = 0;
            foreach my $thisObj ($axmud::CLIENT->ivValues('ttsObjHash')) {

                # The configurations that have the same name as a recognised speech engine can't
                #   be modified. For all the other configurations, update its engine
                if (! defined $axmud::CLIENT->ivFind('constTTSList', $thisObj->name)) {

                    $count++;
                    $thisObj->ivPoke('engine', $ttsObj->engine);
                    $thisObj->ivPoke('voice', $ttsObj->voice);
                    $thisObj->ivPoke('speed', $ttsObj->speed);
                    $thisObj->ivPoke('rate', $ttsObj->rate);
                    $thisObj->ivPoke('pitch', $ttsObj->pitch);
                    $thisObj->ivPoke('volume', $ttsObj->volume);
                }
            }

            if ($count == 1) {

                return $self->complete(
                    $session, $standardCmd,
                       'Updated 1 text-to-speech configuration to use the speech engine \''
                       . $ttsObj->engine . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                       'Updated ' . $count . ' text-to-speech configuration to use the speech'
                       . ' engine \'' . $ttsObj->engine . '\'',
                );
            }
        }

        # Now deal with commands in all other formats

        # Check that the configuration is valid, if specified
        if ($configuration) {

            if (! $axmud::CLIENT->ivExists('ttsObjHash', $configuration)) {

                return $self->error(
                    $session, $inputString,
                    'The text-to-speech configuration \'' . $configuration . '\' is not'
                    . ' recognised (for a quick list of configurations, try using this command'
                    . ' with no arguments)',
                );

            } else {

                $ttsObj = $axmud::CLIENT->ivShow('ttsObjHash', $configuration);
            }
        }

        # Many arguments need to be converted to lower case
        if (defined $args[0]) {

            $var = lc($args[0]);
        }

        if (defined $args[1]) {

            $var2 = lc($args[1]);
        }

        # ;mcf
        if (! $configuration) {

            return $self->complete(
                $session, $standardCmd,
                   'Available text-to-speech configurations are: '
                   . join ('  ', sort {lc($a) cmp lc($b)} ($axmud::CLIENT->ivKeys('ttsObjHash'))),
            );

        # ;mcf <config>
        } elsif (! @args) {

            # Display header
            $session->writeText(
                'Settings for \'' . $configuration . '\' text-to-speech configuration',
            );

            # Display list
            if ($ttsObj->engine) {
                $string = $ttsObj->engine;
            } else {
                $string = '<not set>';
            }

            $session->writeText('   TTS engine:             ' . $string);

            if ($ttsObj->voice) {
                $string = $ttsObj->voice;
            } else {
                $string = '<not set>';
            }

            $session->writeText('   Voice:                  ' . $string);

            if (defined $ttsObj->speed) {
                $string = $ttsObj->speed;
            } else {
                $string = '<not set>';
            }

            $session->writeText('   Word speed:             ' . $string);

            if (defined $ttsObj->rate) {
                $string = $ttsObj->rate;
            } else {
                $string = '<not set>';
            }

            $session->writeText('   Word rate:              ' . $string);

            if (defined $ttsObj->pitch) {
                $string = $ttsObj->pitch;
            } else {
                $string = '<not set>';
            }

            $session->writeText('   Word pitch:             ' . $string);

            if (defined $ttsObj->volume) {
                $string = $ttsObj->volume;
            } else {
                $string = '<not set>';
            }

            $session->writeText('   Word volume:            ' . $string);

            if ($ttsObj->exclusiveList) {

                $session->writeText('   Exclusive patterns:     <none>');

            } else {

                foreach my $pattern ($ttsObj->exclusiveList) {

                    $session->writeText('      ' . $pattern);
                }
            }

            if ($ttsObj->excludedList) {

                $session->writeText('   Excluded patterns:      <none>');

            } else {

                foreach my $pattern ($ttsObj->excludedList) {

                    $session->writeText('      ' . $pattern);
                }
            }

            # Display footer
            return $self->complete($session, $standardCmd, 'End of configuration settings');

        } elsif ($axmud::CLIENT->ivExists('constTtsFixedObjHash', $configuration)) {

            return $self->error(
                $session, $inputString,
                'The text-to-speech configuration \'' . $configuration . '\' can\'t be modified',
            );
        }

        # ;mcf <config> engine <string>
        # ;mcf <config> -e <string>
        if ($var eq 'engine' || $var eq '-e') {

            if (! $var2 || ! defined $axmud::CLIENT->ivFind('constTTSList', $var2)) {

                return $self->error(
                    $session, $inputString,
                    'Set which text-to-speech engine? (Try \'espeak\', \'esng\', \'flite\','
                    . ' \'festival\', \'swift\' or \'none\')',
                );
            }

            # Find the TTS configuration object with the same name, and use its settings, so that
            #   changing the engine also changes the voice, speed, rate, pitch and volume to
            #   default values (but don't modify exclusive/excluded patterns)
            $otherObj = $axmud::CLIENT->ivShow('ttsObjHash', $var2);
            if (! $otherObj) {

                # Better to be safe than sorry
                return $self->error(
                    $session, $inputString,
                    'General error, no text-to-speech configurations modified',
                );
            }

            $ttsObj->ivPoke('engine', $otherObj->engine);
            $ttsObj->ivPoke('voice', $otherObj->voice);
            $ttsObj->ivPoke('speed', $otherObj->speed);
            $ttsObj->ivPoke('rate', $otherObj->rate);
            $ttsObj->ivPoke('pitch', $otherObj->pitch);
            $ttsObj->ivPoke('volume', $otherObj->volume);

            return $self->complete(
                $session, $standardCmd,
                'Text-to-speech configuration \'' . $configuration . '\': engine set to \'' . $var2
                . '\'',
            );

        # ;mcf <config> voice <string>
        # ;mcf <config> -v <string>
        } elsif ($var eq 'voice' || $var eq '-v') {

            if (! $var2) {

                $ttsObj->ivUndef('voice');

                return $self->complete(
                    $session, $standardCmd,
                    'Text-to-speech configuration \'' . $configuration . '\': voice reset',
                );

            } else {

                $ttsObj->ivPoke('voice', $var2);

                return $self->complete(
                    $session, $standardCmd,
                    'Text-to-speech configuration \'' . $configuration . '\': voice set to \''
                    . $var2 . '\'',
                );
            }

        # ;mcf <config> speed <num>
        # ;mcf <config> -s <num>
        } elsif ($var eq 'speed' || $var eq '-s') {

            if (! $var2) {

                $ttsObj->ivUndef('speed');

                return $self->complete(
                    $session, $standardCmd,
                    'Text-to-speech configuration \'' . $configuration . '\': word speed reset',
                );

            } elsif (! $ttsObj->engine eq 'espeak') {

                return $self->error(
                    $session, $inputString,
                    $axmud::SCRIPT  . ' can only modify the word speed of the eSpeak engine',
                );
            }

            # Check <num> is a valid value
            if (! $axmud::CLIENT->intCheck($var2, 0, 100)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid text-to-speech word speed \'' . $var2 . '\' (must be in the range'
                    . ' 0-100)'
                );

            } else {

                # Set the speed
                $ttsObj->ivPoke('speed', $var2);

                return $self->complete(
                    $session, $standardCmd,
                    'Text-to-speech configuration \'' . $configuration . '\': word speed set to \''
                    . $var2 . '\'',
                );
            }

        # ;mcf <config> rate <num>
        # ;mcf <config> -r <num>
        } elsif ($var eq 'rate' || $var eq '-r') {

            if (! $var2) {

                $ttsObj->ivUndef('rate');

                return $self->complete(
                    $session, $standardCmd,
                    'Text-to-speech configuration \'' . $configuration . '\': word rate reset',
                );

            } elsif (! ($ttsObj->engine eq 'festival' || $ttsObj->engine eq 'swift')) {

                return $self->error(
                    $session, $inputString,
                    $axmud::SCRIPT  . ' can only modify the word rate of the Festival and Swift'
                    . ' engines',
                );
            }

            # Check <num> is a valid value
            if (! $axmud::CLIENT->intCheck($var2, 0, 100)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid text-to-speech word rate \'' . $var2 . '\' (must be in the range'
                    . ' 0-100)'
                );

            } else {

                # Set the rate
                $ttsObj->ivPoke('rate', $var2);

                return $self->complete(
                    $session, $standardCmd,
                    'Text-to-speech configuration \'' . $configuration . '\': word rate set to \''
                    . $var2 . '\'',
                );
            }

        # ;mcf <config> pitch <num>
        # ;mcf <config> -p <num>
        } elsif ($var eq 'pitch' || $var eq '-p') {

            if (! $var2) {

                $ttsObj->ivUndef('pitch');

                return $self->complete(
                    $session, $standardCmd,
                    'Text-to-speech configuration \'' . $configuration . '\': word pitch reset',
                );

            } elsif (! ($ttsObj->engine eq 'espeak' || $ttsObj->engine eq 'swift')) {

                return $self->error(
                    $session, $inputString,
                    $axmud::SCRIPT  . ' can only modify the word pitch of the eSpeak and Swift'
                    . ' engines',
                );
            }

            # Check <num> is a valid value
            if (! $axmud::CLIENT->intCheck($var2, 0, 100)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid text-to-speech word pitch \'' . $var2 . '\' (must be in the range'
                    . ' 0-100)'
                );

            } else {

                # Set the pitch
                $ttsObj->ivPoke('pitch', $var2);

                return $self->complete(
                    $session, $standardCmd,
                    'Text-to-speech configuration \'' . $configuration . '\': word pitch set to \''
                    . $var2 . '\'',
                );
            }

        # ;mcf <config> volume <num>
        # ;mcf <config> -l <num>
        } elsif ($var eq 'volume' || $var eq '-l') {

            if (! $var2) {

                $ttsObj->ivUndef('volume');

                return $self->complete(
                    $session, $standardCmd,
                    'Text-to-speech configuration \'' . $configuration . '\': volume reset',
                );

            } elsif (! ($ttsObj->engine eq 'festival' || $ttsObj->engine eq 'swift')) {

                return $self->error(
                    $session, $inputString,
                    $axmud::SCRIPT  . ' can only modify the volume of the Festival and Swift'
                    . ' engines',
                );
            }

            # Check <num> is a valid value
            if (! $axmud::CLIENT->intCheck($var2, 0-100)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid text-to-speech volume \'' . $var2 . '\' (must be in the range'
                    . ' 0-100)'
                );

            } else {

                # Set the volume
                $ttsObj->ivPoke('volume', $var2);

                return $self->complete(
                    $session, $standardCmd,
                    'Text-to-speech configuration \'' . $configuration . '\': volume set to \''
                    . $var2 . '\'',
                );
            }

        # ;mcf <config> use <pattern>
        # ;mcf <config> use
        # ;mcf <config> -u <pattern>
        # ;mcf <config> -u
        # ;mcf <config> exclude <pattern>
        # ;mcf <config> exclude
        # ;mcf <config> -x <pattern>
        # ;mcf <config> -x
        } elsif ($var eq 'use' || $var eq '-u' || $var eq 'exclude' || $var eq '-x') {

            # ;mcf <config> use
            # ;mcf <config> -u
            # ;mcf <config> exclude
            # ;mcf <config> -x
            if (! defined $var2) {

                # Pretty drastic, so get a confirmation first (if there are patterns to remove)
                if ($ttsObj->exclusiveList && ($var eq 'use' || $var eq '-u')) {
                    $string = 'exclusive';
                } elsif ($ttsObj->excludedList && ($var eq 'exclude' || $var eq '-x')) {
                    $string = 'excluded';
                }

                if ($string) {

                    $choice = $session->mainWin->showMsgDialogue(
                        'Reset text-to-speech patterns',
                        'question',
                        'Are you sure you want to remove ' . $string . ' patterns from the'
                        . ' configuration \'' . $configuration . '\'?',
                        'yes-no',
                    );

                    if (! defined $choice || $choice eq 'no') {

                        return $self->complete(
                            $session, $standardCmd,
                            'List of exclusive and exclusive patterns not reset',
                        );
                    }
                }

                if ($var eq 'use' || $var eq '-u') {

                    $ttsObj->ivEmpty('exclusiveList');

                    return $self->complete(
                        $session, $standardCmd,
                        'Text-to-speech configuration \'' . $configuration . '\': exclusive pattern'
                        . ' list reset',
                    );

                } else {

                    $ttsObj->ivEmpty('excludedList');

                    return $self->complete(
                        $session, $standardCmd,
                        'Text-to-speech configuration \'' . $configuration . '\': excluded pattern'
                        . ' list reset',
                    );
                }

            # ;mcf <config> use <pattern>
            # ;mcf <config> -u <pattern>
            } elsif ($var eq 'use' || $var eq '-u') {

                if ($axmud::CLIENT->regexCheck($var2)) {

                    return $self->error(
                        $session, $inputString,
                        'The pattern you specified isn\'t a valid regular expression',
                    );
                }

                $ttsObj->ivPush('exclusiveList', $var2);

                return $self->complete(
                    $session, $standardCmd,
                    'Text-to-speech configuration \'' . $configuration . '\': exclusive pattern'
                    . ' added',
                );

            # ;mcf <config> exclude <pattern>
            # ;mcf <config> -x <pattern>
            } else {

                if ($axmud::CLIENT->regexCheck($var2)) {

                    return $self->error(
                        $session, $inputString,
                        'The pattern you specified isn\'t a valid regular expression',
                    );
                }

                $ttsObj->ivPush('excludedList', $var2);

                return $self->complete(
                    $session, $standardCmd,
                    'Text-to-speech configuration \'' . $configuration . '\': excluded pattern'
                    . ' added',
                );
            }

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid setting - try \';help modifyconfig\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteConfig;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteconfig', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dcf', 'delcf', 'deleteconfig'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a text-to-speech configuration';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $configuration,
            $check,
        ) = @_;

        # Local variables
        my $ttsObj;

        # For the benefit of visually-impaired users, don't check for improper arguments (ignore
        #   anything after <name>)
        if (! $configuration) {

            return $self->error(
                $session, $inputString,
                'Delete which text-to-speech configuration? (Try \'deleteconfig <name>\')',
            );
        }

        # Check the configuration object exists
        if (! $axmud::CLIENT->ivExists('ttsObjHash', $configuration)) {

            return $self->error(
                $session, $inputString,
                'There is no text-to-speech configuration called \'' . $configuration . '\'',
            );

        } else {

            $ttsObj = $axmud::CLIENT->ivShow('ttsObjHash', $configuration);
        }

        # Check we're allowed to delete this configuration object
        if ($axmud::CLIENT->ivExists('constTtsPermObjHash', $configuration)) {

            return $self->error(
                $session, $inputString,
                'The text-to-speech configuration \'' . $configuration . '\' cannot be deleted',
            );
        }

        # Delete the configuration object
        if (! $axmud::CLIENT->del_ttsObj($ttsObj)) {

            return $self->error(
                $session, $inputString,
                'General error deleting the text-to-speech configuration \'' . $configuration
                . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Deleted text-to-speech configuration \'' . $configuration . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListConfig;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listconfig', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lcf', 'listcf', 'listconfig'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows a list of text-to-speech configurations';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Display header
        $session->writeText('List of text-to-speech configurations (* - no delete, + - no modify)');
        $session->writeText('   Configuration    Engine           Voice');

        # Display list
        @list = sort {lc($a->name) cmp lc($b->name)} ($axmud::CLIENT->ivValues('ttsObjHash'));
        foreach my $obj (@list) {

            my ($column, $voice);

            if ($axmud::CLIENT->ivExists('constTtsPermObjHash', $obj->name)) {
                $column = '*';
            } else {
                $column = ' ';
            }

            if ($axmud::CLIENT->ivExists('constTtsFixedObjHash', $obj->name)) {
                $column .= '+ ';
            } else {
                $column .= '  ';
            }

            if (! $obj->voice) {
                $voice = '<none>';
            } else {
                $voice = $obj->voice;
            }

            $session->writeText(
                $column
                . sprintf('%-16.16s %-16.16s %-16.16s', $obj->name, $obj->engine, $voice),
            );
        }

        # Display footer
        return $self->complete(
            $session, $standardCmd,
            'End of list (' . (scalar @list) . ' . configurations found)',
        );
    }
}

# Other windows

{ package Games::Axmud::Cmd::OpenObjectViewer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('openobjectviewer', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['oov', 'viewer', 'openviewer', 'openobjectviewer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens the object viewer window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that an object viewer window for this session isn't already open
        if ($session->viewerWin) {

            # Window already open; draw attention to the fact by presenting it
            $session->viewerWin->restoreFocus();

            return $self->error(
                $session, $inputString,
                'An object viewer window is already open for this session',
            );
        }

        # Open the object viewer window
        if (! $session->mainWin->quickFreeWin('Games::Axmud::OtherWin::Viewer', $session)) {

            return $self->error(
                $session, $inputString,
                'Failed to open an object viewer window for this session',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Object viewer window opened',
            );
        }
    }
}

{ package Games::Axmud::Cmd::CloseObjectViewer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('closeobjectviewer', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cov', 'closeviewer', 'closeobjectviewer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Closes the object viewer window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $winObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (! $session->viewerWin) {

            return $self->error(
                $session, $inputString,
                'The object viewer window is already closed for this session',
            );

        } else {

            # Close the window
            $session->viewerWin->winDestroy();
            if ($session->viewerWin) {

                return $self->error(
                    $session, $inputString,
                    'Failed to close the object viewer window for this session',
                );

            } else {

                return $self->complete($session, $standardCmd, 'Object viewer window closed');
            }
        }
    }
}

{ package Games::Axmud::Cmd::OpenAutomapper;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('openautomapper', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['oam', 'map', 'openmap', 'openautomapper'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens the Automapper window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if ($session->mapWin) {

            $session->mapWin->restoreFocus();

            return $self->error(
                $session, $inputString,
                'An Automapper window is already open for this session',
            );

        } else {

            # Open the window
            $session->mapObj->openWin();
            if (! $session->mapWin) {

                return $self->error(
                    $session, $inputString,
                    'Failed to open an Automapper window for this session',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Automapper window opened',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::CloseAutomapper;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('closeautomapper', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cam', 'closemap', 'closeautomapper'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Closes the Automapper window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (! $session->mapWin) {

            return $self->error(
                $session, $inputString,
                'The Automapper window is already closed for this session',
            );

        } else {

            # Close the window
            $session->mapWin->winDestroy();
            if ($session->mapWin) {

                return $self->error(
                    $session, $inputString,
                    'Failed to close the Automapper window for this session',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Automapper window closed',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ToggleAutomapper;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('toggleautomapper', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tam', 'toggleautomap', 'toggleautomapper'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles various automapper settings';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my $msg;

        # Check for improper arguments
        if (! defined $switch || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;tam -o
        if ($switch eq '-o') {

            if ($session->worldModelObj->autoOpenWinFlag) {

                return $self->error(
                    $session, $inputString,
                    'The Automapper window is already set to open when ' . $axmud::SCRIPT
                    . ' starts',
                );

            } else {

                $session->worldModelObj->set_autoOpenWinFlag(TRUE);

                return $self->complete(
                    $session, $standardCmd,
                    'The Automapper window will now open when ' . $axmud::SCRIPT . ' starts',
                );
            }

        # ;tam -s
        } elsif ($switch eq '-s') {

            if (! $session->worldModelObj->autoOpenWinFlag) {

                return $self->error(
                    $session, $inputString,
                    'The Automapper window is already set not to open when ' . $axmud::SCRIPT
                    . ' starts',
                );

            } else {

                $session->worldModelObj->set_autoOpenWinFlag(FALSE);

                return $self->complete(
                    $session, $standardCmd,
                    'The Automapper window will no longer open when ' . $axmud::SCRIPT . ' starts',
                );
            }

        } else {

            # ;tam -e
            if ($switch eq '-e') {

                $session->worldModelObj->toggle_componentFlag('showMenuBarFlag');
                if ($session->worldModelObj->showMenuBarFlag) {
                    $msg = 'Automapper window menu bar(s) shown';
                } else {
                    $msg = 'Automapper window menu bar(s) hidden';
                }

            # ;tam -t
            } elsif ($switch eq '-t') {

                $session->worldModelObj->toggle_componentFlag('showToolbarFlag');
                if ($session->worldModelObj->showToolbarFlag) {
                    $msg = 'Automapper window toolbar(s) shown';
                } else {
                    $msg = 'Automapper window toolbar(s) hidden';
                }

            # ;tam -r
            } elsif ($switch eq '-r') {

                $session->worldModelObj->toggle_componentFlag('showTreeViewFlag');
                if ($session->worldModelObj->showTreeViewFlag) {
                    $msg = 'Automapper window region list(s) shown';
                } else {
                    $msg = 'Automapper window region list(s) hidden';
                }

            # ;tam -m
            } elsif ($switch eq '-m') {

                $session->worldModelObj->toggle_componentFlag('showCanvasFlag');
                if ($session->worldModelObj->showCanvasFlag) {
                    $msg = 'Automapper window map(s) shown';
                } else {
                    $msg = 'Automapper window map(s) hidden';
                }

            } else {

                # Unrecognised switch
                return $self->improper($session, $inputString);
            }

            return $self->complete($session, $standardCmd, $msg);
        }
    }
}

{ package Games::Axmud::Cmd::LocatorWizard;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('locatorwizard', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lcw', 'locwiz', 'locwizard', 'locatorwizard'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens the Locator wizard window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if ($session->wizWin) {

            return $self->error(
                $session, $inputString,
                'A wizard window is already open for this session',
            );

        } else {

            # Open the window
            $session->mainWin->quickFreeWin('Games::Axmud::WizWin::Locator', $session);
            if (! $session->wizWin) {

                return $self->error(
                    $session, $inputString,
                    'Failed to open the Locator wizard window for this session',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Locator wizard window opened',
                );
            }
        }
    }
}

# Dictionaries

{ package Games::Axmud::Cmd::AddDictionary;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('adddictionary', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ady', 'adddict', 'adddictionary'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a new dictionary';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name, $language,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the dictionary doesn't already exist
        if ($axmud::CLIENT->ivExists('dictHash', $name)) {

            return $self->error(
                $session, $inputString,
                'Could not add the dictionary \'' . $name . '\' - dictionary already exists',
            );
        }

        # Check that $name is a valid name
        if (! $axmud::CLIENT->nameCheck($name, 16)) {

            return $self->error(
                $session, $inputString,
                'Could not add the dictionary \'' . $name . '\' - invalid name',
            );

        # If the language was specified, check it's not too long
        } elsif ($language && ! $axmud::CLIENT->nameCheck($language, 16)) {

            return $self->error(
                $session, $inputString,
                'Could not add the dictionary \'' . $name . '\' - invalid language \'' . $language
                . '\'',
            );
        }

        # Create the dictionary
        $obj = Games::Axmud::Obj::Dict->new($session, $name, $language);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Could not add the dictionary \'' . $name . '\'',
            );

        } else {

            # Update IVs
            $axmud::CLIENT->add_dict($obj);

            return $self->complete($session, $standardCmd, 'Added the dictonary \'' . $name . '\'');
        }
    }
}

{ package Games::Axmud::Cmd::SetDictionary;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setdictionary', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sdy', 'setdict', 'setdictionary'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the current dictionary for this session';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name, $language,
            $check,
        ) = @_;

        # Local variables
        my (
            $matchFlag, $obj,
            @list,
        );

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there are no 'free' windows open
        @list = $axmud::CLIENT->desktopObj->listSessionFreeWins($session, TRUE);
        if (@list) {

            OUTER: foreach my $winObj (@list) {

                if ($winObj->_objClass eq 'Games::Axmud::EditWin::Dict') {

                    $matchFlag = TRUE;
                    last OUTER;
                }
            }

            if ($matchFlag) {

                return $self->error(
                    $session, $inputString,
                    'Can\'t set the current dictionary while there are dictionary \'edit\' windows'
                    . ' open (try closing them first)',
                );
            }
        }

        # If the name is already in use and $language was specified, need to display an error
        if ($axmud::CLIENT->ivExists('dictHash', $name) && defined $language) {

            return $self->error(
                $session, $inputString,
                'This command can\'t be used to change the language of the existing dictionary \''
                . $name . '\' (try \';setlanguage\')',
            );
        }

        # If the dictionary already exists is already in use, make it current
        if ($axmud::CLIENT->ivExists('dictHash', $name)) {

            $obj = $axmud::CLIENT->ivShow('dictHash', $name);
            $session->set_currentDict($obj);
            # The current world profile also stores the current dictionary
            $session->currentWorld->ivPoke('dict', $name);

            return $self->complete(
                $session, $standardCmd,
                'The current dictionary has been set to \'' . $name . '\'',
            );
        }

        # Otherwise, check that $name is a valid name
        if (! $axmud::CLIENT->nameCheck($name, 16)) {

            return $self->error(
                $session, $inputString,
                'Could not add the dictionary \'' . $name . '\' - invalid name',
            );

        # If the language was specified, check it's not too long
        } elsif ($language && ! $axmud::CLIENT->nameCheck($language, 16)) {

            return $self->error(
                $session, $inputString,
                'Could not add the dictionary \'' . $name . '\' - invalid language \'' . $language
                . '\'',
            );
        }

        # Create the dictionary and make it the current one
        $obj = Games::Axmud::Obj::Dict->new($session, $name, $language);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Could not add the dictionary \'' . $name . '\'',
            );

        } else {

            # Update IVs
            $axmud::CLIENT->add_dict($obj);
            $session->set_currentDict($obj);
            # The current world profile also stores the current dictionary
            $session->currentWorld->ivPoke('dict', $name);

            return $self->complete(
                $session, $standardCmd,
                'The current dictionary has been set to \'' . $name . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::CloneDictionary;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('clonedictionary', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cdy', 'clonedict', 'clonedictionary'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Clones a dictionary';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $original, $copy,
            $check,
        ) = @_;

        # Local variables
        my ($originalObj, $copyObj);

        # Check for improper arguments
        if (! defined $original || ! defined $copy || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that <original> exists, and <copy> doesn't
        if (! $axmud::CLIENT->ivExists('dictHash', $original)) {

            return $self->error(
                $session, $inputString,
                'The dictionary \'' . $original . '\' doesn\'t exist',
            );

        } elsif ($axmud::CLIENT->ivExists('dictHash', $copy)) {

            return $self->error(
                $session, $inputString,
                'The dictionary \'' . $copy . '\' already exists',
            );

        } else {

            $originalObj = $axmud::CLIENT->ivShow('dictHash', $original);
        }

        # Check that $copy is a valid name
        if (! $axmud::CLIENT->nameCheck($copy, 16)) {

            return $self->error(
                $session, $inputString,
                'Could not add the dictionary \'' . $copy . '\' - invalid name',
            );
        }

        # Create the dictionary
        $copyObj = $originalObj->clone($session, $copy);
        if (! $copyObj) {

            return $self->error(
                $session, $inputString,
                'Could not clone the dictionary \'' . $original . '\'',
            );

        } else {

            # Update IVs
            $axmud::CLIENT->add_dict($copyObj);

            return $self->complete(
                $session, $standardCmd,
                'Cloned the dictonary \'' . $original . '\' into one named \'' . $copy . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditDictionary;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editdictionary', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['edy', 'editdict', 'editdictionary'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a dictionary';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the dictionary exists
        if (! $name) {

            $name = $session->currentDict->name;
            $obj = $session->currentDict;

        } elsif (! $axmud::CLIENT->ivExists('dictHash', $name)) {

            return $self->error(
                $session, $inputString,
                'Could not edit the \'' . $name . '\' dictionary - object does not exist',
            );

        } else {

            $obj = $axmud::CLIENT->ivShow('dictHash', $name);
        }

        # Open an 'edit' window for the dictionary
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Dict',
                $session->mainWin,
                $session,
                'Edit dictionary \'' . $obj->name . '\'',
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the \'' . $name . '\' dictionary',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the \'' . $name . '\' dictionary',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteDictionary;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletedictionary', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ddy', 'deldict', 'deletedict', 'deletedictionary'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a dictionary';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my (
            $matchFlag, $obj,
            @list,
        );

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there are no dictionary 'edit' windows open
        @list = $axmud::CLIENT->desktopObj->listSessionFreeWins($session, TRUE);
        if (@list) {

            OUTER: foreach my $winObj (@list) {

                if ($winObj->_objClass eq 'Games::Axmud::EditWin::Dict') {

                    $matchFlag = TRUE;
                    last OUTER;
                }
            }

            if ($matchFlag) {

                return $self->error(
                    $session, $inputString,
                    'Can\'t delete a dictionary while there are dictionary \'edit\' window open'
                    . ' (try closing them first)',
                );
            }
        }

        # Check the dictionary exists
        if (! $axmud::CLIENT->ivExists('dictHash', $name)) {

            return $self->error(
                $session, $inputString,
                'Could not delete the dictionary \'' . $name . '\' - dictionary doesn\'t exist',
            );

        } else {

            $obj = $axmud::CLIENT->ivShow('dictHash', $name);
        }

        # Check that the dictionary isn't the current dictionary for this session...
        if (defined $session->currentDict && $session->currentDict eq $obj) {

            return $self->error(
                $session, $inputString,
                'Could not delete the dictionary \'' . $name . '\' because it\'s the current'
                . ' dictionary for this session',
            );
        }

        # ...or any other session
        foreach my $otherSession ($axmud::CLIENT->listSessions()) {

            if ($otherSession->currentDict && $otherSession->currentDict eq $obj) {

                return $self->error(
                    $session, $inputString,
                    'Could not delete the dictionary \'' . $name . '\' because it\'s the current'
                    . ' dictionary for another session',
                );
            }
        }

        # Delete the dictionary
        $axmud::CLIENT->del_dict($obj);

        return $self->complete($session, $standardCmd, 'Deleted the dictonary \'' . $name . '\'');
    }
}

{ package Games::Axmud::Cmd::ListDictionary;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listdictionary', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ldy', 'listdict', 'listdictionary'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists dictionaries';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get a sorted list of dictionary names
        @list = sort {lc($a) cmp lc($b)} ($axmud::CLIENT->ivKeys('dictHash'));
        if (! @list) {

            return $self->complete($session, $standardCmd, 'The dictionary list is empty');
        }

        # Display header
        $session->writeText('List of dictionaries (* = current dictionary)');
        $session->writeText('   Name             Language');

        # Display list
        foreach my $name (@list) {

            my ($dictObj, $string);

            $dictObj = $axmud::CLIENT->ivShow('dictHash', $name);

            if ($name eq $session->currentDict->name) {
                $string = ' * ';
            } else {
                $string = '   ';
            }

            $session->writeText($string . sprintf('%-16.16s', $name) . ' ' . $dictObj->language);
        }

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 dictionary displayed)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . @list . ' dictionaries displayed)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetLanguage;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setlanguage', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['stl', 'setlang', 'setlanguage'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets a dictionary\'s language';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name, $language,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;stl <language>
        if (! defined $language) {

            # Use the current dictionary
            $language = $name;
            $name = $session->currentDict->name;
        }

        # Check that the dictonary <name> exists
        if (! $axmud::CLIENT->ivExists('dictHash', $name)) {

            return $self->error(
                $session, $inputString,
                'Cannot change the dictionary language - dictionary \'' . $name
                . '\' doesn\'t exist',
            );

        } else {

            $obj = $axmud::CLIENT->ivShow('dictHash', $name);
        }

        # Check that <language> is a valid name
        if (! $axmud::CLIENT->nameCheck($language, 16)) {

            return $self->error(
                $session, $inputString,
                'Cannot change the dictionary language - invalid language \'' . $language . '\'',
            );

        # Check the language isn't already set to <language>
        } elsif ($obj->language eq $language) {

            return $self->error(
                $session, $inputString,
                'Cannot change the \'' . $name . '\' dictionary language - the language is already'
                . ' set to \'' . $language . '\'',
            );

        } else {

            # Set the language
            $obj->ivPoke('language', $language);

            return $self->complete(
                $session, $standardCmd,
                'The \'' . $name . '\' dictionary\'s language has been set to \'' . $language
                . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SwitchLanguage;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('switchlanguage', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['swl', 'switchlang', 'switchlanguage'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Uploads a phrasebook to the current dictionary';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $language,
            $check,
        ) = @_;

        # Local variables
        my (
            $pbObj,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import a sorted list of phrasebook objects (GA::Obj::Phrasebook)
        @list = sort {lc($a->name) cmp lc($b->name)}
                    ($axmud::CLIENT->ivValues('constPhrasebookHash'));

        # Text files from which phrasebook objects must be missing
        if (! @list) {

            return $self->error($session, $inputString, 'There are no other languages available');
        }

        # ;swl
        if (! defined $language) {

            # Display header
            $session->writeText('List of available phrasebooks');
            $session->writeText('   Phrasebook name  Language');

            # Display list
            foreach my $pbObj (@list) {

                $session->writeText(
                    sprintf('   %-16.16s %-16.16s', $pbObj->name, $pbObj->targetName),
                ),
            }

            # Display footer
            if (@list == 1) {

                return $self->complete($session, $standardCmd, 'End of list (1 phrasebook found)');

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (' . @list . ' phrasebooks found)',
                );
            }

        # ;swl <phrasebook_name>
        # ;swl <language_name>
        } else {

            # Find the phrasebook object matching <name>. First check phrasebook names
            $pbObj = $axmud::CLIENT->ivShow('constPhrasebookHash', lc($language));
            if (! $pbObj) {

                # Then, check target language names (e.g. 'Francais')
                OUTER: foreach my $otherObj (@list) {

                    if (lc($otherObj->targetName) eq lc($language)) {

                        $pbObj = $otherObj;
                        last OUTER;
                    }
                }
            }

            if (! $pbObj) {

                return $self->error(
                    $session, $inputString,
                    'No phrasebook matching \'' . $language . '\' found',
                );
            }

            # Update the current dictionary
            if (! $session->currentDict->uploadPhrasebook($pbObj)) {

                return $self->error(
                    $session, $inputString,
                    'Could not switch languages (internal error)',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'The current dictionary\'s language has been switched to \''
                    . $session->currentDict->language . '\'',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::AddWord;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addword', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['adw', 'addword'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds words or terms to the current dictionary';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($obj, $count, $switchCount, $addCount, $failCount);

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Check there is a current dictionary to which words can be added
        if (! $session->currentDict) {

            return $self->error(
                $session, $inputString,
                'Can\'t add words and terms because there is no current dictionary for this'
                . ' session',
            );

        } else {

            $obj = $session->currentDict;
        }

        # Extract switches on a continuous loop, until there are no switches left
        $count = 0;         # Total number of switch options
        $failCount = 0;     # Number of invalid switch options
        do {

            my (
                $switch, $word, $line, $portable, $decoration, $type, $plural, $pseudo, $declined,
                $substitution, $numeral, $unit, $singular, $value, $time,
            );

            $switchCount = 0;   # No. switches extracted on this loop

            # ;adw -g <guild>
            ($switch, $word, @args) = $self->extract('-g', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    # User didn't specify a guild
                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -g <guild>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Add the new word
                    $obj->ivAdd('guildHash', $word, 'guild');
                    # Update combined hashes
                    $obj->updateCombNounHash('guild', TRUE, $word, 'guild');

                    if ($obj->ivExists('unknownWordHash', $word)) {

                        # Remove the word from the unknown words list
                        $obj->ivDelete('unknownWordHash', $word);
                    }
                }
            }

            # ;adw -r <race>
            ($switch, $word, @args) = $self->extract('-r', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -r <race>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    $obj->ivAdd('raceHash', $word, 'race');
                    $obj->updateCombNounHash('race', TRUE, $word, 'race');

                    if ($obj->ivExists('unknownWordHash', $word)) {

                        $obj->ivDelete('unknownWordHash', $word);
                    }
                }
            }

            # ;adw -w <weapon>
            ($switch, $word, @args) = $self->extract('-w', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -w <weapon>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    $obj->ivAdd('weaponHash', $word, 'weapon');
                    $obj->updateCombNounHash('weapon', TRUE, $word, 'weapon');

                    if ($obj->ivExists('unknownWordHash', $word)) {

                        $obj->ivDelete('unknownWordHash', $word);
                    }
                }
            }

            # ;adw -a <armour>
            ($switch, $word, @args) = $self->extract('-a', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -a <armour>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    $obj->ivAdd('armourHash', $word, 'armour');
                    $obj->updateCombNounHash('armour', TRUE, $word, 'armour');

                    if ($obj->ivExists('unknownWordHash', $word)) {

                        $obj->ivDelete('unknownWordHash', $word);
                    }
                }
            }

            # ;adw -e <garment>
            ($switch, $word, @args) = $self->extract('-e', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -e <garment>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    $obj->ivAdd('garmentHash', $word, 'garment');
                    $obj->updateCombNounHash('garment', TRUE, $word, 'garment');

                    if ($obj->ivExists('unknownWordHash', $word)) {

                        $obj->ivDelete('unknownWordHash', $word);
                    }
                }
            }

            # ;adw -s <being>
            ($switch, $word, @args) = $self->extract('-s', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -s <being>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    $obj->ivAdd('sentientHash', $word, 'sentient');
                    $obj->updateCombNounHash('sentient', TRUE, $word, 'sentient');

                    if ($obj->ivExists('unknownWordHash', $word)) {

                        $obj->ivDelete('unknownWordHash', $word);
                    }
                }
            }

            # ;adw -k <creature>
            ($switch, $word, @args) = $self->extract('-k', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -k <creature>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    $obj->ivAdd('creatureHash', $word, 'creature');
                    $obj->updateCombNounHash('creature', TRUE, $word, 'creature');

                    if ($obj->ivExists('unknownWordHash', $word)) {

                        $obj->ivDelete('unknownWordHash', $word);
                    }
                }
            }

            # ;adw -p <portable>
            # ;adw -p <portable> <type>
            ($switch, $portable, $type, @args) = $self->extract('-p', 2, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $portable) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -p <portable> <type>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Deafult <type> is 'other'
                    if (! defined $type) {

                        $type = 'other';

                    # If the type doesn't already exist, add it
                    } elsif (! defined $obj->ivFind('portableTypeList', $type)) {

                        $obj->ivPush('portableTypeList', $type);
                    }

                    $obj->ivAdd('portableHash', $portable, 'portable');
                    $obj->ivAdd('portableTypeHash', $portable, $type);
                    $obj->updateCombNounHash('portable', TRUE, $portable, 'portable');

                    if ($obj->ivExists('unknownWordHash', $portable)) {

                        $obj->ivDelete('unknownWordHash', $portable);
                    }
                }
            }

            # ;adw -d <decoration>
            # ;adw -d <decoration> <type>
            ($switch, $decoration, $type, @args) = $self->extract('-d', 2, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $decoration) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -d <decoration> <type>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Deafult <type> is 'other'
                    if (! defined $type) {

                        $type = 'other';

                    # If the type doesn't already exist, add it
                    } elsif (! defined $obj->ivFind('decorationTypeList', $type)) {

                        $obj->ivPush('decorationTypeList', $type);
                    }

                    $obj->ivAdd('decorationHash', $decoration, 'decoration');
                    $obj->ivAdd('decorationTypeHash', $decoration, $type);
                    $obj->updateCombNounHash('decoration', TRUE, $decoration, 'decoration');

                    if ($obj->ivExists('unknownWordHash', $decoration)) {

                        $obj->ivDelete('unknownWordHash', $decoration);
                    }
                }
            }

            # ;adw -l <word> <plural>
            ($switch, $word, $plural, @args) = $self->extract('-l', 2, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word || ! defined $plural) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -l <word> <plural>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    $obj->ivAdd('pluralNounHash', $word, $plural);
                    $obj->ivAdd('reversePluralNounHash', $plural, $word);
                    $obj->updateCombNounHash('pluralNoun', TRUE, $word, 'pluralNoun');

                    if ($obj->ivExists('unknownWordHash', $plural)) {

                        $obj->ivDelete('unknownWordHash', $plural);
                    }
                }
            }

            # ;adw -x <word> <pseudo_noun>
            ($switch, $word, $pseudo, @args) = $self->extract('-x', 2, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word || ! defined $pseudo) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -x <word> <pseudo_noun>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    $obj->ivAdd('pseudoNounHash', $pseudo, $word);
                    $obj->updateCombNounHash('pseudoNoun', TRUE, $word, 'pseudoNoun');

                    if ($obj->ivExists('unknownWordHash', $word)) {

                        $obj->ivDelete('unknownWordHash', $word);
                    }
                }
            }

            # ;adw -j <adjective>
            ($switch, $word, @args) = $self->extract('-j', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -j <adjective>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    $obj->ivAdd('adjHash', $word, 'adj');
                    $obj->updateCombAdjHash('adj', TRUE, $word, 'adj');

                    if ($obj->ivExists('unknownWordHash', $word)) {

                        $obj->ivDelete('unknownWordHash', $word);
                    }
                }
            }

            # ;adw -c <adjective> <declined_form>
            ($switch, $word, $declined, @args) = $self->extract('-c', 2, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word || ! defined $declined) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -c <adjective>'
                        . ' <declined_form>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    $obj->ivAdd('declinedAdjHash', $declined, $word);
                    $obj->ivAdd('reverseDeclinedAdjHash', $word, $declined);
                    $obj->updateCombAdjHash('declinedAdj', TRUE, $declined, 'declinedAdj');

                    if ($obj->ivExists('unknownWordHash', $declined)) {

                        $obj->ivDelete('unknownWordHash', $declined);
                    }
                }
            }

            # ;adw -y <adjective> <pseudo_adjective>
            ($switch, $word, $pseudo, @args) = $self->extract('-y', 2, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word || ! defined $pseudo) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -y <adjective>'
                        . ' <pseudo_adjective>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    $obj->ivAdd('pseudoAdjHash', $pseudo, $word);
                    $obj->updateCombAdjHash('pseudoAdj', TRUE, $pseudo, 'pseudoAdj');

                    if ($obj->ivExists('unknownWordHash', $pseudo)) {

                        $obj->ivDelete('unknownWordHash', $pseudo);
                    }
                }
            }

            # ;adw -v <substitution> <pseudo_object>
            ($switch, $substitution, $pseudo, @args) = $self->extract('-v', 2, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $substitution || ! defined $pseudo) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -v <substitution>'
                        . ' <pseudo_object>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    $obj->ivAdd('pseudoObjHash', $substitution, $pseudo);
                    # (NB Pseudo-objects don't appear in any combined hash and aren't removed from
                    #   the hash of unknown words)
                }
            }

            # ;adw -i <word>
            ($switch, $word, @args) = $self->extract('-i', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -i <word>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    $obj->ivAdd('ignoreWordHash', $word, 'ignoreWord');
                    # (NB Ignorable words don't appear in any combined hash)

                    if ($obj->ivExists('unknownWordHash', $word)) {

                        $obj->ivDelete('unknownWordHash', $word);
                    }
                }
            }

            # ;adw -n <numeral> <word>
            ($switch, $numeral, $word, @args) = $self->extract('-n', 2, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $numeral || ! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -n <numeral> <word>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    $obj->ivAdd('numberHash', $word, $numeral);
                    # (NB Number words don't appear in any combined hash and aren't removed from
                    #   the hash of unknown words)
                }
            }

            # ;adw -t <unit> <singular>
            # ;adw -t <unit> <singular> <plural>
            ($switch, $unit, $singular, $plural, @args) = $self->extract('-t', 3, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $unit || ! defined $singular) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -t <unit> <singular> <plural>\'',
                        $self->_objClass . '->do',
                    );

                # Check that <unit> is one of the allowed types
                } elsif (! $obj->ivExists('timeHash', $unit)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $unit . '\' is not a valid time unit\'',
                        $self->_objClass . '->do',
                    );

                # <unit> is valid
                } else {

                    $obj->ivAdd('timeHash', $unit, $singular);
                    $obj->ivAdd('reverseTimeHash', $singular, $unit);
                    # (NB Time words don't appear in any combined hash and aren't removed from
                    #   the hash of unknown words)

                    if (defined $plural) {

                        $obj->ivAdd('timePluralHash', $unit, $plural);
                        $obj->ivAdd('reverseTimePluralHash', $plural, $unit);

                    } else {

                        # Guess the plural form
                        if ($obj->pluralEndingList) {

                            $obj->ivAdd(
                                'timePluralHash',
                                $unit,
                                $singular . $obj->ivFirst('pluralEndingList'),
                            );
                            $obj->ivAdd(
                                'reverseTimePluralHash',
                                $singular . $obj->ivFirst('pluralEndingList'),
                                $unit,
                            );

                        } else {

                            # No plural ending is defined - use the English one
                            $obj->ivAdd('timePluralHash', $unit, ($singular . 's'));
                            $obj->ivAdd('reverseTimePluralHash', ($singular . 's'), $unit);
                        }
                    }
                }
            }

            # ;adw -b <value> <time_of_day>
            ($switch, $value, $time, @args) = $self->extract('-b', 2, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $value || ! defined $time) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -b <value> <time_of_day>\'',
                        $self->_objClass . '->do',
                    );

                # Check that <value> is valid
                } elsif ($value ne '0' && $value ne '1') {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $unit . '\' is not a valid value (must be 0 or 1)\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    # (NB Clock words don't appear in any combined hash and aren't removed from
                    #   the hash of unknown words)
                    $obj->ivAdd('clockDayHash', $time, $value);
                }
            }

            # ;adw -f <value> <hours>
            ($switch, $value, $time, @args) = $self->extract('-f', 2, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $value || ! defined $time) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -f <value> <hours>\'',
                        $self->_objClass . '->do',
                    );

                # Check that <value> is valid
                } elsif (! ($value =~ /\D/) || $value < 0 || $value > 24) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $unit . '\' is not a valid value (must be in the range 0-24)\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    # (NB Clock words don't appear in any combined hash and aren't removed from
                    #   the hash of unknown words)
                    $obj->ivAdd('clockHourHash', $time, $value);
                }
            }

            # ;adw -m <value> <minutes>
            ($switch, $value, $time, @args) = $self->extract('-m', 2, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $value || ! defined $time) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -m <value> <minutes>\'',
                        $self->_objClass . '->do',
                    );

                # Check that <value> is valid
                } elsif ($value =~ /\D/ || $value < 0 || $value > 60) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $unit . '\' is not a valid value (must be in the range 0-60)\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    # (NB Clock words don't appear in any combined hash and aren't removed from
                    #   the hash of unknown words)
                    $obj->ivAdd('clockMinuteHash', $time, $value);
                }
            }

            # ;adw -u <word>
            ($switch, $word, @args) = $self->extract('-u', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -u <word>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    $obj->ivAdd('unknownWordHash', $word, undef);
                    # (NB Unknown words don't appear in any combined hash)
                }
            }

            # ;adw -o <line>
            ($switch, $line, @args) = $self->extract('-o', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $line) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';addword -o <line>\'',
                        $self->_objClass . '->do',
                    );

                } else {

                    $obj->ivAdd('contentsLinesHash', $line, undef);
                    # (NB Contents lines don't appear in any combined hash)
                }
            }

        # Continue loop until @args is empty, or no valid switches were found during the last loop
        } until (! @args || ! $switchCount);

        # Display confirmation
        $addCount = $count - $failCount;

        if ($addCount == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Added 1 new word or term to the current dictionary (failures: ' . $failCount . ')',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Added ' . $addCount . ' new words and terms to the current dictionary (failures: '
                . $failCount . ')',
            );
        }
    }
}

{ package Games::Axmud::Cmd::QuickAddWord;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('quickaddword', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['qaw', 'quickword', 'quickaddword'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens a window to add words to the dictionary';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Open an 'other' window
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::OtherWin::QuickWord',
                $session->mainWin,
                $session,
                'Quick word adder',
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not open the quick word adder window',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened the quick word adder window',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteWord;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteword', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dlw', 'delword', 'deleteword'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes words or terms from the current dictionary';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $obj, $count, $switchCount, $addCount, $failCount,

        );

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Check there is a current dictionary from which words can be deleted
        if (! $session->currentDict) {

            return $self->error(
                $session, $inputString,
                'Can\'t delete words or terms because there is no current dictionary for this'
                . ' session',
            );

        } else {

            $obj = $session->currentDict;
        }

        # Extract switches on a continuous loop, until there are no switches left
        $count = 0;         # Total number of switch options
        $failCount = 0;    # Number of invalid switch options
        do {

            my ($switch, $word, $portable, $decoration, $plural, $pseudo, $declined, $time);

            $switchCount = 0;   # No. switches extracted on this loop

            # ;dlw -g <guild>
            ($switch, $word, @args) = $self->extract('-g', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    # User didn't specify a guild
                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -g <guild>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('guildHash', $word)) {

                    # Word not in the dictionary
                    $failCount++;
                    $session->writeWarning(
                        '\'' . $word . '\' isn\'t a guild word in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('guildHash', $word);
                    # Update combined hashes
                    $obj->updateCombNounHash('guild', FALSE, $word);
                }
            }

            # ;dlw -r <race>
            ($switch, $word, @args) = $self->extract('-r', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -r <race>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('raceHash', $word)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $word . '\' isn\'t a race word in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('raceHash', $word);
                    # Update combined hashes
                    $obj->updateCombNounHash('race', FALSE, $word);
                }
            }

            # ;dlw -w <weapon>
            ($switch, $word, @args) = $self->extract('-w', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -w <weapon>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('weaponHash', $word)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $word . '\' isn\'t a weapon word in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('weaponHash', $word);
                    # Update combined hashes
                    $obj->updateCombNounHash('weapon', FALSE, $word);
                }
            }

            # ;dlw -a <armour>
            ($switch, $word, @args) = $self->extract('-a', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -a <armour>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('armourHash', $word)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $word . '\' isn\'t an armour word in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('armourHash', $word);
                    # Update combined hashes
                    $obj->updateCombNounHash('armour', FALSE, $word);
                }
            }

            # ;dlw -e <garment>
            ($switch, $word, @args) = $self->extract('-e', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -e <garment>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('garmentHash', $word)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $word . '\' isn\'t a garment word in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('garmentHash', $word);
                    # Update combined hashes
                    $obj->updateCombNounHash('garment', FALSE, $word);
                }
            }

            # ;dlw -s <being>
            ($switch, $word, @args) = $self->extract('-s', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -s <being>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('sentientHash', $word)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $word . '\' isn\'t a sentient being word in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('sentientHash', $word);
                    # Update combined hashes
                    $obj->updateCombNounHash('sentient', FALSE, $word);
                }
            }

            # ;dlw -k <creature>
            ($switch, $word, @args) = $self->extract('-k', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -k <creature>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('creatureHash', $word)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $word . '\' isn\'t a creature word in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('creatureHash', $word);
                    # Update combined hashes
                    $obj->updateCombNounHash('creature', FALSE, $word);
                }
            }

            # ;dlw -p <portable>
            ($switch, $portable, @args) = $self->extract('-p', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $portable) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -p <portable>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('portableHash', $portable)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $portable . '\' isn\'t a portable word in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('portableHash', $portable);
                    $obj->ivDelete('portableTypeHash', $portable);
                    # Update combined hashes
                    $obj->updateCombNounHash('portable', FALSE, $portable);
                }
            }

            # ;dlw -d <decoration>
            ($switch, $decoration, @args) = $self->extract('-p', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $decoration) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -d <decoration>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('decorationHash', $decoration)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $decoration . '\' isn\'t a decoration word in the current'
                        . ' dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('decorationHash', $decoration);
                    $obj->ivDelete('decorationTypeHash', $decoration);
                    # Update combined hashes
                    $obj->updateCombNounHash('decoration', FALSE, $decoration);
                }
            }

            # ;dlw -l <plural>
            ($switch, $plural, @args) = $self->extract('-l', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $plural) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -l <plural>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('reversePluralNounHash', $plural)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $plural . '\' isn\'t a plural noun in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $word = $obj->ivShow('reversePluralNounHash', $plural);
                    $obj->ivDelete('pluralNounHash', $word);
                    $obj->ivDelete('reversePluralNounHash', $plural);
                    # Update combined hashes
                    $obj->updateCombNounHash('pluralNoun', FALSE, $plural);
                }
            }

            # ;dlw -x <pseudo_noun>
            ($switch, $pseudo, @args) = $self->extract('-x', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $pseudo) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -x <pseudo_noun>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('pseudoNounHash', $pseudo)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $pseudo . '\' isn\'t a pseudo-noun in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('pseudoNounHash', $pseudo);
                    # Update combined hashes
                    $obj->updateCombNounHash('pseudoNoun', FALSE, $pseudo);
                }
            }

            # ;dlw -j <adjective>
            ($switch, $word, @args) = $self->extract('-j', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -j <adjective>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('adjHash', $word)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $word . '\' isn\'t an adjective word in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('adjHash', $word);
                    # Update combined hashes
                    $obj->updateCombAdjHash('adj', FALSE, $word);
                }
            }

            # ;dlw -c <declined_form>
            ($switch, $declined, @args) = $self->extract('-c', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $declined) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -c <declined_form>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('declinedAdjHash', $declined)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $declined . '\' isn\'t a declined form of an adjective in the'
                        . ' current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('declinedAdjHash', $declined);
                    # Update combined hashes
                    $obj->updateCombAdjHash('declinedAdj', FALSE, $declined);
                }
            }

            # ;dlw -y <pseudo_adj>
            ($switch, $pseudo, @args) = $self->extract('-y', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $pseudo) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -y <pseudo_adj>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('pseudoAdjHash', $pseudo)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $pseudo . '\' isn\'t a pseudo-adjective in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('pseudoAdjHash', $pseudo);
                    # Update combined hashes
                    $obj->updateCombAdjHash('pseudoAdj', FALSE, $pseudo);
                }
            }

            # ;dlw -v <pseudo_object>
            ($switch, $pseudo, @args) = $self->extract('-v', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $pseudo) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -v <pseudo_object>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('pseudoObjHash', $pseudo)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $pseudo . '\' isn\'t a pseudo-object in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('pseudoObjHash', $pseudo);
                    # (NB Pseudo-objects don't appear in any combined hash)
                }
            }

            # ;dlw -i <ignore_word>
            ($switch, $word, @args) = $self->extract('-i', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -i <ignore_word>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('ignoreWordHash', $word)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $word . '\' isn\'t an ignorable word in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('ignoreWordHash', $word);
                    # (NB Ignorable words don't appear in any combined hash)
                }
            }

            # ;dlw -n <number_word>
            ($switch, $word, @args) = $self->extract('-n', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $word) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -n <number_word>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('numberHash', $word)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $word . '\' isn\'t a number word in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('numberHash', $word);
                    # (NB Number words don't appear in any combined hash)
                }
            }

            # (;dlw -t isn't available)

            # ;dlw -b <time_of_day>
            ($switch, $time, @args) = $self->extract('-b', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $time) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -b <time_of_day>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('clockDayHash', $time)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $time . '\' isn\'t a clock word in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('clockDayHash', $time);
                    # (NB Clock words don't appear in any combined hash)
                }
            }

            # ;dlw -f <hour>
            ($switch, $time, @args) = $self->extract('-f', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $time) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -f <hour>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('clockHourHash', $time)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $time . '\' isn\'t a clock hour word in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('clockHourHash', $time);
                    # (NB Clock words don't appear in any combined hash)
                }
            }

            # ;dlw -m <minute>
            ($switch, $time, @args) = $self->extract('-m', 1, @args);
            if (defined $switch) {

                $count++;
                $switchCount++;

                if (! defined $time) {

                    $failCount++;
                    $session->writeWarning(
                        'Invalid switch option. Usage: \';deleteword -m <minute>\'',
                        $self->_objClass . '->do',
                    );

                } elsif (! $obj->ivExists('clockMinuteHash', $time)) {

                    $failCount++;
                    $session->writeWarning(
                        '\'' . $time . '\' isn\'t a clock minute word in the current dictionary',
                        $self->_objClass . '->do',
                    );

                } else {

                    # Delete the existing word
                    $obj->ivDelete('clockMinuteHash', $time);
                    # (NB Clock words don't appear in any combined hash)
                }
            }

        # Continue loop until @args is empty, or no valid switches were found during the last loop
        } until (! @args || ! $switchCount);

        # Display confirmation
        $addCount = $count - $failCount;

        if ($addCount == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Deleted 1 word or term from the current dictionary (failures: ' . $failCount . ')',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Deleted ' . $addCount . ' words and terms from the current dictionary (failures: '
                . $failCount . ')',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListWord;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listword', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lwd', 'lword', 'listword'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists words and terms from the current dictionary';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! @args) {

            return $self->error(
                $session, $inputString,
                'Invalid switches - try \';listword -z\'',
            );
        }

        # Check there is a current dictionary from which speedwalk characters can be listed
        if (! $session->currentDict) {

            return $self->error(
                $session, $inputString,
                'Can\'t list words or terms because there is no current dictionary for this'
                . ' session',
            );

        } else {

            $obj = $session->currentDict;
        }

        # List each <switch> in turn
        foreach my $switch (@args) {

            my (
                $text,
                @list,
                %hash, %pluralHash,
            );

            if (
                $switch ne '-r' && $switch ne '-g' && $switch ne '-w' && $switch ne '-a'
                && $switch ne '-e' && $switch ne '-s' && $switch ne '-k' && $switch ne '-d'
                && $switch ne '-u' && $switch ne '-p' && $switch ne '-q' && $switch ne '-l'
                && $switch ne '-x' && $switch ne '-j' && $switch ne '-c' && $switch ne '-y'
                && $switch ne '-v' && $switch ne '-i' && $switch ne '-n' && $switch ne '-t'
                && $switch ne '-b' && $switch ne '-f' && $switch ne '-m' && $switch ne '-z'
            ) {
                $session->writeWarning(
                    'Invalid switch - try \;listword -z\;',
                    $self->_objClass . '->do',
                );
            }

            if ($switch eq '-g' || $switch eq '-z') {

                @list = sort {lc($a) cmp lc($b)} ($obj->ivKeys('guildHash'));

                if (! @list) {

                    $session->writeText('List of dictionary guilds (empty)');

                } else {

                    # Display header
                    $session->writeText('List of dictionary guilds (' . scalar @list . ' items)');

                    # Display list
                    $text = '';
                    foreach my $word (@list) {

                        if (! $text) {$text = $word} else {$text .= ', ' . $word}
                    }

                    $session->writeText($text);
                }
            }

            if ($switch eq '-r' || $switch eq '-z') {

                @list = sort {lc($a) cmp lc($b)} ($obj->ivKeys('raceHash'));

                if (! @list) {

                    $session->writeText('List of dictionary races (empty)');

                } else {

                    # Display header
                    $session->writeText('List of dictionary races (' . scalar @list . ' items)');

                    # Display list
                    $text = '';
                    foreach my $word (@list) {

                        if (! $text) {$text = $word} else {$text .= ', ' . $word}
                    }

                    $session->writeText($text);
                }
            }

            if ($switch eq '-w' || $switch eq '-z') {

                @list = sort {lc($a) cmp lc($b)} ($obj->ivKeys('weaponHash'));

                if (! @list) {

                    $session->writeText('List of dictionary weapons (empty)');

                } else {

                    # Display header
                    $session->writeText('List of dictionary weapons (' . scalar @list . ' items)');

                    # Display list
                    $text = '';
                    foreach my $word (@list) {

                        if (! $text) {$text = $word} else {$text .= ', ' . $word}
                    }

                    $session->writeText($text);
                }
            }

            if ($switch eq '-a' || $switch eq '-z') {

                @list = sort {lc($a) cmp lc($b)} ($obj->ivKeys('armourHash'));

                if (! @list) {

                    $session->writeText('List of dictionary armours (empty)');

                } else {

                    # Display header
                    $session->writeText('List of dictionary armours (' . scalar @list . ' items)');

                    # Display list
                    $text = '';
                    foreach my $word (@list) {

                        if (! $text) {$text = $word} else {$text .= ', ' . $word}
                    }

                    $session->writeText($text);
                }
            }

            if ($switch eq '-e' || $switch eq '-z') {

                @list = sort {lc($a) cmp lc($b)} ($obj->ivKeys('garmentHash'));

                if (! @list) {

                    $session->writeText('List of dictionary garments (empty)');

                } else {

                    # Display header
                    $session->writeText('List of dictionary garments (' . scalar @list . ' items)');

                    # Display list
                    $text = '';
                    foreach my $word (@list) {

                        if (! $text) {$text = $word} else {$text .= ', ' . $word}
                    }

                    $session->writeText($text);
                }
            }

            if ($switch eq '-s' || $switch eq '-z') {

                @list = sort {lc($a) cmp lc($b)} ($obj->ivKeys('sentientHash'));

                if (! @list) {

                    $session->writeText('List of dictionary sentient beings (empty)');

                } else {

                    # Display header
                    $session->writeText(
                        'List of dictionary sentient beings (' . scalar @list . ' items)',
                    );

                    # Display list
                    $text = '';
                    foreach my $word (@list) {

                        if (! $text) {$text = $word} else {$text .= ', ' . $word}
                    }

                    $session->writeText($text);
                }
            }

            if ($switch eq '-k' || $switch eq '-z') {

                @list = sort {lc($a) cmp lc($b)} ($obj->ivKeys('creatureHash'));

                if (! @list) {

                    $session->writeText('List of dictionary creatures (empty)');

                } else {

                    # Display header
                    $session->writeText(
                        'List of dictionary creatures (' . scalar @list . ' items)',
                    );

                    # Display list
                    $text = '';
                    foreach my $word (@list) {

                        if (! $text) {$text = $word} else {$text .= ', ' . $word}
                    }

                    $session->writeText($text);
                }
            }

            if ($switch eq '-p' || $switch eq '-z') {

                %hash = $obj->portableTypeHash;
                @list = sort {lc($a) cmp lc($b)} (keys %hash);

                if (! @list) {

                    $session->writeText('List of dictionary portables (empty)');

                } else {

                    # Display header
                    $session->writeText(
                        'List of dictionary portables (' . scalar @list . ' items)',
                    );
                    $session->writeText('   (Portable)                       (Type)');

                    # Display list
                    foreach my $word (@list) {

                        $session->writeText(sprintf('   %-32.32s %-32.32s', $word, $hash{$word}));
                    }
                }
            }

            if ($switch eq '-q' || $switch eq '-z') {

                # Compile a hash of standard types, for quick checking
                @list = $obj->constPortableTypeList;
                %hash = ();
                foreach my $word (@list) {

                    $hash{$word} = undef;
                }

                # Get a sorted list of all types
                @list = sort {lc($a) cmp lc($b)} ($obj->portableTypeList);

                if (! @list) {

                    $session->writeText('List of dictionary portable types (empty)');

                } else {

                    # Display header
                    $session->writeText(
                        'List of dictionary portable types (' . scalar @list . ' items)'
                        . ' (* = standard type)',
                    );

                    # Display list
                    $text = '';
                    foreach my $word (@list) {

                        if (exists $hash{$word}) {
                            $word = '*' . $word;
                        }

                        if (! $text) {
                            $text = $word;
                        } else {
                            $text .= ', ' . $word;
                        }
                    }

                    $session->writeText($text);
                }
            }

            if ($switch eq '-d' || $switch eq '-z') {

                %hash = $obj->decorationTypeHash;
                @list = sort {lc($a) cmp lc($b)} (keys %hash);

                if (! @list) {

                    $session->writeText('List of dictionary decorations (empty)');

                } else {

                    # Display header
                    $session->writeText(
                        'List of dictionary decorations (' . scalar @list . ' items)',
                    );
                    $session->writeText('   (Decoration)                     (Type)');

                    # Display list
                    foreach my $word (@list) {

                        $session->writeText(sprintf('   %-32.32s %-32.32s', $word, $hash{$word}));
                    }
                }
            }

            if ($switch eq '-u' || $switch eq '-z') {

                # Compile a hash of standard types, for quick checking
                @list = $obj->constDecorationTypeList;
                %hash = ();
                foreach my $word (@list) {

                    $hash{$word} = undef;
                }

                # Get a sorted list of all types
                @list = sort {lc($a) cmp lc($b)} ($obj->decorationTypeList);

                if (! @list) {

                    $session->writeText('List of dictionary decoration types (empty)');

                } else {

                    # Display header
                    $session->writeText(
                        'List of dictionary decoration types (' . scalar @list . ' items)'
                        . ' (* = standard type)',
                    );

                    # Display list
                    $text = '';
                    foreach my $word (@list) {

                        if (exists $hash{$word}) {
                            $word = '*' . $word;
                        }

                        if (! $text) {
                            $text = $word;
                        } else {
                            $text .= ', ' . $word;
                        }
                    }

                    $session->writeText($text);
                }
            }

            if ($switch eq '-l' || $switch eq '-z') {

                %hash = $obj->pluralNounHash;
                @list = sort {lc($a) cmp lc($b)} (keys %hash);

                if (! @list) {

                    $session->writeText('List of dictionary plural nouns (empty)');

                } else {

                    # Display header
                    $session->writeText(
                        'List of dictionary plural nouns (' . scalar @list . ' items)',
                    );
                    $session->writeText('   (Singular)                       (Plural)');

                    # Display list
                    foreach my $word (@list) {

                        $session->writeText(sprintf('   %-32.32s %-32.32s', $word, $hash{$word}));
                    }
                }
            }

            if ($switch eq '-x' || $switch eq '-z') {

                %hash = $obj->pseudoNounHash;
                @list = sort {lc($a) cmp lc($b)} (keys %hash);

                if (! @list) {

                    $session->writeText('List of dictionary pseudo-nouns (empty)');

                } else {

                    # Display header
                    $session->writeText(
                        'List of dictionary pseudo-nouns (' . scalar @list . ' items)',
                    );
                    $session->writeText('   (Pseudo-noun)                    (Substitution)');

                    # Display list
                    foreach my $word (@list) {

                        $session->writeText(sprintf('   %-32.32s %-32.32s', $word, $hash{$word}));
                    }
                }
            }

            if ($switch eq '-j' || $switch eq '-z') {

                @list = sort {lc($a) cmp lc($b)} ($obj->ivKeys('adjHash'));

                if (! @list) {

                    $session->writeText('List of dictionary adjectives (empty)');

                } else {

                    # Display header
                    $session->writeText(
                        'List of dictionary adjectives (' . scalar @list . ' items)',
                    );

                    # Display list
                    $text = '';
                    foreach my $word (@list) {

                        if (! $text) {$text = $word} else {$text .= ', ' . $word}
                    }

                    $session->writeText($text);
                }
            }

            if ($switch eq '-c' || $switch eq '-z') {

                %hash = $obj->declinedAdjHash;
                @list = sort {lc($a) cmp lc($b)} (keys %hash);

                if (! @list) {

                    $session->writeText('List of dictionary declined adjectives (empty)');

                } else {

                    # Display header
                    $session->writeText(
                        'List of dictionary declined adjectives (' . scalar @list . ' items)',
                    );
                    $session->writeText('   (Declined adjective)             (Substitution)');

                    # Display list
                    foreach my $word (@list) {

                        $session->writeText(sprintf('   %-32.32s %-32.32s', $word, $hash{$word}));
                    }
                }
            }

            if ($switch eq '-y' || $switch eq '-z') {

                %hash = $obj->pseudoAdjHash;
                @list = sort {lc($a) cmp lc($b)} (keys %hash);

                if (! @list) {

                    $session->writeText('List of dictionary pseudo-adjectives (empty)');

                } else {

                    # Display header
                    $session->writeText(
                        'List of dictionary pseudo-adjectives (' . scalar @list . ' items)',
                    );
                    $session->writeText('   (Pseudo-adjective)               (Substitution)');

                    # Display list
                    foreach my $word (@list) {

                        $session->writeText(sprintf('   %-32.32s %-32.32s', $word, $hash{$word}));
                    }
                }
            }

            if ($switch eq '-v' || $switch eq '-z') {

                %hash = $obj->pseudoObjHash;
                @list = sort {lc($a) cmp lc($b)} (keys %hash);

                if (! @list) {

                    $session->writeText('List of dictionary pseudo-objects (empty)');

                } else {

                    # Display header
                    $session->writeText(
                        'List of dictionary pseudo-objects (' . scalar @list . ' items)',
                    );
                    $session->writeText('   (Pseudo-object)                  (Substitution)');

                    # Display list
                    foreach my $word (@list) {

                        $session->writeText(sprintf('   %-32.32s %-32.32s', $word, $hash{$word}));
                    }
                }
            }

            if ($switch eq '-i' || $switch eq '-z') {

                @list = sort {lc($a) cmp lc($b)} ($obj->ivKeys('ignoreWordHash'));

                if (! @list) {

                    $session->writeText('List of dictionary ignorable words (empty)');

                } else {

                    # Display header
                    $session->writeText('List of ignorable words (' . scalar @list . ' items)');

                    # Display list
                    $text = '';
                    foreach my $word (@list) {

                        if (! $text) {$text = $word} else {$text .= ', ' . $word}
                    }

                    $session->writeText($text);
                }
            }

            if ($switch eq '-n' || $switch eq '-z') {

                %hash = $obj->numberHash;
                # Sort by value first, then by key
                @list = sort {
                    if ($hash{$a} == $hash{$b}) {
                        lc($a) cmp lc($b);
                    } else {
                        $hash{$a} <=> $hash{$b}
                    }
                } (keys %hash);

                if (! @list) {

                    $session->writeText('List of dictionary number words (empty)');

                } else {

                    # Display header
                    $session->writeText(
                        'List of dictionary number words (' . scalar @list . ' items)',
                    );
                    $session->writeText('   (Number)                         (Substitution)');

                    # Display list
                    foreach my $key (@list) {

                        $session->writeText(sprintf('   %-32.32s %-32.32s', $hash{$key}, $key));
                    }
                }
            }

            if ($switch eq '-t' || $switch eq '-z') {

                # (The keys in ->timeHash and ->timePluralHash, in a standard order)
                @list = qw(second minute hour day week month year decade century millennium);
                %hash = $obj->timeHash;
                %pluralHash = $obj->timePluralHash;

                if (! @list) {

                    $session->writeText('List of dictionary standard time words (empty)');

                } else {

                    # Display header
                    $session->writeText('List of dictionary standard time words (10)');
                    $session->writeText(
                        '   (Standard)       (Custom)                 (Custom plural)',
                    );

                    # Display list
                    foreach my $word (@list) {

                        $session->writeText(
                            sprintf(
                                '   %-16.16s %-24.24s %-24.24s',
                                $word,
                                $hash{$word},
                                $pluralHash{$word},
                            )
                        );
                    }
                }
            }

            if ($switch eq '-b' || $switch eq '-z') {

                %hash = $obj->clockDayHash;
                # Sort by value first, then by key
                @list = sort {
                    if ($hash{$a} == $hash{$b}) {
                        lc($a) cmp lc($b);
                    } else {
                        $hash{$a} <=> $hash{$b}
                    }
                } (keys %hash);

                if (! @list) {

                    $session->writeText('List of dictionary time of day phrases (empty)');

                } else {

                    # Display header
                    $session->writeText(
                        'List of dictionary time of day phrases (' . scalar @list . ' items)');
                    $session->writeText('   (Time of day)                    (Substitution)');

                    # Display list
                    foreach my $word (@list) {

                        $session->writeText(sprintf('   %-32.32s %-32.32s', $word, $hash{$word}));
                    }
                }
            }

            if ($switch eq '-f' || $switch eq '-z') {

                %hash = $obj->clockHourHash;
                # Sort by value, then by key
                @list = sort {
                    if ($hash{$a} == $hash{$b}) {
                        lc($a) cmp lc($b);
                    } else {
                        $hash{$a} <=> $hash{$b}
                    }
                } (keys %hash);

                if (! @list) {

                    $session->writeText('List of dictionary clock hour phrases (empty)');

                } else {

                    # Display header
                    $session->writeText(
                        'List of dictionary clock hour phrases (' . scalar @list . ' items)',
                    );
                    $session->writeText('   (Clock hour)                     (Substitution)');

                    # Display list
                    foreach my $word (@list) {

                        $session->writeText(sprintf('   %-32.32s %-32.32s', $word, $hash{$word}));
                    }
                }
            }

            if ($switch eq '-m' || $switch eq '-z') {

                %hash = $obj->clockDayHash;
                # Sort by value, then by key
                @list = sort {
                    if ($hash{$a} == $hash{$b}) {
                        lc($a) cmp lc($b);
                    } else {
                        $hash{$a} <=> $hash{$b}
                    }
                } (keys %hash);

                if (! @list) {

                    $session->writeText('List of dictionary clock minute phrases (empty)');

                } else {

                    # Display header
                    $session->writeText(
                        'List of dictionary clock minute phrases (' . scalar @list . ' items)');
                    $session->writeText('   (Clock minute)                   (Substitution)');

                    # Display list
                    foreach my $word (@list) {

                        $session->writeText(sprintf('   %-32.32s %-32.32s', $word, $hash{$word}));
                    }
                }
            }
        }

        # Display footer
        return $self->complete($session, $standardCmd, 'Dictionary list(s) complete');
    }
}

{ package Games::Axmud::Cmd::ModifyPrimary;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('modifyprimary', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mpr', 'modprimary', 'modifyprimary'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies a primary direction';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $standard, $custom, $abbrev,
            $check,
        ) = @_;

        # Local variables
        my (
            $dictObj,
            %checkHash,
        );

        # Check for improper arguments
        if (! defined $standard || ! defined $custom || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Use 'undef' rather than an empty string
        if (defined $abbrev && $abbrev eq '') {

            $abbrev = undef;
        }

        # Check there is a current dictionary
        $dictObj = $session->currentDict;
        if (! $dictObj) {

            return $self->error(
                $session, $inputString,
                'Can\'t modify primary directions because there is no current dictionary for this'
                . ' session',
            );
        }

        # Check that the standard primary direction $standard exists
        if (! $axmud::CLIENT->constOppDirHash('standard')) {

            return $self->error(
                $session, $inputString,
                '\'' . $standard . '\' isn\'t a standard primary direction',
            );
        }

        # $custom must not already exist anywhere in the dictionary (unless it's the entry
        #   corresponding to $standard, in which case this function updates its abbreviated
        #   direction)
        foreach my $key ($dictObj->ivKeys('primaryDirHash')) {

            my $value = $dictObj->ivShow('primaryDirHash', $key);

            $checkHash{$value} = $key;
        }

        foreach my $key ($dictObj->ivKeys('primaryAbbrevHash')) {

            my $value = $dictObj->ivShow('primaryDirHash', $key);

            $checkHash{$value} = $key;
        }

        foreach my $key ($dictObj->ivKeys('secondaryDirHash')) {

            $checkHash{$key} = $key;
        }

        foreach my $key ($dictObj->ivKeys('secondaryAbbrevHash')) {

            $checkHash{$key} = $key;
        }

        foreach my $key ($dictObj->ivKeys('relativeDirHash')) {

            my $value = $dictObj->ivShow('relativeDirHash', $key);

            $checkHash{$value} = undef;
        }

        foreach my $key ($dictObj->ivKeys('relativeAbbrevHash')) {

            my $value = $dictObj->ivShow('relativeAbbrevHash', $key);

            $checkHash{$value} = undef;
        }

        if (exists $checkHash{$custom} && $checkHash{$custom} ne $standard) {

            return $self->error(
                $session, $inputString,
                'The direction \'' . $custom . '\' already exists in the current dictionary as a'
                . ' primary, secondary or relative direction (perhaps abbreviated)',
            );

        } elsif (
            defined $abbrev
            && exists $checkHash{$abbrev}
            && $checkHash{$custom} ne $standard
        ) {
            return $self->error(
                $session, $inputString,
                'The abbreviated direction \'' . $abbrev . '\' already exists in the current'
                . ' dictionary as a primary, secondary or relative direction (perhaps abbreviated)',
            );
        }

        # Update the dictonary
        if (! $dictObj->modifyPrimaryDir($standard, $custom, $abbrev)) {

            return $self->error(
                $session, $inputString,
                'Unable to update the current dictionary (internal error)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Added the custom primary direction \'' . $custom . '\' to the current dictionary',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddSecondary;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addsecondary', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ads', 'addsecond', 'addsecondary'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a secondary direction';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $custom, $abbrev,
            $check,
        ) = @_;

        # Local variables
        my (
            $dictObj,
            %checkHash,
        );

        # Check for improper arguments
        if (! defined $custom || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Use 'undef' rather than an empty string
        if (defined $abbrev && $abbrev eq '') {

            $abbrev = undef;
        }

        # Check there is a current dictionary
        $dictObj = $session->currentDict;
        if (! $dictObj) {

            return $self->error(
                $session, $inputString,
                'Can\'t add secondary directions because there is no current dictionary for this'
                . ' session',
            );
        }

        # $custom must not already exist anywhere in the dictionary
        foreach my $key ($dictObj->ivKeys('primaryDirHash')) {

            my $value = $dictObj->ivShow('primaryDirHash', $key);

            $checkHash{$value} = undef;
        }

        foreach my $key ($dictObj->ivKeys('primaryAbbrevHash')) {

            my $value = $dictObj->ivShow('primaryDirHash', $key);

            $checkHash{$value} = undef;
        }

        foreach my $key ($dictObj->ivKeys('secondaryDirHash')) {

            $checkHash{$key} = undef;
        }

        foreach my $key ($dictObj->ivKeys('secondaryAbbrevHash')) {

            $checkHash{$key} = undef;
        }

        foreach my $key ($dictObj->ivKeys('relativeDirHash')) {

            my $value = $dictObj->ivShow('relativeDirHash', $key);

            $checkHash{$value} = undef;
        }

        foreach my $key ($dictObj->ivKeys('relativeAbbrevHash')) {

            my $value = $dictObj->ivShow('relativeAbbrevHash', $key);

            $checkHash{$value} = undef;
        }

        if (exists $checkHash{$custom}) {

            return $self->error(
                $session, $inputString,
                'The direction \'' . $custom . '\' already exists in the current dictionary as a'
                . ' primary, secondary or relative direction (perhaps abbreviated)',
            );

        } elsif (defined $abbrev && exists $checkHash{$abbrev}) {

            return $self->error(
                $session, $inputString,
                'The abbreviated direction \'' . $abbrev . '\' already exists in the current'
                . ' dictionary as a primary, secondary or relative direction (perhaps abbreviated)',
            );
        }

        # Update the dictonary
        if (! $dictObj->addSecondaryDir($custom, $abbrev)) {

            return $self->error(
                $session, $inputString,
                'Unable to update the current dictionary (internal error)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Added the custom secondary direction \'' . $custom
                . '\' to the current dictionary',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ModifySecondary;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('modifysecondary', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mds', 'modsecond', 'modifysecondary'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets a secondary direction\'s opposite direction';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $custom, $opp,
            $check,
        ) = @_;

        # Local variables
        my $dictObj;

        # Check for improper arguments
        if (! defined $custom || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Use an empty string rather than 'undef'
        if (! defined $opp) {

            $opp = '';
        }

        # Check there is a current dictionary
        $dictObj = $session->currentDict;
        if (! $dictObj) {

            return $self->error(
                $session, $inputString,
                'Can\'t modify secondary directions because there is no current dictionary for this'
                . ' session',
            );
        }

        # Check that both $custom and $opp exist as custom secondary directions in the dictionary
        if (! $dictObj->ivExists('secondaryDirHash', $custom)) {

            return $self->error(
                $session, $inputString,
                'The custom secondary direction \'' . $custom . '\' doesn\'t exist in the current'
                . ' dictionary (see the help for \';addsecondary\')',
            );

        } elsif (defined $opp && ! $dictObj->ivExists('secondaryDirHash', $opp)) {

            return $self->error(
                $session, $inputString,
                'The custom secondary direction \'' . $opp . '\' doesn\'t exist in the current'
                . ' dictionary (see the help for \';addsecondary\')',
            );
        }

        # Update the dictonary
        if (! $dictObj->modifySecondaryDir($custom, $opp)) {

            return $self->error(
                $session, $inputString,
                'Unable to update the current dictionary (internal error)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Set the opposite of the custom secondary direction \'' . $custom
                . '\' in the current dictionary',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteSecondary;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletesecondary', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dds', 'delsecond', 'deletesecondary'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a secondary direction';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $custom,
            $check,
        ) = @_;

        # Local variables
        my $dictObj;

        # Check for improper arguments
        if (! defined $custom || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there is a current dictionary
        $dictObj = $session->currentDict;
        if (! $dictObj) {

            return $self->error(
                $session, $inputString,
                'Can\'t delete secondary directions because there is no current dictionary for this'
                . ' session',
            );
        }

        # Check that $custom exists as a custom secondary direction in the dictionary
        if (! $dictObj->ivExists('secondaryDirHash', $custom)) {

            return $self->error(
                $session, $inputString,
                'The custom secondary direction \'' . $custom . '\' doesn\'t exist in the current'
                . ' dictionary (see the help for \';addsecondary\')',
            );
        }

        # Update the dictonary
        if (! $dictObj->deleteSecondaryDir($custom)) {

            return $self->error(
                $session, $inputString,
                'Unable to update the current dictionary (internal error)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Deleted the custom secondary direction \'' . $custom
                . '\' in the current dictionary',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddRelative;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addrelative', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['arl', 'addrel', 'addrelative'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a relative direction';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $type, $dir, $abbrev,
            $check,
        ) = @_;

        # Local variables
        my (
            $dictObj,
            %checkHash, %standardHash,
        );

        # Check for improper arguments
        if (! defined $type || ! defined $dir || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Use 'undef' rather than an empty string
        if (defined $abbrev && $abbrev eq '') {

            $abbrev = undef;
        }

        # Check there is a current dictionary
        $dictObj = $session->currentDict;
        if (! $dictObj) {

            return $self->error(
                $session, $inputString,
                'Can\'t add relative directions because there is no current dictionary for this'
                . ' session',
            );
        }

        # $type should be an integer in the range 0-7, but it can also be one of a subset of
        #   standard or custom primary direction (namely, n/ne/e/se/s/sw/w/nw)
        # If $type is a direction, convert it into an integer
        if (! $axmud::CLIENT->intCheck($type, 0, 7)) {

            %standardHash = (
                'north'     => 0,
                'northeast' => 0,
                'east'      => 0,
                'southeast' => 0,
                'south'     => 0,
                'southwest' => 0,
                'west'      => 0,
                'northwest' => 0,
            );

            # $type can be a standard or custom primary direction; convert it into the standard form
            $type = $dictObj->convertStandardDir($type);
            if (! defined $type || ! exists $standardHash{$type}) {

                return $self->error(
                    $session, $inputString,
                    'You must specify (as the first argument) a standard primary direction like'
                    . ' \'north\' or \'southeast\' (but not \'up\', \'down\', \'northnortheast\''
                    . ' etc), or an integer equivalent in the range 0-7 (moving clockwise from 0,'
                    . ' representing \'north\')',
                );
            }

            # Convert the standard form to an integer
            $type = $standardHash{$type};
        }

        # $custom must not already exist anywhere in the dictionary
        foreach my $key ($dictObj->ivKeys('primaryDirHash')) {

            my $value = $dictObj->ivShow('primaryDirHash', $key);

            $checkHash{$value} = undef;
        }

        foreach my $key ($dictObj->ivKeys('primaryAbbrevHash')) {

            my $value = $dictObj->ivShow('primaryDirHash', $key);

            $checkHash{$value} = undef;
        }

        foreach my $key ($dictObj->ivKeys('secondaryDirHash')) {

            $checkHash{$key} = undef;
        }

        foreach my $key ($dictObj->ivKeys('secondaryAbbrevHash')) {

            $checkHash{$key} = undef;
        }

        foreach my $key ($dictObj->ivKeys('relativeDirHash')) {

            my $value = $dictObj->ivShow('relativeDirHash', $key);

            $checkHash{$value} = undef;
        }

        foreach my $key ($dictObj->ivKeys('relativeAbbrevHash')) {

            my $value = $dictObj->ivShow('relativeAbbrevHash', $key);

            $checkHash{$value} = undef;
        }

        if (exists $checkHash{$dir}) {

            return $self->error(
                $session, $inputString,
                'The direction \'' . $dir . '\' already exists in the current dictionary as a'
                . ' primary, secondary or relative direction (perhaps abbreviated)',
            );

        } elsif (defined $abbrev && exists $checkHash{$abbrev}) {

            return $self->error(
                $session, $inputString,
                'The abbreviated direction \'' . $abbrev . '\' already exists in the current'
                . ' dictionary as a primary, secondary or relative direction (perhaps abbreviated)',
            );
        }

        # Update the dictonary
        if (! $dictObj->addRelativeDir($type, $dir, $abbrev)) {

            return $self->error(
                $session, $inputString,
                'Unable to update the current dictionary (internal error)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Added the relative direction \'' . $dir . '\' to the current dictionary at'
                . ' position ' . $type,
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteRelative;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleterelative', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['drl', 'delrel', 'deleterelative'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a relative direction';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my ($dictObj, $index, $result, $string);

        # Check for improper arguments
        if (! defined $arg || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there is a current dictionary
        $dictObj = $session->currentDict;
        if (! $dictObj) {

            return $self->error(
                $session, $inputString,
                'Can\'t delete relative directions because there is no current dictionary for this'
                . ' session',
            );
        }

        # $arg can be a relative direction (one of the values in $dictObj->relativeDirHash or
        #   ->relativeAbbrevHash), or it can be an integer in the range 0-7, or it can be one of a
        #   subset of standard or custom primary direction (namely, n/ne/e/se/s/sw/w/nw)
        $index = $dictObj->convertRelativeDir($arg);
        if ($axmud::CLIENT->intCheck($arg, 0, 7)) {

            $result = $dictObj->deleteRelativeDir($arg);
            $string = $dictObj->ivShow('relativeDirHash', $arg);

        } elsif (defined $index) {

            $result = $dictObj->deleteRelativeDir($index);
            $string = $arg;

        } else {

            $result = $dictObj->deleteRelativeDir(undef, $arg);
            $string = $arg;
        }

        if (! $result) {

            return $self->error(
                $session, $inputString,
                'Unable to update the current dictionary (internal error)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Deleted the relative direction direction \'' . $string
                . '\' in the current dictionary',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListDirection;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listdirection', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ldi', 'listdirection'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists primary/secondary directions';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $dictObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there is a current dictionary
        $dictObj = $session->currentDict;
        if (! $dictObj) {

            return $self->error(
                $session, $inputString,
                'Can\'t list secondary directions because there is no current dictionary for this'
                . ' session',
            );
        }

        # Display header
        $session->writeText('List of primary directions in dictionary');
        $session->writeText(
            '   Standard         Custom           Abbreviation     Opposite         Opp. abbrev.',
        );

        # Display list
        foreach my $standard ($axmud::CLIENT->constPrimaryDirList) {

            $session->writeText(
                sprintf(
                    '   %-16.16s %-16.16s %-16.16s %-16.16s %-16.16s',
                    $standard,
                    $dictObj->ivShow('primaryDirHash', $standard),
                    $dictObj->ivShow('primaryAbbrevHash', $standard),
                    $dictObj->ivShow('primaryOppHash', $standard),
                    $dictObj->ivShow('primaryOppAbbrevHash', $standard),
                ),
            );
        }

        # Display header
        $session->writeText('List of secondary directions (no standard forms exist)');
        $session->writeText(
            '                    Custom           Abbreviation     Opposite         Opp. abbrev.',
        );

        # Display list
        foreach my $custom ($dictObj->secondaryDirList) {

            my ($abbrev, $oppAbbrev);

            $abbrev = $dictObj->ivShow('secondaryAbbrevHash', $custom);
            if (! defined $abbrev || $abbrev eq '') {

                $abbrev = '<not set>';
            }

            $oppAbbrev = $dictObj->ivShow('secondaryOppAbbrevHash', $custom);
            if (! defined $oppAbbrev || $oppAbbrev eq '') {

                $oppAbbrev = '<not set>';
            }

            $session->writeText(
                sprintf(
                    '                    %-16.16s %-16.16s %-16.16s %-16.16s',
                    $dictObj->ivShow('secondaryDirHash', $custom),
                    $abbrev,
                    $dictObj->ivShow('secondaryOppHash', $custom),
                    $oppAbbrev,
                ),
            );
        }

        # Display header
        $session->writeText('List of relative directions');
        $session->writeText(
            '   Slot             Direction        Abbreviation     Opposite         Opp. abbrev.',
        );

        # Display list
        for (my $index = 0; $index < 8; $index++) {

            my ($dir, $abbrev, $oppDir, $oppAbbrev, $oppIndex);

            $dir = $dictObj->ivShow('relativeDirHash', $index);
            if (! defined $dir || $dir eq '') {

                $dir = '<not set>';
            }

            $abbrev = $dictObj->ivShow('relativeAbbrevHash', $index);
            if (! defined $abbrev || $abbrev eq '') {

                $abbrev = '<not set>';
            }

            # There are eight slots in a circle, so $index + 4 is the opposite slot on that circle
            $oppIndex = $index + 4;
            if ($oppIndex > 7) {

                $oppIndex -= 8;
            }

            $oppDir = $dictObj->ivShow('relativeDirHash', $oppIndex);
            if (! defined $oppDir || $oppDir eq '') {

                $oppDir = '<not set>';
            }

            $oppAbbrev = $dictObj->ivShow('relativeAbbrevHash', $oppIndex);
            if (! defined $oppAbbrev || $oppAbbrev eq '') {

                $oppAbbrev = '<not set>';
            }

            $session->writeText(
                sprintf(
                    '   %-16.16s %-16.16s %-16.16s %-16.16s %-16.16s',
                    $index,
                    $dir,
                    $abbrev,
                    $oppDir,
                    $oppAbbrev,
                ),
            );
        }

        # Display footer
        return $self->complete(
            $session, $standardCmd,
            'End of lists (primary directions: ' . scalar ($axmud::CLIENT->constPrimaryDirList)
            . ', secondary directions: ' . scalar ($dictObj->secondaryDirList)
            . ', relative directions: 8)',
        );
    }
}

{ package Games::Axmud::Cmd::SetAutoSecondary;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setautosecondary', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sas', 'setsecond', 'setautosecond', 'setautosecondary'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets preferred way to draw secondary directions';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $custom, $standard,
            $check,
        ) = @_;

        # Local variables
        my $dictObj;

        # Check for improper arguments
        if (! defined $custom || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there is a current dictionary
        $dictObj = $session->currentDict;
        if (! $dictObj) {

            return $self->error(
                $session, $inputString,
                'Can\'t modify secondary directions because there is no current dictionary for this'
                . ' session',
            );
        }

        # Check that $custom exists as a custom secondary direction in the dictionary
        if (! $dictObj->ivExists('secondaryDirHash', $custom)) {

            return $self->error(
                $session, $inputString,
                'The custom secondary direction \'' . $custom . '\' doesn\'t exist in the current'
                . ' dictionary (see the help for \';addsecondary\')',
            );

        # If $auto was specified, check it's a standard priamry direction
        } elsif (defined $standard && ! $axmud::CLIENT->ivExists('constOppDirHash', $standard)) {

            return $self->error(
                $session, $inputString,
                'The standard primary direction \'' . $standard . '\' doesn\'t exist',
            );
        }

        # Update the dictonary
        $dictObj->ivPoke('secondaryAutoHash', $custom, $standard);

        if (! defined $standard) {

            return $self->complete(
                $session, $standardCmd,
                'Set the custom secondary direction \'' . $custom . '\' to be drawn in no specified'
                . ' standard primary direction',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Set the custom secondary direction \'' . $custom . '\' to be drawn in the'
                . ' standard primary direction \'' . $standard . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListAutoSecondary;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listautosecondary', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['las', 'listsecond', 'listautosecond', 'listautosecondary'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists preferred ways to draw secondary directions';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my ($dictObj, $count);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there is a current dictionary
        $dictObj = $session->currentDict;
        if (! $dictObj) {

            return $self->error(
                $session, $inputString,
                'Can\'t list secondary directions because there is no current dictionary for this'
                . ' session',
            );
        }

        # Display header
        $session->writeText('List of preferred map directions for secondary directions');
        $session->writeText('   Secondary dir    Drawn in primary direction');

        # Display list
        foreach my $dir ($dictObj->secondaryDirList) {

            my $standard = $dictObj->ivShow('secondaryAutoHash', $dir);

            if (! defined $standard) {
                $session->writeText(sprintf('   %-16.16s <none>', $dir));
            } else {
                $session->writeText(sprintf('   %-16.16s %-16.16s', $dir, $standard));
            }
        }

        # Display footer
        $count = scalar $dictObj->secondaryDirList;
        if ($count == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 secondary direction found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . $count . ' secondary directions found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddSpeedWalk;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addspeedwalk', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['awk', 'addspeed', 'addspeedwalk'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a speedwalk character';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $char, $moveCmd,
            $check,
        ) = @_;

        # Local variables
        my $dictObj;

        # Check for improper arguments
        if (! defined $char || ! defined $moveCmd || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there is a current dictionary to which speedwalk characters can be added
        if (! $session->currentDict) {

            return $self->error(
                $session, $inputString,
                'Can\'t add speedwalk characters because there is no current dictionary for this'
                . ' session',
            );

        } else {

            $dictObj = $session->currentDict;
        }

        # Check $char is valid (converting to lower case, if need be)
        $char = lc($char);
        if (! $char =~ m/^[[:lower:]]$/) {

            return $self->error(
                $session, $inputString,
                'The speedwalk character must be a letter in the range a-z (non-Latin alphabets'
                . ' acceptable)',
            );
        }

        # Add the speedwalk character
        $dictObj->ivAdd('speedDirHash', $char, $moveCmd);

        return $self->complete(
            $session, $standardCmd,
            'Added speedwalk character \'' . $char . '\' representing the movement command \''
            . $moveCmd . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::DeleteSpeedWalk;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletespeedwalk', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dwk', 'delspeed', 'deletespeed', 'deletespeedwalk'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a speedwalk character';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $char,
            $check,
        ) = @_;

        # Local variables
        my $dictObj;

        # Check for improper arguments
        if (! defined $char || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there is a current dictionary from which speedwalk modifier characters can be
        #   deleted
        if (! $session->currentDict) {

            return $self->error(
                $session, $inputString,
                'Can\'t delete speedwalk characters because there is no current dictionary for this'
                . ' session',
            );

        } else {

            $dictObj = $session->currentDict;
        }

        # Check the speedwalk character exists in the dictionary's hash (for convenience, convert to
        #   lower case)
        $char = lc($char);
        if (! $dictObj->ivExists('speedDirHash', $char)) {

            return $self->error(
                $session, $inputString,
                'The speedwalk character \'' . $char . '\' doesn\'t exist in the current'
                . 'dictionary',
            );
        }

        # Remove the speedwalk character
        $dictObj->ivDelete('speedDirHash', $char);

        return $self->complete(
            $session, $standardCmd,
            'Removed speedwalk character \'' . $char . '\' from the current dictionary',
        );
    }
}

{ package Games::Axmud::Cmd::ListSpeedWalk;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listspeedwalk', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lwk', 'listspeed', 'listspeedwalk'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists speedwalk characters';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my ($dictObj, $num);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there is a current dictionary from which speedwalk characters can be listed
        if (! $session->currentDict) {

            return $self->error(
                $session, $inputString,
                'Can\'t list speedwalk characters because there is no current dictionary for this'
                . ' session',
            );

        } else {

            $dictObj = $session->currentDict;
        }

        # Display header
        $session->writeText('List of speedwalk characters');

        # Display list
        foreach my $char (sort {$a cmp $b} ($dictObj->ivKeys('speedDirHash'))) {

            $session->writeText('   ' . $char . '  ' . $dictObj->ivShow('speedDirHash', $char));
        }

        # Display footer
        $num = $dictObj->ivPairs('speedDirHash');
        if ($num == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 character found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . $num . ' characters found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddModifierChar;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addmodifierchar', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['amc', 'addmod', 'addmodchar', 'addmodifierchar'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a speedwalk modifier character';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $char, $cageCmd,
            $check,
        ) = @_;

        # Local variables
        my ($dictObj, $cageObj);

        # Check for improper arguments
        if (! defined $char || ! defined $cageCmd || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there is a current dictionary to which speedwalk modifier characters can be added
        if (! $session->currentDict) {

            return $self->error(
                $session, $inputString,
                'Can\'t add speedwalk modifier characters because there is no current dictionary'
                . ' for this session',
            );

        } else {

            $dictObj = $session->currentDict;
        }

        # Check $char is valid (converting to upper case, if need be)
        $char = uc($char);
        if (! $char =~ m/^[[:upper:]]$/) {

            return $self->error(
                $session, $inputString,
                'The speedwalk character must be a letter in the range A-Z (non-Latin alphabets'
                . ' acceptable)',
            );
        }

        # Check the standard command actually exists
        $cageObj = $session->findHighestCage('cmd');
        if (! $cageObj->ivExists('cmdHash', $cageCmd)) {

            return $self->error(
                $session, $inputString,
                'The standard command \'' . $cageCmd . '\' doesn\'t exist in command cages',
            );
        }

        # Add the speedwalk modifier character
        $dictObj->ivAdd('speedModifierHash', $char, $cageCmd);

        return $self->complete(
            $session, $standardCmd,
            'Added speedwalk modifier character \'' . $char . '\' representing the standard'
            . ' command \'' . $cageCmd . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::DeleteModifierChar;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletemodifierchar', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dmc', 'delmod', 'delmodchar', 'deletemodifierchar'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a speedwalk modifier character';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $char,
            $check,
        ) = @_;

        # Local variables
        my $dictObj;

        # Check for improper arguments
        if (! defined $char || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there is a current dictionary to which speedwalk characters can be added
        if (! $session->currentDict) {

            return $self->error(
                $session, $inputString,
                'Can\'t delete speedwalk modifier characters because there is no current dictionary'
                . ' for this session',
            );

        } else {

            $dictObj = $session->currentDict;
        }

        # Check the speedwalk modifier character exists in the dictionary's hash (for convenience,
        #   convert to upper case)
        $char = uc($char);
        if (! $dictObj->ivExists('speedModifierHash', $char)) {

            return $self->error(
                $session, $inputString,
                'The speedwalk modifier character \'' . $char . '\' doesn\'t exist in the current'
                . 'dictionary',
            );
        }

        # Remove the speedwalk modifier character
        $dictObj->ivDelete('speedModifierHash', $char);

        return $self->complete(
            $session, $standardCmd,
            'Removed speedwalk modifier character \'' . $char . '\' from the current dictionary',
        );
    }
}

{ package Games::Axmud::Cmd::ListModifierChar;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listmodifierchar', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lmc', 'listmod', 'listmodchar', 'listmodifierchar'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists speedwalk modifier characters';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my ($dictObj, $num);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there is a current dictionary from which speedwalk modifier characters can be listed
        if (! $session->currentDict) {

            return $self->error(
                $session, $inputString,
                'Can\'t list speedwalk modifier characters because there is no current dictionary'
                . ' for this session',
            );

        } else {

            $dictObj = $session->currentDict;
        }

        # Display header
        $session->writeText('List of speedwalk modifier characters');
        $session->writeText('   Char Standard command   Current replacement command');

        # Display list
        foreach my $char (sort {$a cmp $b} ($dictObj->ivKeys('speedModifierHash'))) {

            my ($standard, $replace);

            $standard = $dictObj->ivShow('speedModifierHash', $char);
            $replace = $session->findCmd($standard);
            if (! $replace) {

                $replace = '(not set)';
            }

            $session->writeText(
                sprintf('   %-4.4s %-18.18s', $char, $standard)
                . ' ' . $replace,
            );
        }

        # Display footer
        $num = $dictObj->ivPairs('speedModifierHash');
        if ($num == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 character found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . $num . ' characters found)',
            );
        }
    }
}

# Profiles - general

{ package Games::Axmud::Cmd::ListProfile;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listprofile', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lpr', 'listprof', 'listprofile'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists profiles';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if ((defined $switch && $switch ne '-c' && $switch ne '-w') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;lpr
        if (! $switch) {

            # Import a list of profiles, sorted by name
            @list = sort {lc($a->name) cmp lc($b->name)} ($session->ivValues('profHash'));
            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'The profile list for this world is empty',
                );
            }

            # Display header
            $session->writeText(
                'List of profiles for this world (* = current profile)',
            );
            $session->writeText('   Profile name     Category');

            # Display list
            foreach my $profObj (@list) {

                my $string;

                if (
                    $session->ivExists('currentProfHash', $profObj->category)
                    && $session->ivShow('currentProfHash', $profObj->category) eq $profObj
                )  {
                    $string = ' * ';

                } else {

                    $string = '   ';
                }

                $session->writeText(
                    $string . sprintf('%-16.16s', $profObj->name) . ' ' . $profObj->category,
                );
            }

        # ;lpr -c
        } elsif ($switch eq '-c') {

            # Import a list of profiles, sorted by name
            @list = sort {lc($a->name) cmp lc($b->name)} ($session->ivValues('currentProfHash'));
            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'The current profile list for this world is empty',
                );
            }

            # Display header
            $session->writeText('List of current profiles for this world');
            $session->writeText('   Profile name     Category');

            # Display list
            foreach my $profObj (@list) {

                $session->writeText(
                    '   ' . sprintf('%-16.16s', $profObj->name) . ' ' . $profObj->category,
                );
            }

        # ;lpr -w
        } elsif ($switch eq '-w') {

            # Import a list of all world profiles, sorted by name
            @list = sort {lc($a->name) cmp lc($b->name)}
                ($axmud::CLIENT->ivValues('worldProfHash'));

            if (! @list) {

                return $self->complete($session, $standardCmd, 'The world profile list is empty');
            }

            # Display header
            $session->writeText('List of all world profiles (* - used in this session)');
            $session->writeText(
                '   Profile name     Category         Guild Races Chars Others  Dictionary');

            # Display list
            foreach my $profObj (@list) {

                my ($guildCount, $raceCount, $charCount, $customCount, $dict, $column);

                $guildCount = scalar $profObj->findProfiles('guild');
                $raceCount = scalar $profObj->findProfiles('race');
                $charCount = scalar $profObj->findProfiles('char');
                $customCount = scalar $profObj->findProfiles();

                if ($profObj->dict) {
                    $dict = $profObj->dict;
                } else {
                    $dict = '<no dictionary>';      # Very unlikely
                }

                if (defined $session->currentWorld && $session->currentWorld eq $profObj) {
                    $column = ' * ';
                } else {
                    $column = '   ';
                }

                $session->writeText(
                    $column . sprintf(
                    '%-16.16s %-16.16s %-5.5s %-5.5s %-5.5s %-5.5s %-16.16s',
                        $profObj->name,
                        $profObj->category,
                        $guildCount,
                        $raceCount,
                        $charCount,
                        $customCount,
                        $dict,
                    ),
                );
            }
        }

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 profile found)');

        } else {

            return $self->complete(
            $session, $standardCmd,
            'End of list (' . scalar @list . ' profiles found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetProfilePriority;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setprofilepriority', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['spp', 'setpriority', 'setprofilepriority'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the profile priority list';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @list,
        ) = @_;

        # Local variables
        my %hash;

        # (No improper arguments to check)

        # ;spp
        if (! @list) {

            # Reset the profile priority list for this session
            $session->set_profPriorityList($axmud::CLIENT->constProfPriorityList);
            # Reset cages and their inferior cages according to the new priority list
            $session->setCageInferiors();

            return $self->complete(
                $session, $standardCmd,
                'Profile priority list for this session reset',
            );

        # ;spp <list>
        } else {

             # The last item on the list must be 'world'
            if ($list[-1] ne 'world') {

                return $self->error(
                    $session, $inputString,
                    'Can\'t set profile priority list - the last category on the list must be'
                    . ' \'world\'',
                );
            }

            # Go through the list, adding each item to a hash to check there are no repeating items,
            #   and that each category is valid (one of 'world', 'guild', 'race', 'char' or a
            #   custom category matching an existing profile template)
            OUTER: foreach my $item (@list) {

                my $matchFlag;

                # See if it's a standard profile category
                INNER: foreach my $standard ($axmud::CLIENT->constProfPriorityList) {

                    if ($standard eq $item) {

                        $matchFlag = TRUE;
                        last INNER;
                    }
                }

                # See if it's a profile template
                if (! $matchFlag && ! $session->ivExists('templateHash', $item)) {

                    return $self->error(
                        $session, $inputString,
                        'Can\'t set profile priority list - the item \'' . $item . '\' isn\'t a'
                        . ' standard category of profile, nor is it an existing profile template',
                    );
                }

                # See if the item has already appeared earlier in the list
                if (exists $hash{$item}) {

                    return $self->error(
                        $session, $inputString,
                        'Can\'t set profile priority list - the item \'' . $item . '\' appears at'
                        . ' least twice (no items should appear more than once)',
                    );
                }

                # Mark this item as appearing for the first time in @list
                $hash{$item} = undef;
            }

            # Does 'char' appear in the list?
            if (! exists $hash{'char'}) {

                return $self->error(
                    $session, $inputString,
                    'Can\'t set profile priority list - the category \'char\' must appear somewhere'
                    . ' in it',
                );
            }

            # Checking complete. Set the list
            $session->set_profPriorityList(@list);
            # Reset cages and their inferior cages according to the new priority list
            $session->setCageInferiors();

            return $self->complete(
                $session, $standardCmd,
                'Profile priority list set to \'' . join(' ', @list) . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListProfilePriority;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listprofilepriority', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lpp', 'listpriority', 'listprofilepriority'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows the profile priority list';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my $count;

        # Check for improper arguments
        if (($switch && $switch ne '-d') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;lpp
        if (! $switch) {

            if (! $session->profPriorityList) {

                return $self->complete(
                    $session, $standardCmd,
                    'The profile priority list is empty',
                );
            }

            # Display header
            $session->writeText('Current profile priority list (highest priority first):');

            # Display list
            $count = 0;
            foreach my $category ($session->profPriorityList) {

                $count++;
                $session->writeText('   ' . sprintf('%-4.4s', $count) . ' ' . $category);
            }

        # ;lpp -d
        } else {

            # Display header
            $session->writeText('Default profile priority list (highest priority first):');

            # Display list
            $count = 0;
            foreach my $category ($axmud::CLIENT->constProfPriorityList) {

                $count++;
                $session->writeText('   ' . sprintf('%-4.4s', $count) . ' ' . $category);
            }
        }

        # Display footer
        if ($count == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 category found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . $count . ' categories found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddTemplate;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addtemplate', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['atm', 'addtm', 'addtemplate'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a new profile template';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $category,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $category || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that <category> is a valid name
        if (! $axmud::CLIENT->nameCheck($category, 16)) {

            return $self->error(
                $session, $inputString,
                'Could not add ' . $category . ' profile template - invalid name',
            );
        }

        # Check that <category> isn't one of the standard profile categories ('world', 'guild',
        #   'race', 'char')...
        if (defined $axmud::CLIENT->ivFind('constProfPriorityList', $category)) {

            return $self->error(
                $session, $inputString,
                'Could not add ' . $category . ' profile template - \'' . $category
                . '\' is a standard profile category that doesn\'t use templates',
            );
        }

        # ...nor the <category> of an existing profile template
        if ($session->ivExists('templateHash', $category)) {

            return $self->error(
                $session, $inputString,
                'Could not add ' . $category . ' profile template - profile template already'
                . ' exists',
            );
        }

        # Create the new template
        $obj = Games::Axmud::Profile::Template->new($session, $category);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Could not add the profile template \'' . $category . '\'',
            );

        } else {

            # Update IVs
            $session->add_template($obj);

            return $self->complete(
                $session, $standardCmd,
                'Added the profile template \'' . $category . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::CloneTemplate;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('clonetemplate', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ctm', 'clonetm', 'clonetemplate'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Clones an existing profile template';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $original, $copy,
            $check,
        ) = @_;

        # Local variables
        my ($originalObj, $copyObj);

        # Check for improper arguments
        if (! defined $original || ! defined $copy || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that <category> isn't one of the standard profile categories ('world', 'guild',
        #   'race', 'char'), which don't exist as templates (so can't be cloned)
        if (defined $axmud::CLIENT->ivFind('constProfPriorityList', $original)) {

            return $self->error(
                $session, $inputString,
                'Could not clone ' . $original . ' profile template - \'' . $original
                . '\' is a standard profile category that doesn\'t use templates',
            );

        } elsif (defined $axmud::CLIENT->ivFind('constProfPriorityList', $copy)) {

            return $self->error(
                $session, $inputString,
                'Could not add ' . $copy . ' profile template - \'' . $copy
                . '\' is a standard profile category that doesn\'t use templates',
            );
        }

        # Check that <original> exists, and that <copy> doesn't
        if (! $session->ivExists('templateHash', $original)) {

            return $self->error(
                $session, $inputString,
                'Could not clone ' . $original . ' profile template - profile template doesn\'t'
                . ' exist',
            );

        } elsif ($session->ivExists('templateHash', $copy)) {

            return $self->error(
                $session, $inputString,
                'Could not add ' . $copy . ' profile template - profile template already exists',
            );

        } else {

            $originalObj = $session->ivShow('templateHash', $original);
        }

        # Check that <copy> is a valid name
        if (! $axmud::CLIENT->nameCheck($copy, 16)) {

            return $self->error(
                $session, $inputString,
                'Could not add ' . $copy . ' profile template - invalid name',
            );
        }

        # Create the template
        $copyObj = $originalObj->clone($session, $copy);
        if (! $copyObj) {

            return $self->error(
                $session, $inputString,
                'Could not clone the profile template \'' . $original . '\'',
            );

        } else {

            # Update IVs
            $session->add_template($copyObj);

            return $self->complete(
                $session, $standardCmd,
                'Created cloned ' . $original . ' profile template \'' . $copy . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditTemplate;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('edittemplate', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['etm', 'edittm', 'edittemplate'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a profile template';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $category,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $category || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that template exists
        if (! $session->ivExists('templateHash', $category)) {

            return $self->error(
                $session, $inputString,
                'Could not edit the profile template \'' . $category . '\' - a template of that'
                . ' category does not exist',
            );

        } else {

            $obj = $session->ivShow('templateHash', $category);
        }

        # Open an 'edit' window for the template
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Profile::Template',
                $session->mainWin,
                $session,
                'Edit \'' . $obj->category . '\' profile template',
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the profile template \'' . $category . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the profile template \'' . $category . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteTemplate;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletetemplate', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dtm', 'deltm', 'deletetm', 'deletetemplate'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a profile template';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $category,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $category || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that <category> isn't one of the standard profile categories ('world', 'guild',
        #   'race', 'char'), which don't exist as templates (so can't be deleted)
        if (defined $axmud::CLIENT->ivFind('constProfPriorityList', $category)) {

            return $self->error(
                $session, $inputString,
                'Could not delete ' . $category . ' profile template - \'' . $category
                . '\' is a standard profile category that doesn\'t use templates',
            );
        }

        # Check the template exists
        if (! $session->ivExists('templateHash', $category)) {

            return $self->error(
                $session, $inputString,
                'The ' . $category . ' profile template doesn\'t exist',
            );

        } else {

            $obj = $session->ivShow('templateHash', $category);
        }

        # Check that there are no profiles based on this template
        foreach my $profile ($session->ivValues('profHash')) {

            if ($profile->category eq $category) {

                return $self->error(
                    $session, $inputString,
                    'Could not delete ' . $category . ' profile template - one or more profiles'
                    . ' exist which are based on it; try deleting them first',
                );
            }
        }

        # Delete the template
        $session->del_template($obj);

        return $self->complete(
            $session, $standardCmd,
            'Deleted the profile template \'' . $category . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::ListTemplate;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listtemplate', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ltm', 'listtm', 'listtemplate'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists profile templates for the current world';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Compile a list of blessed references to profile templates, sorted by ->category
        @list = sort {lc($a->category) cmp lc($b->category)} ($session->ivValues('templateHash'));
        if (! @list) {

            return $self->complete(
                $session, $standardCmd,
                'The profile template list is empty',
            );
        }

        # Display header
        $session->writeText('List of profile templates (* = variables fixed)');

        # Display list
        foreach my $obj (@list) {

            my $string;

            if ($obj->constFixedFlag) {
                $string = ' * ';
            } else {
                $string = '   ';
            }

            $session->writeText($string . $obj->category);
        }

        # Display footer
        if (@list == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 profile template found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @list . ' profile templates found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddScalarProperty;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addscalarproperty', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['asp', 'addscalar', 'addscalarprop', 'addscalarproperty'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a new scalar property to a template';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $templ, $property, $value,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $templ || ! defined $property || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that <templ> isn't one of the standard profile categories ('world', 'guild',
        #   'race', 'char'), which don't exist as templates
        if (defined $axmud::CLIENT->ivFind('constProfPriorityList', $templ)) {

            return $self->error(
                $session, $inputString,
                'Could not modify ' . $templ . ' profile template - \'' . $templ
                . '\' is a standard profile category that doesn\'t use templates',
            );
        }

        # Check that <templ> exists
        if (! $session->ivExists('templateHash', $templ)) {

            return $self->error(
                $session, $inputString,
                'Could not modify ' . $templ . ' profile template - profile template doesn\'t'
                . ' exist',
            );

        } else {

            $obj = $session->ivShow('templateHash', $templ);
        }

        # Check that the property name isn't a template IV (as opposed to an IV that can be
        #   transferred into a custom profile)
        if ($axmud::CLIENT->ivExists('constProfStandardHash', $property)) {

            return $self->error(
                $session, $inputString,
                'Could not modify ' . $templ . ' profile template - the property \'' . $property
                . '\' is a reserved property that can\'t be manipulated',
            );
        }

        # Check that the template isn't already fixed (meaning that no new properties can be added
        #   or deleted)
        if ($obj->constFixedFlag) {

            return $self->error(
                $session, $inputString,
                'The profile template \'' . $templ . '\' has been fixed; no new properties can be'
                . ' added or deleted',
            );
        }

        # Add the property
        if (! $obj->createScalarProperty($property, $value)) {

            return $self->error(
                $session, $inputString,
                'Could not modify ' . $templ . ' profile template',
            );

        } else {

            if (! defined $value) {

                $value = 'undef';
            }

            return $self->complete(
                $session, $standardCmd,
                'Added scalar property \'' . $property . '\' with value \'' . $value . '\' to the'
                . ' profile template \'' . $templ . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddListProperty;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addlistproperty', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['alp', 'addlist', 'addlistprop', 'addlistproperty'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a new list property to a template';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $templ, $property, @list,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $templ || ! defined $property) {

            return $self->improper($session, $inputString);
        }

        # Check that <templ> isn't one of the standard profile categories ('world', 'guild',
        #   'race', 'char'), which don't exist as templates
        if (defined $axmud::CLIENT->ivFind('constProfPriorityList', $templ)) {

            return $self->error(
                $session, $inputString,
                'Could not modify ' . $templ . ' profile template - \'' . $templ
                . '\' is a standard profile category that doesn\'t use templates',
            );
        }

        # Check that <templ> exists
        if (! $session->ivExists('templateHash', $templ)) {

            return $self->error(
                $session, $inputString,
                'Could not modify ' . $templ . ' profile template - profile template doesn\'t'
                . ' exist',
            );

        } else {

            $obj = $session->ivShow('templateHash', $templ);
        }

        # Check that the property name isn't a template IV (as opposed to an IV that can be
        #   transferred into a custom profile)
        if ($axmud::CLIENT->ivExists('constProfStandardHash', $property)) {

            return $self->error(
                $session, $inputString,
                'Could not modify ' . $templ . ' profile template - the property \''
                . $property . '\' is a reserved property that can\'t be manipulated',
            );
        }

        # Check that the template isn't already fixed (meaning that no new properties can be added
        #   or deleted)
        if ($obj->constFixedFlag) {

            return $self->error(
                $session, $inputString,
                'The profile template \'' . $templ . '\' has been fixed; no new properties can'
                . ' be added or deleted',
            );
        }

        # Add the property
        if (! $obj->createListProperty($property, @list)) {

            return $self->error(
                $session, $inputString,
                'Could not modify ' . $templ . ' profile template',
            );

        } elsif (@list == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Added scalar property \'' . $property . '\' with 1 element to the profile'
                . ' template \'' . $templ . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Added scalar property \'' . $property . '\' with ' . scalar @list
                . ' elements to the profile template \'' . $templ . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddHashProperty;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addhashproperty', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ahp', 'addhash', 'addhashprop', 'addhashproperty'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a new hash property to a template';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $templ, $property, @list,
        ) = @_;

        # Local variables
        my (
            $obj, $count,
            %hash,
        );

        # Check for improper arguments
        if (! defined $templ || ! defined $property) {

            return $self->improper($session, $inputString);
        }

        # Check that <templ> isn't one of the standard profile categories ('world', 'guild',
        #   'race', 'char'), which don't exist as templates
        if (defined $axmud::CLIENT->ivFind('constProfPriorityList', $templ)) {

            return $self->error(
                $session, $inputString,
                'Could not modify ' . $templ . ' profile template - \'' . $templ
                . '\' is a standard profile category that doesn\'t use templates',
            );
        }

        # Check that <templ> exists
        if (! $session->ivExists('templateHash', $templ)) {

            return $self->error(
                $session, $inputString,
                'Could not modify ' . $templ . ' profile template - profile template doesn\'t'
                . ' exist',
            );

        } else {

            $obj = $session->ivShow('templateHash', $templ);
        }

        # Check that the property name isn't a template IV (as opposed to an IV that can be
        #   transferred into a custom profile)
        if ($axmud::CLIENT->ivExists('constProfStandardHash', $property)) {

            return $self->error(
                $session, $inputString,
                'Could not modify ' . $templ . ' profile template - the property \''
                . $property . '\' is a reserved property that can\'t be manipulated',
            );
        }

        # Check that the template isn't already fixed (meaning that no new properties can be added
        #   or deleted)
        if ($obj->constFixedFlag) {

            return $self->error(
                $session, $inputString,
                'The profile template \'' . $templ . '\' has been fixed; no new properties can'
                . ' be added or deleted',
            );
        }

        # Convert @list into a hash, one key-value pair at a time. Check that keys don't repeat and
        #   that each key has a corresponding value
        $count = 0;
        while (@list) {

            my $key = shift @list;
            my $value = shift @list;

            if (exists $hash{$key}) {

                return $self->error(
                    $session, $inputString,
                    'Can\'t add the hash property \'' . $property . '\' because the key \'' . $key
                    . '\' occurs at least twice (keys in a hash must be unique)',
                );

            } elsif (! defined $value) {

                return $self->error(
                    $session, $inputString,
                    'Can\'t add the hash property \'' . $property . '\' because one key has no'
                    . ' corresponding value',
                );

            } else {

                $hash{$key} = $value;
                $count++;
            }
        }

        # Add the property
        if (! $obj->createHashProperty($property, %hash)) {

            return $self->error(
                $session, $inputString,
                'Could not modify ' . $templ . ' profile template',
            );

        } elsif ($count == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Added hash property \'' . $property . '\' with 1 key-value pair to the profile'
                . ' template \'' . $templ . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Added hash property \'' . $property . '\' with ' . $count
                . ' key-value pairs to the profile template \'' . $templ . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteProperty;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteproperty', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dp', 'delprop', 'deleteprop', 'deleteproperty'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a profile template property';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $templ, $property,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $templ || ! defined $property || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that <templ> isn't one of the standard profile categories ('world', 'guild',
        #   'race', 'char'), which don't exist as templates
        if (defined $axmud::CLIENT->ivFind('constProfPriorityList', $templ)) {

            return $self->error(
                $session, $inputString,
                'Could not modify ' . $templ . ' profile template - \'' . $templ
                . '\' is a standard profile category that doesn\'t use templates',
            );
        }

        # Check that <templ> exists
        if (! $session->ivExists('templateHash', $templ)) {

            return $self->error(
                $session, $inputString,
                'Could not modify ' . $templ . ' profile template - profile template doesn\'t'
                . ' exist',
            );

        } else {

            $obj = $session->ivShow('templateHash', $templ);
        }

        # Check that the property name isn't a template IV (as opposed to an IV that can be
        #   transferred into a custom profile)
        if ($axmud::CLIENT->ivExists('constProfStandardHash', $property)) {

            return $self->error(
                $session, $inputString,
                'Could not modify ' . $templ . ' profile template - the property \''
                . $property . '\' is a reserved property that can\'t be manipulated',
            );
        }

        # Check that the template isn't already fixed (meaning that no new properties can be added
        #   or deleted)
        if ($obj->constFixedFlag) {

            return $self->error(
                $session, $inputString,
                'The profile template \'' . $templ . '\' has been fixed; no new properties can'
                . ' be added or deleted',
            );
        }

        # Delete the property
        if (! $obj->removeProperty($property)) {

            return $self->error(
                $session, $inputString,
                'Could not modify ' . $templ . ' profile template',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Deleted the property \'' . $property . '\' from the profile template \''
                . $templ . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListProperty;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listproperty', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lp', 'listprop', 'listproperty'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists a profile template\'s properties';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $templ,
            $check,
        ) = @_;

        # Local variables
        my (
            $obj, $string,
            @propList, @modList, @reservedList,
            %reservedHash,
        );

        # Check for improper arguments
        if (! defined $templ || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;lp -r
        if ($templ eq '-r') {

            # Import the list of property names, sorted alphabetically
            @propList = sort {lc($a) cmp lc($b)} ($axmud::CLIENT->ivKeys('constProfStandardHash'));

            # Display header
            $session->writeText(
                'List of profile template reserved properties',
            );

            # Display list
            foreach my $prop (@propList) {

                $session->writeText('   ' . $prop);
            }

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @propList . 'reserved properties found)',
            );

        # ;lp <templ>
        } else {

            # Check that <template> isn't one of the standard profile categories ('world', 'guild',
            #   'race', 'char'), which don't exist as templates
            if (defined $axmud::CLIENT->ivFind('constProfPriorityList', $templ)) {

                return $self->error(
                    $session, $inputString,
                    'Could not show ' . $templ . ' profile template - \'' . $templ
                    . '\' is a standard profile category that doesn\'t use templates',
                );
            }

            # Check that <templ> exists
            if (! $session->ivExists('templateHash', $templ)) {

                return $self->error(
                    $session, $inputString,
                    'Could not show ' . $templ . ' profile template - profile template doesn\'t'
                    . ' exist',
                );

            } else {

                $obj = $session->ivShow('templateHash', $templ);
            }

            # Import the hash of reserved IV names
            %reservedHash = $axmud::CLIENT->constProfStandardHash;

            # Import the list of properties, and sort them alphabetically
            @propList = sort {lc($a) cmp lc($b)} ($obj->ivList());
            if (! @propList) {

                return $self->complete(
                    $session, $standardCmd,
                    'The property list for the profile template is empty',
                );
            }

            # Display header
            if ($obj->constFixedFlag) {
                $string = '(Fixed) ';
            } else {
                $string = '';
            }

            $session->writeText(
                'List of properties for the def\'n template \'' . $templ . '\' ' . $string
                . '(S scalar, L list, H hash - * Reserved)',
            );

            # Display list


            # Separate into two lists - one with only reserved IV names, another without any
            #   reserved IV names (so we can show one list before the other)
            foreach my $prop (@propList) {

                if (exists $reservedHash{$prop}) {
                    push (@reservedList, $prop);
                } else {
                    push (@modList, $prop);
                }
            }
            # Recombined the lists, reserved IVs first, non-reserved IVs after that
            @propList = (@reservedList, @modList);

            foreach my $prop (@propList) {

                my (
                    $type, $column, $count,
                    @list,
                    %hash,
                );

                # Is it a scalar, list or hash?
                $type = $obj->ivType($prop);

                # Is it a reserved property name (which can't be added or deleted) ?
                if (exists $reservedHash{$prop}) {
                    $column = '*';
                } else {
                    $column = ' ';
                }

                if ($type eq 'scalar') {

                    if (defined $obj->ivGet($prop)) {

                        $session->writeText(
                            ' ' . $column . 'S ' . sprintf('%-16.16s', $prop) . ' '
                            . sprintf('%-32.32s',  $obj->ivGet($prop)),
                        );

                    } else {

                        $session->writeText(
                            ' ' . $column . 'S ' . sprintf('%-16.16s', $prop) . ' <undef>',
                        );
                    }

                } elsif ($type eq 'list') {

                    @list = $obj->ivPeek($prop);
                    $count = 0;

                    $session->writeText(
                        ' ' . $column . 'L ' . sprintf('%-16.16s', $prop) . ' (List size: '
                        . @list . ')',
                    );

                    foreach my $item (@list) {

                        if (defined $item) {

                            $session->writeText(
                                '      #' . sprintf('%-16.16s', $count) . ' Value: ' . $item,
                            );

                        } else {

                            $session->writeText(
                                '      #' . sprintf('%-16.16s', $count) . ' Value: <undef>',
                            );
                        }

                        $count++;
                    }

                } elsif ($type eq 'hash') {

                    %hash = $obj->ivPeek($prop);

                    $session->writeText(
                        ' ' . $column . 'H ' . sprintf('%-16.16s', $prop)
                        . ' (Hash size: ' . scalar (keys %hash) . ')',
                    );

                    foreach my $key (keys %hash) {

                        if (defined $hash{$key}) {

                            $session->writeText(
                                '      Key:   ' . sprintf('%-16.16s', $key) . ' Value: '
                                . sprintf('%-32.32s', $hash{$key}),
                            );

                        } else {

                            $session->writeText(
                                '      Key:   ' . sprintf('%-16.16s', $key) . ' Value: <undef>',
                            );
                        }
                    }
                }
            }

            # Display footer
            if (@modList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (1 non-reserved property found)',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (' . scalar @modList . ' non-reserved properties found)',
                );
            }
        }
    }
}

# Profiles - world profiles

{ package Games::Axmud::Cmd::AddWorld;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addworld', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['awo', 'addworld'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a world profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my ($obj, $fileObj, $dictObj);

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that $name is a valid name
        if (! $axmud::CLIENT->nameCheck($name, 16)) {

            return $self->error(
                $session, $inputString,
                'Could not add world profile \'' . $name . '\' - invalid name',
            );

        # Check the profile doesn't already exist
        } elsif ($axmud::CLIENT->ivExists('worldProfHash', $name)) {

            return $self->error(
                $session, $inputString,
                'Could not add world profile \'' . $name . '\' - profile already exists',
            );
        }

        # Create a new world profile
        $obj = Games::Axmud::Profile::World->new($session, $name);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Could not add world profile \'' . $name . '\'',
            );
        }

        # Create a file object for the world profile
        $fileObj = Games::Axmud::Obj::File->new('worldprof', $name);
        if (! $fileObj) {

            return $self->error(
                $session, $inputString,
                'Could not add world profile \'' . $name . '\'',
            );
        }

        # Update IVs with the new profile and file object
        $axmud::CLIENT->add_fileObj($fileObj);
        $axmud::CLIENT->add_worldProf($obj);

        # If a dictionary object with the same name as the world doesn't already exist, create it.
        #   Otherwise use the existing one
        if (! $axmud::CLIENT->ivExists('dictHash', $name)) {

            $dictObj = Games::Axmud::Obj::Dict->new($session, $name);
            if (! $dictObj) {

                $session->writeWarning(
                    'Could not create a dictionary called \'' . $name . '\'',
                    $self->_objClass . '->do',
                );

            } else {

                # Update IVs with the new dictionary
                $axmud::CLIENT->add_dict($dictObj);
            }
        }

        return $self->complete($session, $standardCmd, 'Added world profile \'' . $name . '\'');
    }
}

{ package Games::Axmud::Cmd::SetWorld;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setworld', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['swo', 'setworld'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the current world profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $world, $char,
            $check,
        ) = @_;

        # Local variables
        my ($result, $statusTask);

        # Check for improper arguments
        if (! defined $world || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there are no 'free' windows open
        if ($axmud::CLIENT->desktopObj->listSessionFreeWins($session, TRUE)) {

            return $self->error(
                $session, $inputString,
                'Can\'t set the current world profile while there are edit, preference and wizard'
                . ' windows open (try closing them first)',
            );
        }

        # If the world profile already exists, use it
        if ($axmud::CLIENT->ivExists('worldProfHash', $world)) {

            # Set the current world profile. If <char> was specified, make that the current
            #   character profile, too
            $result = $session->setupProfiles('set_exist', $world, $char);

        # Otherwise, create a new world profile, and make it the current one
        } else {

            # Check that $world is valid
            if (! $axmud::CLIENT->nameCheck($world, 16)) {

                return $self->error(
                    $session, $inputString,
                    'Could not add world profile \'' . $world . '\' - invalid name',
                );
            }

            # Create a world profile and set it as the current world profile. If <char> was
            #   specified, make that the current character profile, too (creating it, if necessary)
            $result = $session->setupProfiles('set_new', $world, $char);
        }

        # If the Status task's counters are running, reset their values, and turn them off
        if ($session->statusTask) {

            $session->statusTask->update_profiles();
        }

        if (! $result) {

            return $self->error(
                $session, $inputString,
                'Could not set \'' . $world . '\' as the current world profile',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Set \'' . $world . '\' as the current world profile (don\'t forget to set a'
                . ' current character profile with the \';setchar\' command)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::CloneWorld;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('cloneworld', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cwo', 'copyworld', 'cloneworld'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Clones a world profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $noAssocFlag, $noCageFlag, $noSkelFlag, $noModelFlag, $noDictFlag,
            $originalName, $copyName, $originalObj, $copyObj, $copyFileObj, $otherProfFileObj,
            $worldModelFileObj, $failFlag, $result, $originalDictObj, $copyDictObj,
            $backupWorldModelObj, $copyWorldModelObj,
            @clonedProfList, @clonedCageList, @backupPriorityList,
            %backupProfHash, %backupCageHash, %backupSkelHash,
        );

        ($switch, @args) = $self->extract('-a', 0, @args);
        if (defined $switch) {

            $noAssocFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-t', 0, @args);
        if (defined $switch) {

            $noCageFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-k', 0, @args);
        if (defined $switch) {

            $noSkelFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-m', 0, @args);
        if (defined $switch) {

            $noModelFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-d', 0, @args);
        if (defined $switch) {

            $noDictFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-e', 0, @args);
        if (defined $switch) {

            # Don't clone any optional extras at all
            $noAssocFlag = TRUE;
            $noCageFlag = TRUE;
            $noSkelFlag = TRUE;
            $noModelFlag = TRUE;
            $noDictFlag = TRUE;
        }

        # Extract remaining arguments
        $originalName = shift @args;
        $copyName = shift @args;

        # ;cwo <copy>
        if (! defined $copyName) {

            # Use the current world as the original
            $copyName = $originalName;
            $originalName = $session->currentWorld->name;
        }

        # Check for improper arguments
        if (! defined $originalName || ! defined $copyName || @args) {

            return $self->improper($session, $inputString);
        }

        # Check that the <original> profile exists and that <copy> doesn't
        if (! $axmud::CLIENT->ivExists('worldProfHash', $originalName)) {

            return $self->error(
                $session, $inputString,
                'Could not clone world profile \'' . $originalName . '\'- profile does not exist',
            );

        } elsif ($axmud::CLIENT->ivExists('worldProfHash', $copyName)) {

            return $self->error(
                $session, $inputString,
                'Could not clone world profile \'' . $originalName . '\'- profile with name \''
                . $copyName . '\' already exists',
            );

        } else {

            $originalObj = $axmud::CLIENT->ivShow('worldProfHash', $originalName);
        }

        # Check it's a world profile
        if ($originalObj->category ne 'world') {

            return $self->error(
                $session, $inputString,
                '\'' . $originalName . '\' is a \'' . $originalObj->category . '\' profile and'
                . ' can\'t be cloned with this command',
            );
        }

        # Only current world profiles can be cloned
        if ($session->currentWorld->name ne $originalName) {

            # All of the cages (and so on) haven't been loaded, if the world profile isn't current,
            #   so it's not possible to clone it
            return $self->error(
                $session, $inputString,
                'Sorry, world profiles can only be cloned when they are the current world profile',
            );
        }

        # World profiles which have been marked as 'not saveable' can't be cloned
        if ($session->currentWorld->noSaveFlag) {

            return $self->error(
                $session, $inputString,
                'Sorry, temporary world profiles can\'t be clone',
            );
        }

        # Check that $copyName is a valid name
        if (! $axmud::CLIENT->nameCheck($copyName, 16)) {

            return $self->error(
                $session, $inputString,
                'Could not add world profile \'' . $copyName . '\' - invalid name',
            );
        }

        # Create the cloned world profile
        $copyObj = $originalObj->clone($session, $copyName);
        if (! $copyObj) {

            return $self->error(
                $session, $inputString,
                'Could not create cloned world profile \''. $copyName . '\'',
            );
        };

        # Create a file object for the world profile
        $copyFileObj = Games::Axmud::Obj::File->new('worldprof', $copyName);
        if (! $copyFileObj) {

            return $self->error(
                $session, $inputString,
                'Could not create cloned world profile \''. $copyName . '\'',
            );
        }

        # Create associated 'otherprof' and 'worldmodel' file objects (but don't add them to any
        #   registry)
        $otherProfFileObj = Games::Axmud::Obj::File->new('otherprof', $copyName, $session);
        if (! $otherProfFileObj) {

            return $self->error(
                $session, $inputString,
                'Could not create cloned world profile \''. $copyName . '\'',
            );
        }

        $worldModelFileObj = Games::Axmud::Obj::File->new('worldmodel', $copyName, $session);
        if (! $worldModelFileObj) {

            return $self->error(
                $session, $inputString,
                'Could not create cloned world profile \''. $copyName . '\'',
            );
        }

        # Mark each of the new file objects as having had their data modified (which forces
        #   ->saveDataFile to create the file, when we call it)
        $copyFileObj->set_modifyFlag(TRUE, $self->_objClass . '->do');
        $otherProfFileObj->set_modifyFlag(TRUE, $self->_objClass . '->do');
        $worldModelFileObj->set_modifyFlag(TRUE, $self->_objClass . '->do');

        # Save the 'worldprof' file
        $result = $copyFileObj->saveDataFile();
        if (! $result) {

            return $self->error(
                $session, $inputString,
                'Save error - could not create cloned world profile \''. $copyName . '\'',
            );

        } else {

            # We can now update the client's registries. Even if some part of this function
            #   fails, the world profile itself has been cloned and saved

            # Update the client with the cloned world's file object (only)
            $axmud::CLIENT->add_fileObj($copyFileObj);
            # Update the GA::Client with the new world profile
            $axmud::CLIENT->add_worldProf($copyObj);
        }

        # Prepare the data to be saved in the 'otherprof' file. To do this, we need to
        #   temporarily empty a few registries
        %backupProfHash = $session->profHash;
        $session->ivEmpty('profHash');
        %backupCageHash = $session->cageHash;
        $session->ivEmpty('cageHash');
        if ($noSkelFlag) {

            @backupPriorityList = $session->profPriorityList;
            $session->ivEmpty('profPriorityList');
            %backupSkelHash = $session->templateHash;
            $session->ivEmpty('templateHash');
        }

        # Clone all of the current world profile's associated profiles, if allowed (i.e. everything
        #   in $self->profHash, except the current world profile)
        # Change their ->parentWorld to the cloned world
        if (! $noAssocFlag) {

            OUTER: foreach my $obj (values %backupProfHash) {

                if ($obj->category ne 'world') {

                    my $clonedObj = $obj->clone($session, $obj->name);  # Clone has the same name
                    if (! $clonedObj) {

                        $self->writeWarning(
                            'Error cloning the \'' . $obj->name . '\' profile - the \'' . $copyName
                            . '\' world profile has been cloned, but no associated profile or'
                            . ' cages will be cloned alongside it',
                        );

                        $failFlag = TRUE;
                        last OUTER;

                    } else {

                        $clonedObj->ivPoke('parentWorld', $copyName);
                        push (@clonedProfList, $obj);
                    }
                }
            }
        }

        # Clone the world's associated cages (if allowed), storing the output in a hash
        if (! $failFlag && ! $noCageFlag) {

            @clonedCageList = $session->cloneCages($originalObj, $copyObj, \%backupCageHash);
            if (! @clonedCageList && %backupCageHash) {

                $self->writeWarning(
                    'Error cloning cages for the \'' . $copyName . '\' world profile - although the'
                    . ' profile has been cloned, no associated profiles or cages will be cloned'
                    . ' alongside it',
                );

                $failFlag = TRUE;
            }
        }

        # Temporarily store these objects in the registries we emptied a few minutes ago, so that
        #   the file object uses the clones when saving its file
        if (! $failFlag) {

            foreach my $obj (@clonedProfList) {

                $session->ivAdd('profHash', $obj->name, $obj);
            }

            foreach my $obj (@clonedCageList) {

                $session->ivAdd('cageHash', $obj->name, $obj);
            }

            # Save the 'otherprof' file using the cloned data
            $result = $otherProfFileObj->saveDataFile();
            if (! $result) {

                $self->writeWarning(
                    $session, $inputString,
                    'Error cloning the \'' . $copyName . '\' world profile - although the profile'
                    . ' has been cloned, no associated profiles or cages will be cloned alongside'
                    . ' it',
                );

                $failFlag = TRUE;
            }
        }

        # Restore the registries we empties a few moments ago
        $session->ivPoke('profHash', %backupProfHash);
        $session->ivPoke('cageHash', %backupCageHash);
        if ($noSkelFlag) {

            $session->ivPoke('profPriorityList', @backupPriorityList);
            $session->ivPoke('templateHash', %backupSkelHash);
        }

        # Prepare the data to be saved in the 'worldmodel' file
        if (! $failFlag && $noModelFlag) {

            # We can't clone the existing world model, so we need to temporarily create a new
            #   (empty) world model
            $copyWorldModelObj = Games::Axmud::Obj::WorldModel->new();
            if (! $copyWorldModelObj) {

                $self->writeWarning(
                    $session, $inputString,
                    'Error cloning the \'' . $copyName . '\' world profile - although the profile'
                    . ' has been cloned, the world model was not cloned alongside it',
                );

                $failFlag = TRUE;

            } else {

                $backupWorldModelObj = $session->worldModelObj;
                $session->ivPoke('worldModelObj', $copyWorldModelObj);
            }
        }

        # Save the 'worldmodel' file. We don't need to temporarily empty any registries
        if (! $failFlag) {

            $result = $worldModelFileObj->saveDataFile();
            if (! $result) {

                $self->writeWarning(
                    $session, $inputString,
                    'Error cloning the \'' . $copyName . '\' world profile - although the world'
                        . ' profile and all its associated profiles have been cloned, the world'
                        . ' model could not be cloned alongside it',
                );

                $failFlag = TRUE;
            }
        }

        # Restore the registries we empties a few moments ago
        if (! $failFlag && $noModelFlag) {

            $session->ivPoke('worldModelObj', $backupWorldModelObj);
        }

        # Clone <original>'s dictionary (if allowed, and if it has one), with the copy having the
        #   same name as the new world profile
        if (! $failFlag && ! $noDictFlag) {

            if (
                $originalObj->dict
                && $axmud::CLIENT->ivExists('dictHash', $originalObj->dict)
            ) {
                $originalDictObj = $axmud::CLIENT->ivShow('dictHash', $originalObj->dict);
                $copyDictObj = $originalDictObj->clone($session, $copyName);
                if (! $copyDictObj) {

                    $self->writeWarning(
                        $session, $inputString,
                        'Error cloning the \'' . $copyName . '\' world profile\'s dictionary',
                    );

                    $failFlag = TRUE;

                } else {

                    # Update IVs. Add the dictionary object to its registry
                    $axmud::CLIENT->add_dict($copyDictObj);
                }
            }
        }

        # Process complete
        if ($failFlag) {

            return $self->complete(
                $session, $standardCmd,
                'Created cloned world profile \'' . $copyName . '\' (with errors)',
            );

        } else {

             return $self->complete(
                $session, $standardCmd,
                'Created cloned world profile \'' . $copyName . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditWorld;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editworld', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ewo', 'editworld'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a world profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $worldName,
            $check,
        ) = @_;

        # Local variables
        my $worldObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that profile exists
        if (! defined $worldName) {

            # Use the current world
            $worldName = $session->currentWorld->name;
            $worldObj = $session->currentWorld;

        } else {

            if (! $axmud::CLIENT->ivExists('worldProfHash', $worldName)) {

                return $self->error(
                    $session, $inputString,
                    'Could not edit the world profile \'' . $worldName . '\' - profile does not'
                    . ' exist',
                );

            } else {

                $worldObj = $axmud::CLIENT->ivShow('worldProfHash', $worldName);
            }
        }

        # Open an 'edit' window for the profile
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Profile::World',
                $session->mainWin,
                $session,
                'Edit world profile \'' . $worldObj->name . '\'',
                $worldObj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the world profile \'' . $worldName . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the world profile \'' . $worldName . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteWorld;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteworld', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dwo', 'delworld', 'deleteworld'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a world profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $worldName,
            $check,
        ) = @_;

        # Local variables
        my ($worldObj, $result, $fileObj);

        # Check for improper arguments
        if (! defined $worldName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there are no 'free' windows open
        if ($axmud::CLIENT->desktopObj->listSessionFreeWins($session, TRUE)) {

            return $self->error(
                $session, $inputString,
                'Can\'t delete a world profile while there are edit, preference and wizard windows'
                . ' open (try closing them first)',
            );
        }

        # Check that profile exists
        if (! $axmud::CLIENT->ivExists('worldProfHash', $worldName)) {

            return $self->error(
                $session, $inputString,
                'Could not delete world profile \'' . $worldName . '\'- profile does not exist',
            );

        } else {

            $worldObj = $axmud::CLIENT->ivShow('worldProfHash', $worldName);
        }

        # Check it's not the current world profile for this session...
        if (defined $session->currentWorld && $session->currentWorld eq $worldObj) {

            return $self->error(
                $session, $inputString,
                'The current world profile can\'t be deleted',
            );

        # ...or for any other session
        } else {

            foreach my $otherSession ($axmud::CLIENT->listSessions()) {

                if (
                    defined $otherSession->currentWorld
                    && $otherSession->currentWorld eq $worldObj
                ) {
                    return $self->error(
                        $session, $inputString,
                        'Could not delete world profile \'' . $worldName . '\' - it is in use by'
                        . ' another session',
                    );
                }
            }
        }

        # Ask the user if they're sure...
        $result = $session->mainWin->showMsgDialogue(
            'Delete world profile',
            'question',
            'Are you sure you want to delete the world profile \'' . $worldName . '\'? (Doing so'
            . ' will remove it from memory AND destroy its data files)',
            'yes-no',
        );

        if ($result ne 'yes') {

            return $self->complete($session, $standardCmd, 'World profile deletion cancelled');
        }

        # Delete the world's data files. First find the file object
        $fileObj = $axmud::CLIENT->ivShow('fileObjHash', $worldName);
        if (! $fileObj) {

            return $self->error(
                $session, $inputString,
                'Could not delete world profile \'' . $worldName . '\' - file object is missing',
            );
        }

        # Delete the whole directory used for that world (e.g. /data/worlds/WORLD_NAME)
        $result = $fileObj->destroyStandardDir();
        if (! $result) {

            # (Not a fatal error)
            $session->writeWarning(
                'Could not delete data files for the world profile \'' . $worldName . '\'',
                $self->_objClass . '->do',
            );
        }

        # Update the GA::Client registries
        $axmud::CLIENT->del_worldProf($worldObj);
        $axmud::CLIENT->del_fileObj($fileObj);

        if (! $result) {

            return $self->complete(
                $session, $standardCmd,
                'Deleted world profile \'' . $worldName . '\', but could not delete its data files',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Deleted world profile \'' . $worldName . '\' and its data files',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListWorld;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listworld', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lwo', 'listworld'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists all world profiles';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @profList;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Compile a list of blessed references to profiles, sorted by name
        @profList
            = sort {lc($a->name) cmp lc($b->name)} ($axmud::CLIENT->ivValues('worldProfHash'));

        if (! @profList) {

            return $self->complete($session, $standardCmd, 'The world profile list is empty');
        }

        # Display header
        $session->writeText('List of world profiles (* = current world, T - temporary)');
        $session->writeText('    Profile name     World long name                  IP/DNS & Port');

        # Display list
        foreach my $obj (@profList) {

            my ($column, $port, $longName);

            if (defined $session->currentWorld && $session->currentWorld eq $obj) {
                $column = ' *';
            } else {
                $column = '  ';
            }

            if ($obj->noSaveFlag) {
                $column .= 'T ';
            } else {
                $column .= '  ';
            }

            if ($obj->port) {
                $port = $obj->port;
            } else {
                $port = '';
            }

            if ($obj->longName) {
                $longName = $obj->longName;
            } else {
                $longName = '';
            }

            # Display a world's details
            if ($obj->dns) {

                $session->writeText(
                    $column . sprintf('%-16.16s %-32.32s ', $obj->name, $longName)
                    . $obj->dns . ' ' . $port,
                );

            } elsif ($obj->ipv4) {

                $session->writeText(
                    $column . sprintf('%-16.16s %-32.32s ', $obj->name, $longName)
                    . $obj->ipv4 . ' ' . $port,
                );

            } elsif ($obj->ipv6) {

                $session->writeText(
                    $column . sprintf('%-16.16s %-32.32s ', $obj->name, $longName)
                    . $obj->ipv6 . ' ' . $port,
                );

            } else {

                $session->writeText(
                    $column . sprintf('%-16.16s %-32.32s ', $obj->name, $longName)
                    . '(unknown connection details)',
                );
            }
        }

        # Display footer
        if (@profList == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 profile found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @profList . ' profiles found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetFavouriteWorld;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setfavouriteworld', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'sfw',
            'setfaveworld',
            'setfavoriteworld',
            'setfavouriteworld',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the list of favourite worlds';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @list,
        ) = @_;

        # Local variables
        my (
            @modList, @rejectList,
            %hash,
        );

        # (No improper arguments to check)

        # ;sfw
        if (! @list) {

            # Reset the favourite world list
            $axmud::CLIENT->set_favouriteWorldList();

            return $self->complete($session, $standardCmd, 'Favourite world list reset');

        # ;sfw <list>
        } else {

            # Items in the favourite world list don't have to actually exist as world profiles, but
            #   we still need to check every item in the list, removing duplicates and illegal names
            foreach my $name (@list) {

                # If it's not a duplicate...
                if (! exists $hash{$name}) {

                    $hash{$name} = undef;

                    # Check it's a valid name
                    if ($axmud::CLIENT->nameCheck($name, 16)) {
                        push (@modList, $name);
                    } else {
                        push (@rejectList, $name);
                    }
                }
            }

            if (! @modList) {

                return $self->error(
                    $session, $inputString,
                    'Favourite world list not modified - the list did not contain any valid world'
                    . ' names',
                );

            } else {

                # Set the favourite world list
                $axmud::CLIENT->set_favouriteWorldList(@modList);

                if (@rejectList) {

                    return $self->complete(
                        $session, $standardCmd,
                        'Favourite world list set to \'' . join(' ', @modList) . '\' (rejected'
                        . ' world names: \'' . join(' ', @rejectList) . '\')',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'Favourite world list set to \'' . join(' ', @modList) . '\'',
                    );
                }
            }
        }
    }
}

{ package Games::Axmud::Cmd::ListFavouriteWorld;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listfavouriteworld', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'lfv',
            'listfaveworld',
            'listfavoriteworld',
            'listfavouriteworld',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows the list of favourite worlds';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $count,
            %hash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the registry of world profiles for quick lookup
        %hash = $axmud::CLIENT->worldProfHash;
        if (! %hash) {

            return $self->complete($session, $standardCmd, 'The favourite world list is empty');
        }

        # Display header
        $session->writeText('List of favourite worlds (* - world profile exists)');

        # Display list
        $count = 0;
        foreach my $name ($axmud::CLIENT->favouriteWorldList) {

            my $column;

            $count++;

            if (exists $hash{$name}) {
                $column = ' * ';
            } else {
                $column = '   ';
            }

            $session->writeText($column . sprintf('%-4.4s', $count) . ' ' . $name);
        }

        # Display footer
        if ($count == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 world found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . $count . ' worlds found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetAutoWorld;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setautoworld', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['saw', 'setautoworld'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the list of auto-connecting worlds';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @list,
        ) = @_;

        # (No improper arguments to check)

        # ;saw
        if (! @list) {

            # Reset the auto-connecting world list
            $axmud::CLIENT->set_autoConnectList();

            return $self->complete($session, $standardCmd, 'Auto-connecting world list reset');

        # ;saw <list>
        } else {

            $axmud::CLIENT->set_autoConnectList(
                $axmud::CLIENT->autoConnectList,
                join (' ', @list),
            );

            return $self->complete(
                $session, $standardCmd,
                'Auto-connecting world list updated with the line \''
                . $axmud::CLIENT->ivIndex(
                    'autoConnectList',
                    $axmud::CLIENT->ivLast('autoConnectList'),
                ) . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListAutoWorld;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listautoworld', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['law', 'listautoworld'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows the list of auto-connecting worlds';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $count,
            %hash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Display header
        $session->writeText('List of auto-connecting worlds (plus optional characters)');

        # Display list
        foreach my $line ($axmud::CLIENT->autoConnectList) {

            $session->writeText('   ' . $line);
        }

        # Display footer
        if ((scalar $axmud::CLIENT->autoConnectList) == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 world found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . (scalar $axmud::CLIENT->autoConnectList) . ' worlds found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::RestoreWorld;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('restoreworld', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rwo', 'restoreworld'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Restores pre-configured worlds';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my (
            $count, $msg, $response, $successCount,
            @worldList, @existList, @missingList, @successList, @newWorldList,
            %fileObjHash, %worldHash, %archivePathHash, %newHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there are no 'free' windows open
        if ($axmud::CLIENT->desktopObj->listSessionFreeWins($session, TRUE)) {

            return $self->error(
                $session, $inputString,
                'Can\'t restore a world profile while there are edit, preference and wizard windows'
                . ' open (try closing them first)',
            );
        }

        # Check that all sessions have their ->status set to 'disconnected' (already disconnected
        #   from the world), or running in 'connect offline' mode
        $count = 0;
        foreach my $otherSession ($axmud::CLIENT->listSessions()) {

            if ($otherSession->status ne 'disconnected' && $otherSession->status ne 'offline') {

                $count++;
            }
        }

        if ($count) {

            $msg = 'To avoid losing data, the restore pre-configured worlds operation can only be'
                    . ' started when all sessions are disconnected (or running in \'offline\''
                    . ' mode); there ';

            if ($count == 1) {
                $msg .= 'is 1 session';
            } else {
                $msg .= 'are ' . $count . ' sessions';
            }

            return $self->error(
                $session, $inputString,
                $msg . ' still connected to a world',
            );
        }

        # If there are any unsaved files, show a warning before continuing. First, count the number
        #   of unsaved files. Store each file object found in a hash, so that we don't count
        #   duplicates
        $count = 0;
        foreach my $fileObj ($axmud::CLIENT->ivValues('fileObjHash')) {

            if (! exists $fileObjHash{$fileObj}) {

                $fileObjHash{$fileObj} = undef;

                if ($fileObj->modifyFlag) {

                    # Unsaved file
                    $count++;
                }
            }
        }

        foreach my $otherSession ($axmud::CLIENT->listSessions()) {

            foreach my $fileObj ($otherSession->ivValues('sessionFileObjHash')) {

                if (! exists $fileObjHash{$fileObj}) {

                    $fileObjHash{$fileObj} = undef;

                    if ($fileObj->modifyFlag) {

                        # Unsaved file
                        $count++;
                    }
                }
            }
        }

        if ($count) {

            if ($count == 1) {
                $msg = 'There is 1 file which hasn\'t';
            } else {
                $msg = 'There are ' . $count . ' files which haven\'t';
            }

            $msg .= ' yet been saved. When this operation is complete, ' . $axmud::SCRIPT
                    . ' will shut down without saving files (except the newly-restored ones). Are'
                    . ' you sure you want to continue?';

        } else {

            # We need to give the user at least one opportunity to change their mind...

            $msg = 'This operation will import pre-configured worlds. If there are any existing'
                    . ' worlds with the same name, their files will be replaced (but other files'
                    . ' will not be changed). Are you sure you want to continue?';
        }

        $response = $session->mainWin->showMsgDialogue(
            'Unsaved files',
            'warning',
            $msg,
            'yes-no',
        );

        if (! $response || $response ne 'yes') {

            return $self->complete(
                $session, $standardCmd,
                'Restore pre-configured worlds operation cancelled',
            );

        } else {

            $session->writeText('Starting \'restore pre-configured worlds\' operation...');
        }

        # Import the registry of world profiles for quick lookup
        %worldHash = $axmud::CLIENT->worldProfHash;

        # Compile a list of pre-configured worlds to restore
        if (! $arg) {
            @worldList = $axmud::CLIENT->constWorldList;
        } else {
            @worldList = ($arg);
        }

        # Go through the list of pre-configured worlds, GA::Client->constWorldList, checking that
        #   the archive file still exists
        foreach my $world (@worldList) {

            my ($string, $name, $path);

            $name = '\'' . $world . '\'';
            $string = '   Pre-configured world ' . sprintf('%-18.18s', $name) . ' : ';
            $path = $axmud::SHARE_DIR . '/items/worlds/' . $world . '/' . $world . '.tgz';
            $archivePathHash{$world} = $path;

            if (-e $path) {

                push (@existList, $world);
                $string .= 'found';

            } else {

                push (@missingList, $world);
                $string .= 'archive file missing';
            }

            if (exists $worldHash{$world}) {
                $string .= ' (world profile exists)';
            } else {
                $string .= ' (no corresponding world profile)';
            }

            $session->writeText($string);
        }

        # If no archive files were found, we can't continue
        if (! @existList) {

            $session->writeText('Operation halted');

            return $self->error(
                $session, $inputString,
                'Cannot restore pre-configured worlds (no matching archive files found)',
            );
        }

        # Restore each world, one by one  (code adapted from GA::Client->copyPreConfigWorlds, which
        #   was in turn adapted from GA::Cmd::ImportFiles->do)
        if (@existList == 1) {
            $session->writeText('Restoring 1 world...');
        } else {
            $session->writeText('Restoring ' . scalar @existList . ' worlds...');
        }

        # From this point on, Axmud will shut down, even if there is an error
        $successCount = 0;
        foreach my $world (@existList) {

            my (
                $importPath, $extractObj, $tempDir, $newDir, $backupFlag, $origLogo, $newLogo,
                @fileList,
                %fileHash,
            );

            $importPath = $archivePathHash{$world};
            $session->writeText('   Restoring from archive ' . $importPath);

            # Build an Archive::Extract object
            $extractObj = Archive::Extract->new(archive => $importPath);
            if (! $extractObj) {

                return $self->haltRestore(
                    $session,
                    $successCount,
                    'Cannot restore pre-configured worlds (no matching archive files found)',
                );
            }

            # Extract the object to a temporary directory
            $tempDir = $axmud::DATA_DIR . '/data/temp/import';
            if (! $extractObj->extract(to => $tempDir)) {

                return $self->haltRestore(
                    $session,
                    $successCount,
                    'General error importing pre-configured worlds (extraction error)',
                );
            }

            # All the files are now in /data/temp/import. Get a list of paths, relative to $tempDir,
            #   of all the extracted files
            @fileList = @{$extractObj->files};  # e.g. export/tasks.axm
            # Convert all the paths into absolute paths. Check they are real Axmud files and, if so,
            #   store them in a hash
            INNER: foreach my $file (@fileList) {

                my (
                    $fileType, $filePath,
                    %headerHash,
                );

                $filePath = $tempDir . '/' . $file;

                %headerHash
                    = $axmud::CLIENT->configFileObj->examineDataFile($filePath, 'return_header');
                if (! %headerHash) {

                    return $self->haltRestore(
                        $session,
                        $successCount,
                        'General error importing pre-configured worlds (archive contains invalid'
                        . ' file)',
                    );

                } else {

                    $fileType = $headerHash{'file_type'};
                    $fileHash{$fileType} = $filePath;
                }
            }

            # Now we can check that we have the right three files ('worldprof', 'otherprof' and
            #   'worldmodel')
            if (
                ! exists $fileHash{'worldprof'}
                || ! exists $fileHash{'otherprof'}
                || ! exists $fileHash{'worldmodel'}
                || scalar (keys %fileHash) != 3
            ) {
                return $self->haltRestore(
                    $session,
                    $successCount,
                    'General error importing pre-configured worlds (incorrect archive for \''
                    . $world . '\' world',
                );
            }

            # Create the data sub-directory, if it doesn't already exist
            $newDir = $axmud::DATA_DIR . '/data/worlds/' . $world . '/';
            if (! (-e $newDir)) {

                if (! mkdir ($newDir, 0755)) {

                    return $self->haltRestore(
                        $session,
                        $successCount,
                        'General error importing pre-configured worlds (could not copy files)',
                    );
                }

            } else {

                # The sub-directory already exists. Make backup copies of its existing files (don't
                #   respond to a copy failure; the user had already been warned)
                # If a logo for this world exists, and if the equivalent logo doesn't exist in the
                #   data directory, copy it$backupFlag = TRUE;
                File::Copy::copy($newDir . 'worldprof.axm', $newDir . 'worldprof_bu.axm');
                File::Copy::copy($newDir . 'otherprof.axm', $newDir . 'otherprof.axm');
                File::Copy::copy($newDir . 'worldmodel.axm', $newDir . 'worldmodel_bu.axm');
            }

            # Copy the files into the directory
            foreach my $file (keys %fileHash) {

                my $filePath = $fileHash{$file};

                if (! File::Copy::copy($filePath, $newDir . $file . '.axm')) {

                    # Try to restore backups (if any)
                    if ($backupFlag) {

                        File::Copy::copy($newDir . 'worldprof_bu.axm', $newDir . 'worldprof.axm');
                        File::Copy::copy($newDir . 'otherprof_bu.axm', $newDir . 'otherprof.axm');
                        File::Copy::copy($newDir . 'worldmodel_bu.axm', $newDir . 'worldmodel.axm');

                    } else {

                        # The sub-directory was created by this operation, so we can delete it
                        unlink $newDir;
                    }

                    return $self->haltRestore(
                        $session,
                        $successCount,
                        'General error importing pre-configured worlds (could not copy files)',
                    );
                }
            }

            # Add a dummy entry to the this object's profile registry so the calling function,
            #   GA::Obj::File->setupConfigFile, can add the world to the 'config' file it's about to
            #   save
            $axmud::CLIENT->ivAdd('worldProfHash', $world, undef);

            # If a logo for this world exists, and if the equivalent logo doesn't exist in the data
            #   directory, copy it
            $origLogo = $axmud::SHARE_DIR . '/items/worlds/' . $world . '/' . $world . '.jpg';
            $newLogo = $axmud::DATA_DIR . '/logos/' . $world . '.jpg';

            if (-e $origLogo && ! (-e $newLogo)) {

                File::Copy::copy($origLogo, $newLogo);
            }

            # World restored
            $successCount++;
            push (@successList, $world);

            $session->writeText('   Restored \'' . $world . '\'');
        }

        # The 'config' file contains a list of world profiles which is now obsolete. Compile a list
        #   of the world profile names it should contain....
        @newWorldList = $axmud::CLIENT->ivKeys('worldProfHash');
        # Load the obsolete 'config' file. Do it, even if file saving/loading has been disabled
        $axmud::CLIENT->ivPoke('loadConfigFlag', TRUE);
        if (! $axmud::CLIENT->configFileObj->loadConfigFile()) {

            $self->configError($session, @successList);
        }

        # Replace GA::Client->worldProfHash with a partial hash - the new 'config' files only needs
        #   the names of world profiles
        foreach my $world (@newWorldList) {

            $newHash{$world} = undef;
        }

        $axmud::CLIENT->ivPoke('worldProfHash', %newHash);

        # Save the recently-loaded 'config' file. Do it, even if file saving/loading has been
        #   disabled
        $axmud::CLIENT->ivPoke('saveConfigFlag', TRUE);
        if (! $axmud::CLIENT->configFileObj->saveConfigFile()) {

            $self->configError($session, @successList);
        }

        # Now disable all file saving/loading, show a confirmation, and shut down Axmud
        $axmud::CLIENT->ivPoke('loadConfigFlag', FALSE);
        $axmud::CLIENT->ivPoke('saveConfigFlag', FALSE);
        $axmud::CLIENT->ivPoke('loadDataFlag', FALSE);
        $axmud::CLIENT->ivPoke('saveDataFlag', FALSE);

        $msg = 'Operation complete (restored ';

        if ($successCount == 1) {
            $msg .= '1 world';
        } else {
            $msg .= $successCount . ' worlds';
        }

        $msg .= '; ' . $axmud::SCRIPT . ' will now shut down';

        # (Show the message both in the 'main' window, and in a dialogue - so that logfiles can
        #   read it)
        $session->writeText($msg);

        $session->mainWin->showMsgDialogue(
            'Restore worlds',
            'info',
            $msg,
            'ok',
        );

        # (Cannot show a $self->complete message)
        return $axmud::CLIENT->stop();
    }

    sub haltRestore {

        # Called by $self->do when the operation fails while importing files
        # Shows an explanatory message before shutting down Axmud
        #
        # Expected arguments
        #   $session        - The calling function's GA::Session
        #   $successCount   - The number of pre-configured worlds which have been incorporated into
        #                       Axmud's data files (may be 0)
        #   $msg            - An error message to show
        #
        # Return values
        #   The value of GA::Client->stop()

        my ($self, $session, $successCount, $msg, $check) = @_;

        # Check for improper arguments
        if (! defined $session || ! defined $successCount || ! defined $msg || defined $check) {

            # (No return value - Axmud must still shut down)
            $axmud::CLIENT->writeImproper($self->_objClass . '->haltRestore', @_);
        }

        # Disable all file saving/loading, show a confirmation, and shut down Axmud
        $axmud::CLIENT->ivPoke('loadConfigFlag', FALSE);
        $axmud::CLIENT->ivPoke('saveConfigFlag', FALSE);
        $axmud::CLIENT->ivPoke('loadDataFlag', FALSE);
        $axmud::CLIENT->ivPoke('saveDataFlag', FALSE);

        # (Show the message both in the 'main' window, and in a 'dialogue' window, so that logfiles
        #   can read it)
        $session->writeText($msg);

        $session->mainWin->showMsgDialogue(
            'Operation failed',
            'error',
            $msg,
            'ok',
        );

        return $axmud::CLIENT->stop();
    }

    sub configError {

        # Called by $self->do when the operation fails to save (or load) the 'config' file
        # Shows an explanatory message before shutting down Axmud
        #
        # Expected arguments
        #   $session        - The calling function's GA::Session
        #
        # Optional arguments
        #   @successList    - A list of pre-configured worlds that were succesfully imported,
        #                       before the error occured (may be an empty list)
        #
        # Return values
        #   The value of GA::Client->stop()

        my ($self, $session, @successList) = @_;

        # Local variables
        my $msg;

        # Check for improper arguments
        if (! defined $session) {

            # (No return value - Axmud must still shut down)
            $axmud::CLIENT->writeImproper($self->_objClass . '->configError', @_);
        }

        # Disable all file saving/loading, show a confirmation, and shut down Axmud
        $axmud::CLIENT->ivPoke('loadConfigFlag', FALSE);
        $axmud::CLIENT->ivPoke('saveConfigFlag', FALSE);
        $axmud::CLIENT->ivPoke('loadDataFlag', FALSE);
        $axmud::CLIENT->ivPoke('saveDataFlag', FALSE);

        $msg = 'Error replacing the \'config\' file. ';

        if (! @successList) {

            $msg .= 'No pre-configured worlds have been incorporated into ' . $axmud::SCRIPT
                    . '\'s data files';

        } else {

            if (@successList == 1) {
                $msg .= '1 pre-configured world has';
            } else {
                $msg .= scalar @successList . ' pre-configured worlds have';
            }

            $msg .= ' been incorporated into ' . $axmud::SCRIPT . '\'s data files; your options now'
                    . ' are to (1) restore the entire ' . $axmud::SCRIPT . ' data directory from'
                    . ' backup, or (2) manually edit the config file to add the worlds: \''
                    . join(' ', @successList) . '\'';
        }

        $msg .= '. Click \'OK\' to shut down ' . $axmud::SCRIPT . '.';

        # (Show the message both in the 'main' window, and in a 'dialogue' window, so that logfiles
        #   can read it)
        $session->writeText($msg);

        $session->mainWin->showMsgDialogue(
            'Operation failed',
            'error',
            $msg,
            'ok',
        );

        return $axmud::CLIENT->stop();
    }
}

{ package Games::Axmud::Cmd::ListRestoreWorld;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listrestoreworld', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lrw', 'listrestoreworld'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows the list of pre-configured worlds';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $count,
            %hash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the registry of world profiles for quick lookup
        %hash = $axmud::CLIENT->worldProfHash;
        if (! %hash) {

            return $self->complete(
                $session, $standardCmd,
                'The pre-configured world list is empty',
            );
        }

        # Display header
        $session->writeText('List of pre-configured worlds (* - world profile exists)');

        # Display list
        foreach my $name ($axmud::CLIENT->constWorldList) {

            my $column;

            if (exists $hash{$name}) {
                $column = ' * ';
            } else {
                $column = '   ';
            }

            $session->writeText($column . $name);
        }

        # Display footer
        $count = scalar (keys %hash);
        if ($count == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 pre-configured world found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . $count . ' pre-configured worlds found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::UpdateWorld;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('updateworld', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['uwo', 'updateworld'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Updates the current world profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $importPath,
            $check,
        ) = @_;

        # Local variables
        my (
            $extractObj, $tempDir, $dataHashRef, $otherWorldObj, $choice, $version,
            @fileList, @worldList,
        );

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->improper($session, $inputString);
        }

        # (Code adapted from GA::Cmd::ImportFiles->do)

        # Check that loading is allowed at all
        if (! $axmud::CLIENT->loadDataFlag) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'File load/save is disabled in all sessions',
            );
        }

        # If a file path was not specified, open a file chooser dialog to decide which file to
        #   import
        if (! $importPath) {

            $importPath = $session->mainWin->showFileChooser(
                'Import file',
                'open',
                $axmud::SHARE_DIR . '/items/worlds/',
            );

            if (! $importPath) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->complete($session, $standardCmd, 'File(s) not imported');
            }
        }

        # Check that $importPath is a valid compressed file (ending .tar, .tar.gz, .tgz, .gz, .zip,
        #   .bz2, .tar.bz2, .tbz or .lzma)
        if (
            ! ($importPath =~ m/\.tar$/)
            && ! ($importPath =~ m/\.tgz$/)
            && ! ($importPath =~ m/\.gz$/)
            && ! ($importPath =~ m/\.zip$/)
            && ! ($importPath =~ m/\.bz2$/)
            && ! ($importPath =~ m/\.tbz$/)
            && ! ($importPath =~ m/\.lzma$/)
        ) {
            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'File not imported (you specified something that doesn\'t appear to be a'
                . ' compressed archive, e.g. a .zip or .tar.gz file)',
            );
        }

        # Build an Archive::Extract object
        $extractObj = Archive::Extract->new(archive => $importPath);
        if (! $extractObj) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'File not imported (file decompression error)',
            );
        }

        # Extract the object to a temporary directory
        $tempDir = $axmud::DATA_DIR . '/data/temp';
        if (! $extractObj->extract(to => $tempDir)) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'File not imported (file decompression error)',
            );
        }

        # All the files are now in /data/temp/export. Get a list of paths, relative to $tempDir, of
        #   all the extracted files
        @fileList = @{$extractObj->files};  # e.g. export/tasks.axm
        # Convert all the paths into absolute paths
        foreach my $file (@fileList) {

            $file = $axmud::DATA_DIR . '/data/temp/' . $file;
        }

        # Extract from @fileList all of the temporary files which are 'worldprof' files
        OUTER: foreach my $file (@fileList) {

            my (
                $matchFlag,
                %headerHash,
            );

            # Ignore files that don't end with a compatible file extension (like .axm)
            INNER: foreach my $ext (@axmud::COMPAT_EXT_LIST) {

                if ($file =~ m/\.$ext$/) {

                    $matchFlag = TRUE;
                    last INNER;
                }
            }

            if (! $matchFlag) {

                next OUTER;
            }

            # Check it's really an Axmud file by loading the file into a hash
            %headerHash = $axmud::CLIENT->configFileObj->examineDataFile($file, 'return_header');

            # Only keep 'worldprof' files
            if (
                exists $headerHash{'file_type'}
                && $headerHash{'file_type'} eq 'worldprof'
            ) {
                push (@worldList, $file);
            }
        }

        # @worldList should only contain one file
        if (! @worldList) {

            return $self->error(
                $session, $inputString,
                'File not imported (does not contain a world profile)',
            );

        } elsif (scalar @worldList > 1) {

            return $self->error(
                $session, $inputString,
                'File not imported (contains ' . scalar @worldList . ' world profiles)',
            );
        }

        # Load all the temporary file's data (including its header) into a hash reference
        eval { $dataHashRef = Storable::lock_retrieve($worldList[0]); };
        if (! $dataHashRef) {

            return $self->error(
                $session, $inputString,
                'World not updated (file load error)',
            );
        }

        # Extract the temporary file's world profile
        $otherWorldObj = $$dataHashRef{'world_prof'};
        $version = $$dataHashRef{'script_version'};

        # If the temporary profile's name isn't the same as the current world profile's name, show
        #   a warning
        if ($otherWorldObj->name ne $session->currentWorld->name) {

            $choice = $session->mainWin->showMsgDialogue(
                'Update world',
                'question',
                'Are you sure you want to transfer properties from the importable world \''
                . $otherWorldObj->name . '\' into the current world \''
                . $session->currentWorld->name . '\'?',
                'yes-no',
            );

            if (! $choice || $choice ne 'yes') {

                return $self->complete(
                    $session, $standardCmd,
                    'World update cancelled',
                );
            }
        }

        # Transfer data from the temporary profile into the current world profile
        if (! $session->currentWorld->mergeData($otherWorldObj, $version)) {

            return $self->error(
                $session, $inputString,
                'World not updated (general error)',
            );

        } else {

            # Force profile-dependent tasks to reset, as if a current profile had been changed
            $session->set_currentProfChangeFlag();

            # If the Status task's counters are running, reset their values, and turn them off
            if ($session->statusTask) {

                $session->statusTask->update_profiles();
            }

            return $self->complete(
                $session, $standardCmd,
                'Transferred data from the importable world profile \'' . $otherWorldObj->name
                . '\' into the current world profile',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListBasicWorld;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listbasicworld', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lbw', 'listbasic', 'listbasicworld'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Displays the basic mudlist';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there are some basic worlds to list
        if (! $axmud::CLIENT->constBasicWorldHash) {

            return $self->error(
                $session, $inputString,
                'The basic mudlist was not loaded, or was empty',
            );
        }

        # Display header
        $session->writeText('Basic mudlist (* adult or sexual themes, + world profile exists)');
        $session->writeText('    Name             Long name                        Host');

        # Display list
        @list = sort {lc($a->name) cmp lc($b->name)}
                    ($axmud::CLIENT->ivValues('constBasicWorldHash'));

        foreach my $obj (@list) {

            my ($column, $profileFlag, $longName, $host);

            if ($obj->adultFlag) {
                $column = ' *';
            } else {
                $column = '  ';
            }

            if ($axmud::CLIENT->ivExists('worldProfHash', $obj->name)) {
                $column .= '+ ';
            } else {
                $column .= '  ';
            }

            # Can't guarantee that the IVs will be set
            if ($obj->longName) {
                $longName = $obj->longName;
            } else {
                $longName = '(not set)';
            }

            if ($obj->host) {

                $host = $obj->host;
                if ($obj->port) {

                    $host .= ' ' . $obj->port;
                }

            } else {

                $host = '(not set)';
            }

            $session->writeText(
                $column . sprintf('%-16.16s %-32.32s %-32.32s', $obj->name, $longName, $host),
            );
        }

        # Display footer
        if ((scalar @list) == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 world found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . (scalar @list) . ' worlds found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ToggleHistory;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('togglehistory', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tgh', 'toghis', 'togglehistory'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles collection of a world\'s connection history';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (! $axmud::CLIENT->connectHistoryFlag) {

            $axmud::CLIENT->set_connectHistoryFlag(TRUE);

            return $self->complete(
                $session, $standardCmd,
                $axmud::SCRIPT . ' is now collecting each world\'s connection history',
            );

        } else {

            $axmud::CLIENT->set_connectHistoryFlag(FALSE);

            return $self->complete(
                $session, $standardCmd,
                $axmud::SCRIPT . ' has stopped collecting each world\'s connection history',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ClearHistory;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('clearhistory', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['clh', 'clearhis', 'clearhistory'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Clears the current world\'s connection history';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $choice;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there are some connection history objects for this world to clear
        if (! $session->currentWorld->connectHistoryList) {

            return $self->error(
                $session, $inputString,
                'The current world profile hasn\'t stored a connection history',
            );
        }

        # Better get a confirmation
        $choice = $session->mainWin->showMsgDialogue(
            'Clear history',
            'question',
            'Are you sure you want to clear the connection history stored in \''
            . $session->currentWorld->name . '?',
            'yes-no',
        );

        if ($choice eq 'yes') {

            $session->currentWorld->ivEmpty('connectHistoryList');

            return $self->complete(
                $session, $standardCmd,
                'Connection history for \'' . $session->currentWorld->name . '\' cleared',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Operation cancelled',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ShowHistory;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('showhistory', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['shh', 'showhis', 'showhistory'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Displays the current world\'s connection history';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $filter,
            $check,
        ) = @_;

        # Local variables
        my (
            $profObj, $status, $title,
            @list, @modList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the character exists, if specified
        if ($filter) {

            $profObj = $session->ivShow('profHash', $filter);
            if (! $profObj) {

                return $self->error(
                    $session, $inputString,
                    'The character profile \'' . $filter . '\' doesn\'t exist',
                );

            } elsif ($profObj->category ne 'char') {

                return $self->error(
                    $session, $inputString,
                    'The profile \'' . $filter . '\' is not a character profile',
                );
            }
        }

        # Compile a list of connection history objects, filtered by character, if necessary
        @list = $session->currentWorld->connectHistoryList;
        foreach my $historyObj (@list) {

            if (! $filter || $filter eq $historyObj->char) {

                push (@modList, $historyObj);
            }
        }

        # Display header
        $session->writeText('Connection history status');
        if (! $axmud::CLIENT->connectHistoryFlag) {
            $status = 'is NOT';
        } else {
            $status = 'is';
        }

        $session->writeText(
            '   ' . $axmud::SCRIPT . ' ' . $status . ' collecting connection histories',
        );

        $title = 'Connection history for world \'' . $session->currentWorld->name;
        if ($filter) {
            $title .= '\' (char: \'' . $filter . '\')';
        } else {
            $title .= '\'',
        }

        $session->writeText($title . ' (* - current connection, [...] estimated)');

        if (! @modList) {

            if ($filter) {
                $session->writeText('   (the filtered list is empty)');
            } else {
                $session->writeText('   (the list is empty)');
            }

        } else {


            $session->writeText(
                '   Char             Attempted                        Connect  Disconnect Length',
            );

            # Display list
            foreach my $historyObj (@modList) {

                my ($column, $char, $cTime, $cdTime, $dcTime, $len);

                if (
                    $session->connectHistoryObj
                    && $session->connectHistoryObj eq $historyObj
                ) {
                    $column = ' * ';
                } else {
                    $column = '   ';
                }

                if (! defined $historyObj->char) {
                    $char = '(not set)';
                } else {
                    $char = $historyObj->char;
                }

                if (defined $historyObj->connectingTime) {

                    $cTime = $axmud::CLIENT->convertTime($historyObj->connectingTime, 'date') . ', '
                                . $axmud::CLIENT->convertTime($historyObj->connectingTime, 'clock');
                }

                if (defined $historyObj->connectedTime) {

                    $cdTime = $axmud::CLIENT->convertTime($historyObj->connectedTime, 'clock');
                }

                if (! $session->connectHistoryObj || $session->connectHistoryObj ne $historyObj) {

                    if (! defined $historyObj->disconnectedTime) {

                        # Axmud crashed or was terminated abruptly
                        $dcTime = '['
                            . $axmud::CLIENT->convertTime($historyObj->currentTime, 'clock') . ']';

                    } else {

                        $dcTime
                            = $axmud::CLIENT->convertTime($historyObj->disconnectedTime, 'clock');
                    }

                } else {

                    $dcTime = 'connected';
                }

                if (defined $historyObj->connectionLength) {

                    if (
                        ! defined $historyObj->disconnectedTime
                        && (
                            ! $session->connectHistoryObj
                            || $session->connectHistoryObj ne $historyObj
                        )
                    ) {
                        $len = '[' . $axmud::CLIENT->getCounter($historyObj->connectionLength)
                                    . ']';

                    } else {

                        $len = $axmud::CLIENT->getCounter($historyObj->connectionLength);
                    }
                }

                $session->writeText(
                    $column
                    . sprintf('%-16.16s %-32.32s %-8.8s %-10.10s', $char, $cTime, $cdTime, $dcTime)
                    . ' ' . $len,
                );
            }
        }

        # Display footer
        if ((scalar @modList) == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 record found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . (scalar @modList) . ' records found)',
            );
        }
    }
}

# Profiles - other profiles

{ package Games::Axmud::Cmd::AddGuild;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addguild', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ag', 'addguild'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a guild profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Add the profile
        return $self->addProfile($session, $inputString, $standardCmd, $name, 'guild');
    }
}

{ package Games::Axmud::Cmd::SetGuild;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setguild', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sg', 'setguild'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the current guild';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Set the current profile
        return $self->setProfile($session, $inputString, $standardCmd, $name, 'guild');
    }
}

{ package Games::Axmud::Cmd::UnsetGuild;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('unsetguild', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ug', 'unsetguild'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Unsets the current guild';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Unset the current profile
        return $self->unsetProfile($session, $inputString, $standardCmd, 'guild');
    }
}

{ package Games::Axmud::Cmd::CloneGuild;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('cloneguild', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cg', 'copyguild', 'cloneguild'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Clones a guild profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $original, $copy,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $original || ! defined $copy || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Clone the profile
        return $self->cloneProfile($session, $inputString, $standardCmd, $original, $copy, 'guild');
    }
}

{ package Games::Axmud::Cmd::EditGuild;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editguild', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['eg', 'editguild'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a guild profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $profName,
            $check,
        ) = @_;

        # Local variables
        my $profObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that profile exists
        if (! defined $profName) {

            if (! $session->currentGuild) {

                return $self->error(
                    $session, $inputString,
                    'Could not edit the current guild profile - no current profile set',
                );

            } else {

                $profName = $session->currentGuild->name;
                $profObj = $session->currentGuild;
            }

        } elsif (! $session->ivExists('profHash', $profName)) {

            return $self->error(
                $session, $inputString,
                'Could not edit the guild profile \'' . $profName . '\' - profile does not exist',
            );

        } else {

            $profObj = $session->ivShow('profHash', $profName);
        }

        # Open an 'edit' window for the profile
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Profile::Guild',
                $session->mainWin,
                $session,
                'Edit guild profile \'' . $profObj->name . '\'',
                $profObj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the guild profile \'' . $profName . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the guild profile \'' . $profName . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteGuild;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteguild', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dg', 'delguild', 'deleteguild'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a guild profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Delete the profile
        return $self->deleteProfile($session, $inputString, $standardCmd, $name, 'guild');
    }
}

{ package Games::Axmud::Cmd::ListGuild;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listguild', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lg', 'listguild'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists guild profiles';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # List the profiles
        return $self->listProfile($session, $inputString, $standardCmd, 'guild');
    }
}

{ package Games::Axmud::Cmd::AddRace;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addrace', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ar', 'addrace'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a race profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Add the profile
        return $self->addProfile($session, $inputString, $standardCmd, $name, 'race');
    }
}

{ package Games::Axmud::Cmd::SetRace;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setrace', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sr', 'setrace'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the current race';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Set the current profile
        return $self->setProfile($session, $inputString, $standardCmd, $name, 'race');
    }
}

{ package Games::Axmud::Cmd::UnsetRace;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('unsetrace', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ur', 'unsetrace'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Unsets the current race';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Unset the current profile
        return $self->unsetProfile($session, $inputString, $standardCmd, 'race');
    }
}

{ package Games::Axmud::Cmd::CloneRace;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('clonerace', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cr', 'copyrace', 'clonerace'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Clones a race profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $original, $copy,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $original || ! defined $copy || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Clone the profile
        return $self->cloneProfile($session, $inputString, $standardCmd, $original, $copy, 'race');
    }
}

{ package Games::Axmud::Cmd::EditRace;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editrace', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['er', 'editrace'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a race profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $profName,
            $check,
        ) = @_;

        # Local variables
        my $profObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that profile exists
        if (! defined $profName) {

            if (! $session->currentRace) {

                return $self->error(
                    $session, $inputString,
                    'Could not edit the current race profile - no current profile set',
                );

            } else {

                $profName = $session->currentRace->name;
                $profObj = $session->currentRace;
            }

        } elsif (! $session->ivExists('profHash', $profName)) {

            return $self->error(
                $session, $inputString,
                'Could not edit the race profile \'' . $profName . '\' - profile does not'
                . ' exist',
            );

        } else {

            $profObj = $session->ivShow('profHash', $profName);
        }

        # Open an 'edit' window for the profile
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Profile::Race',
                $session->mainWin,
                $session,
                'Edit race profile \'' . $profObj->name . '\'',
                $profObj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the race profile \'' . $profName . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the race profile \'' . $profName . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteRace;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleterace', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dr', 'delrace', 'deleterace'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a race profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Delete the profile
        return $self->deleteProfile($session, $inputString, $standardCmd, $name, 'race');
    }
}

{ package Games::Axmud::Cmd::ListRace;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listrace', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lr', 'listrace'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists race profiles';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # List the profiles
        return $self->listProfile($session, $inputString, $standardCmd, 'race');
    }
}

{ package Games::Axmud::Cmd::AddChar;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addchar', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ach', 'addchar'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a character profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name, $pwd, $account,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Add the profile
        return $self->addProfile(
            $session,
            $inputString,
            $standardCmd,
            $name,
            'char',
            $pwd,
            $account,
        );
    }
}

{ package Games::Axmud::Cmd::SetChar;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setchar', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sch', 'setchar'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the current character';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name, $pwd, $account,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Set the current profile
        return $self->setProfile(
            $session,
            $inputString,
            $standardCmd,
            $name,
            'char',
            $pwd,
            $account,
        );
    }
}

{ package Games::Axmud::Cmd::UnsetChar;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('unsetchar', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['uch', 'unsetchar'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Unsets the current character';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Unset the current profile
        return $self->unsetProfile($session, $inputString, $standardCmd, 'char');
    }
}

{ package Games::Axmud::Cmd::CloneChar;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('clonechar', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cch', 'copychar', 'clonechar'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Clones a character profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $original, $copy,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $original || ! defined $copy || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Clone the profile
        return $self->cloneProfile($session, $inputString, $standardCmd, $original, $copy, 'char');
    }
}

{ package Games::Axmud::Cmd::EditChar;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editchar', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ech', 'editchar'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a character profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $profName,
            $check,
        ) = @_;

        # Local variables
        my $profObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that profile exists
        if (! defined $profName) {

            if (! $session->currentChar) {

                return $self->error(
                    $session, $inputString,
                    'Could not edit the current character profile - no current profile set',
                );

            } else {

                $profName = $session->currentChar->name;
                $profObj = $session->currentChar;
            }

        } elsif (! $session->ivExists('profHash', $profName)) {

            return $self->error(
                $session, $inputString,
                'Could not edit the character profile \'' . $profName . '\' - profile does not'
                . ' exist',
            );

        } else {

            $profObj = $session->ivShow('profHash', $profName);
        }

        # Open an 'edit' window for the profile
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Profile::Char',
                $session->mainWin,
                $session,
                'Edit character profile \'' . $profObj->name . '\'',
                $profObj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the character profile \'' . $profName . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the character profile \'' . $profName . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteChar;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletechar', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dch', 'delchar', 'deletechar'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a character profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Delete the profile
        return $self->deleteProfile($session, $inputString, $standardCmd, $name, 'char');
    }
}

{ package Games::Axmud::Cmd::ListChar;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listchar', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lch', 'listchar'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists character profiles';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # List the profiles
        return $self->listProfile($session, $inputString, $standardCmd, 'char');
    }
}

# Profiles - custom profiles

{ package Games::Axmud::Cmd::AddCustomProfile;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addcustomprofile', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['acp', 'addcustom', 'addcustomprof', 'addcustomprofile'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a custom profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $profile, $templ,
            $check,
        ) = @_;

        # Local variables
        my ($profObj, $templObj);

        # Check for improper arguments
        if (! defined $profile || ! defined $templ || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that <templ> isn't one of the standard profile categories ('world', 'guild',
        #   'race', 'char'), which don't exist as templates
        if (defined $axmud::CLIENT->ivFind('constProfPriorityList', $templ)) {

            return $self->error(
                $session, $inputString,
                'Cannot create custom profiles for that category - \'' . $templ
                . '\' is a standard profile category that doesn\'t use templates',
            );
        }

        # Check that <templ> exists
        if (! $session->ivExists('templateHash', $templ)) {

            return $self->error(
                $session, $inputString,
                'Could not use ' . $templ . ' profile template - profile template doesn\'t exist',
            );

        } else {

            $templObj = $session->ivShow('templateHash', $templ);
        }

        # Check that <name> isn't an existing profile
        if ($session->ivExists('profHash', $profile)) {

            return $self->error(
                $session, $inputString,
                'Could not add custom profile \'' . $profile . '\' - profile already exists',
            );
        }

        # Check that $profile is a valid name
        if (! $axmud::CLIENT->nameCheck($profile, 16)) {

            return $self->error(
                $session, $inputString,
                'Could not add custom profile \'' . $profile . '\' - invalid name',
            );
        }

        # Create the new custom profile (which marks the template as 'fixed', if it isn't already)
        $profObj = $templObj->spawn($session, $profile, $session->currentWorld->name);
        if (! $profObj) {

            return $self->error(
                $session, $inputString,
                'Could not add the custom profile \'' . $profile . '\'',
            );
        }

        # Create its associated cages
        if (! $session->createCages($profObj)) {

            return $self->error(
                $session, $inputString,
                'Could not add the custom profile \'' . $profile . '\' - errors while creating'
                . ' cages',
            );
        }

        # Update IVs
        $session->add_prof($profObj);
        # Tell the current world it's acquired a new associated definiton
        $session->currentWorld->ivAdd('profHash', $profile, $templ);

        return $self->complete(
            $session, $standardCmd,
            'Added custom profile \'' . $profile . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::SetCustomProfile;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setcustomprofile', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['scp', 'setcustom', 'setcustomprof', 'setcustomprofile'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets a current custom profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $profile, $templ,
            $check,
        ) = @_;

        # Local variables
        my ($profObj, $templObj, $oldObj, $currentObj, $fixedFlag);

        # Check for improper arguments
        if (! defined $profile || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there are no 'free' windows open
        if ($axmud::CLIENT->desktopObj->listSessionFreeWins($session, TRUE)) {

            return $self->error(
                $session, $inputString,
                'Can\'t set a current profile while there are edit, preference and wizard windows'
                . ' open (try closing them first)',
            );
        }

        # The user can only omit <templ> if the custom profile <name> already exists
        if (! $templ) {

            if (! $session->ivExists('profHash', $profile)) {

                return $self->error(
                    $session, $inputString,
                    'Could not set custom profile \'' . $profile . '\' - profile does not yet exist'
                    . ' (try \';setcustomprofile <name> <templ>\')',
                );

            } else {

                $profObj = $session->ivShow('profHash', $profile);
                $templ = $profObj->category;
            }
        }

        # Check that <templ> isn't one of the standard profile categories ('world', 'guild',
        #   'race', 'char'), which don't exist as templates
        if (defined $axmud::CLIENT->ivFind('constProfPriorityList', $templ)) {

            return $self->error(
                $session, $inputString,
                'Cannot set custom profiles for that category - \'' . $templ
                . '\' is a standard profile category, not a profile template',
            );
        }

        # Check that <templ> exists
        if (! $session->ivExists('templateHash', $templ)) {

            return $self->error(
                $session, $inputString,
                'Could not use ' . $templ . ' profile template - profile template doesn\'t exist',
            );

        } else {

            $templObj = $session->ivShow('templateHash', $templ);
        }

        # If the profile already exists, check it isn't already a current profile
        if ($session->ivExists('currentProfHash', $templ)) {

            $currentObj = $session->ivShow('currentProfHash', $templ);
            if ($currentObj->name eq $profile) {

                # Error message depends on whether it's the right kind of profile
                if ($currentObj->category eq $templ) {

                    return $self->error(
                        $session, $inputString,
                        'The profile \'' . $profile . '\' is already the current ' . $templ
                        . ' profile',
                    );

                } else {

                    return $self->error(
                        $session, $inputString,
                        'The profile \'' . $profile . '\' is the current ' . $currentObj->category
                        . ' profile',
                    );
                }
            }
        }

        # If there is already a current custom profile of this category, we need to remove all its
        #   interfaces before replacing them with ones belonging to the new custom profile.
        #   Remembers its blessed reference so we can do that, later in this function
        if ($session->ivExists('currentProfHash', $templ)) {

            $oldObj = $session->ivShow('currentProfHash', $templ);
        }

        # Use the existing custom profile and its existing related objects
        if ($session->ivExists('profHash', $profile)) {

            $profObj = $session->ivShow('profHash', $profile);

            # Check it's the right category of profile
            if ($profObj->category ne $templ) {

                return $self->error(
                    $session, $inputString,
                    'The profile \'' . $profile . '\' is a ' . $profObj->category . ' profile',
                );
            }

            # If there was already a current profile of this category, remove its interfaces
            if ($oldObj) {

                $session->resetProfileInterfaces($oldObj->name);
            }

            # Update IVs
            $session->add_currentProf($profObj); # Also sets ->currentGuild, ->currentProfChangeFlag
            # Tell the current world it's acquired a new associated definiton
            $session->currentWorld->ivAdd('profHash', $profile, $templ);

            # Set up cages for the new current profile
            $session->setCurrentCages($profile, $templ);

            # Set cage inferiors
            $session->setCageInferiors();
            # Add new interfaces for this profile's cages
            $session->setProfileInterfaces($profile);

            # If there's a current character, inform it it's acquired a new custom profile
            if ($session->currentChar) {

                $session->currentChar->ivAdd('customProfHash', $profile, $templ);
            }

            # If the Status task's counters are running, reset their values, and turn them off
            if ($session->statusTask) {

                $session->statusTask->update_profiles();
            }

            return $self->complete(
                $session, $standardCmd,
                'Set the profile \'' . $profile . '\' as the current ' . $templ . ' profile',
            );

        # Otherwise create a new profile, and make it the current one
        } else {

            # Check that $profile is a valid name
            if (! $axmud::CLIENT->nameCheck($profile, 16)) {

                return $self->error(
                    $session, $inputString,
                    'Could not add custom profile \'' . $profile . '\' - invalid name',
                );
            }

            # Remember whether the template is marked as 'fixed' or not, in case we need to destroy
            #   the new profile
            $fixedFlag = $templObj->constFixedFlag;

            # If there was already a current profile of this category, remove its interfaces
            if ($oldObj) {

                $session->resetProfileInterfaces($oldObj->name);
            }

            # Create the new custom profile (which marks the template as 'fixed', if it isn't
            #   already)
            $profObj = $templObj->spawn($session, $profile, $session->currentWorld->name);
            if (! $profObj) {

                return $self->error(
                    $session, $inputString,
                    'Could not add the custom profile \'' . $profile . '\'',
                );
            }

            # Update IVs now, because the call to ->createCages won't work without them
            $session->add_prof($profObj);
            $session->add_currentProf($profObj); # Also sets ->currentGuild, ->currentProfChangeFlag

            # Create its associated cages
            if (! $session->createCages($profObj, TRUE)) {

                # Some objects couldn't be created. Destroy any newly-created cages, if any (don't
                #   specify the TRUE flag because $profObj isn't a current profile yet)
                $session->destroyCages($profObj);

                # Unset the IVs set above
                $session->del_prof($profObj);
                $session->del_currentProf($templ);

                # Restore the old value of the template's ->constFixedFlag (since the value hasn't
                #   actually changed, we don't have to set the parent file object's ->modifyFlag)
                $templObj->{constFixedFlag} = $fixedFlag;

                return $self->error(
                    $session, $inputString,
                    'Could not add ' . $templ .' profile \'' . $profile . '\' - errors while'
                    . ' creating cages',
                );
            }

            # Tell the current world it's acquired a new associated definiton
            $session->currentWorld->ivAdd('profHash', $profile, $templ);
            # If there's a current character, inform it it's acquired a new custom profile
            if ($session->currentChar) {

                $session->currentChar->ivAdd('customProfHash', $profile, $templ);
            }

            # Update IVs
            $session->add_prof($profObj);
            # Tell the current world it's acquired a new associated definiton
            $session->currentWorld->ivAdd('profHash', $profile, $templ);

            # If the Status task's counters are running, reset their values, and turn them off
            if ($session->statusTask) {

                $session->statusTask->update_profiles();
            }

            return $self->complete(
                $session, $standardCmd,
                'Added \'' . $profile . '\' as the current ' . $templ . ' profile',
            );
        }
    }
}

{ package Games::Axmud::Cmd::UnsetCustomProfile;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('unsetcustomprofile', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'ucp',
            'unsetcustom',
            'unsetcustomprof',
            'unsetcustomprofile',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Unsets a current custom profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $profile,
            $check,
        ) = @_;

        # Local variables
        my $profObj;

        # Check for improper arguments
        if (! defined $profile || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there are no 'free' windows open
        if ($axmud::CLIENT->desktopObj->listSessionFreeWins($session, TRUE)) {

            return $self->error(
                $session, $inputString,
                'Can\'t unset a current profile while there are edit, preference and wizard windows'
                . ' open (try closing them first)',
            );
        }

        # Check that the profile exists
        if (! $session->ivExists('profHash', $profile)) {

            return $self->error(
                $session, $inputString,
                'The profile \'' . $profile . '\' doesn\'t exist',
            );

        } else {

            $profObj = $session->ivShow('profHash', $profile);
        }

        # Check it's a current custom profile
        if (! $session->ivExists('templateHash', $profObj->category)) {

            return $self->error(
                $session, $inputString,
                'The profile \'' . $profile . '\' isn\'t a custom profile',
            );

        } elsif (
            ! $session->ivExists('currentProfHash', $profObj->category)
            || $session->ivShow('currentProfHash', $profObj->category) ne $profObj
        ) {
            return $self->error(
                $session, $inputString,
                'The profile \'' . $profile . '\' isn\'t a current profile',
            );
        }

        # If there's a current character, inform it it's lost a custom profile
        if ($session->currentChar) {

            $session->currentChar->ivDelete('customProfHash', $profile);
        }

        # Remove this profile's interfaces
        $session->resetProfileInterfaces($profObj->name);
        # Unset any cages for this profile as current cages
        $session->unsetCurrentCages($profile, $profObj->category);
        # Unset the profile as a current defintitions
        $session->del_currentProf($profObj->category);

        # If the Status task's counters are running, reset their values, and turn them off
        if ($session->statusTask) {

            $session->statusTask->update_profiles();
        }

        return $self->complete(
            $session, $standardCmd,
            'The current ' . $profObj->category . ' profile has been unset as a current profile',
        );
    }
}

{ package Games::Axmud::Cmd::CloneCustomProfile;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('clonecustomprofile', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'ccp',
            'clonecustom',
            'clonecustomprof',
            'clonecustomprofile',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Clones a custom profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $original, $copy,
            $check,
        ) = @_;

        # Local variables
        my ($originalObj, $category);

        # Check for improper arguments
        if (! defined $original || ! defined $copy || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that the $original profile exists (without which, we can't find its ->category)
        if (! $session->ivExists('profHash', $original)) {

            return $self->error(
                $session, $inputString,
                'Could not clone the custom profile \'' . $original . '\' - the profile doesn\'t'
                . ' exist',
            );

        } else {

            $originalObj = $session->ivShow('profHash', $original);
            $category = $originalObj->category;
        }

        # Check it's a custom profile
        if (! $session->ivExists('templateHash', $category)) {

            return $self->error(
                $session, $inputString,
                'Could not clone the custom profile \'' . $original . '\' - the profile is not a'
                . ' custom profile',
            );
        }

        # Clone the profile
        return $self->cloneProfile(
            $session,
            $inputString,
            $standardCmd,
            $original,
            $copy,
            $category,
        );
    }
}

{ package Games::Axmud::Cmd::EditCustomProfile;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editcustomprofile', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ecp', 'editcustom', 'editcustomprof', 'editcustomprofile'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a custom profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $profName,
            $check,
        ) = @_;

        # Local variables
        my $profObj;

        # Check for improper arguments
        if (! defined $profName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that profile exists
        if (! $session->ivExists('profHash', $profName)) {

            return $self->error(
                $session, $inputString,
                'Could not edit the custom profile \'' . $profName . '\' - profile does not exist',
            );

        } else {

            $profObj = $session->ivShow('profHash', $profName);
        }

        # Open an 'edit' window for the profile
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Profile::Custom',
                $session->mainWin,
                $session,
                'Edit ' . $profObj->category . ' profile \'' . $profObj->name . '\'',
                $profObj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the custom profile \'' . $profName . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the ' . $profObj->category . ' profile \'' . $profName
                . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteCustomProfile;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletecustomprofile', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'dcp',
            'delcustom',
            'delcustomprof',
            'deletecustomprofile',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a custom profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $profile,
            $check,
        ) = @_;

        # Local variables
        my ($profObj, $category);

        # Check for improper arguments
        if (! defined $profile || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there are no 'free' windows open
        if ($axmud::CLIENT->desktopObj->listSessionFreeWins($session, TRUE)) {

            return $self->error(
                $session, $inputString,
                'Can\'t delete a profile while there are edit, preference and wizard windows'
                . ' open (try closing them first)',
            );
        }

        # Check that the original profile exists (without which, we can't find its
        #   ->category)
        if (! $session->ivExists('profHash', $profile)) {

            return $self->error(
                $session, $inputString,
                'Could not delete the custom profile \'' . $profile . '\' - the profile doesn\'t'
                . ' exist',
            );

        } else {

            $profObj = $session->ivShow('profHash', $profile);
            $category = $profObj->category;
        }

        # Check it's a custom profile
        if (! $session->ivExists('templateHash', $category)) {

            return $self->error(
                $session, $inputString,
                'Could not delete the custom profile \'' . $profile . '\' - the profile is not a'
                . ' custom profile',
            );
        }

        # Delete the profile
        return $self->deleteProfile($session, $inputString, $standardCmd, $profile, $category);
    }
}

{ package Games::Axmud::Cmd::ListCustomProfile;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listcustomprofile', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lcp', 'listcustom', 'listcustomprof',  'listcustomprofile'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists custom profiles';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $templ,
            $check,
        ) = @_;

        # Local variables
        my (
            @customList, @sortedList,
            %standardHash,
        );
        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Put the standard profile categories 'world', 'guild', 'race' and 'char' into a hash for
        #   quick lookup
        foreach my $category ($axmud::CLIENT->constProfPriorityList) {

            $standardHash{$category} = undef;
        }

        # Compile a list of all custom profiles
        foreach my $obj ($session->ivValues('profHash')) {

            if (
                (defined $templ && $obj->category eq $templ)
                || (! defined $templ && ! exists $standardHash{$obj->category})
            ) {
                push (@customList, $obj);
            }
        }

        # Sort the list alphabetically
        @sortedList = sort {lc($a->name) cmp lc($b->name)} (@customList);
        if (! @sortedList) {

            return $self->complete($session, $standardCmd, 'The custom profile list is empty');
        }

        # Display header
        $session->writeText('List of custom profiles (* = current profile)');
        $session->writeText('   Profile name     Category');

        # Display list
        foreach my $obj (@sortedList) {

            my $column;

            if (
                $session->ivExists('currentProfHash', $obj->category)
                && $session->ivShow('currentProfHash', $obj->category) eq $obj
            ) {
                $column = ' * ';
            } else {
                $column = '   ';
            }

            # Display the race's details
            $session->writeText(
                $column . sprintf('%-16.16s %-16.16s', $obj->name, $obj->category),
            );
        }

        # Display footer
        if (@sortedList == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 custom profile found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @sortedList . ' custom profiles found)',
            );
        }


        # List the profiles
        return $self->listProfile($session, $inputString, $standardCmd, 'char');
    }
}

# Cages

{ package Games::Axmud::Cmd::EditCage;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editcage', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ecg', 'editcage'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a cage';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my ($cageObj, $nameFlag, $package, $winObj);

        # Check for improper arguments
        if (! defined $arg || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get the cage
        if ($arg eq '-t') {
            $cageObj = $session->findCurrentCage('trigger', 'world');
        } elsif ($arg eq '-a') {
            $cageObj = $session->findCurrentCage('alias', 'world');
        } elsif ($arg eq '-m') {
            $cageObj = $session->findCurrentCage('macro', 'world');
        } elsif ($arg eq '-i') {
            $cageObj = $session->findCurrentCage('timer', 'world');
        } elsif ($arg eq '-h') {
            $cageObj = $session->findCurrentCage('hook', 'world');
        } elsif ($arg eq '-c') {
            $cageObj = $session->findCurrentCage('cmd', 'world');
        } elsif ($arg eq '-r') {
            $cageObj = $session->findCurrentCage('route', 'world');
        } else {

            # $arg is a cage name. Check the cage exists and isn't a cage mask
            $cageObj = $session->ivShow('cageHash', $arg);
            if (! defined $cageObj) {

                return $self->error(
                    $session, $inputString,
                    'Could not edit the \'' . $arg . '\' cage - object does not exist',
                );

            } elsif ($cageObj->isa('Games::Axmud::Generic::CageMask')) {

                return $self->error(
                    $session, $inputString,
                    '\'' . $arg . '\' is a cage mask - try \';editcagemask\' instead',
                );

            } else {

                $nameFlag = TRUE;
            }
        }

        if (! $cageObj) {

            return $self->error(
                $session, $inputString,
                'Could not edit the cage - object not found',
            );
        }

        # Work out the package name of the correct cage 'edit' window
        if ($axmud::CLIENT->ivExists('pluginCageEditWinHash', $cageObj->cageType)) {

            # Cage 'edit' window added by a plugin
            $package = $axmud::CLIENT->ivShow('pluginCageEditWinHash', $cageObj->cageType);

        } else {

            # Built-in cage 'edit' window
            $package = 'Games::Axmud::EditWin::Cage::' . ucfirst($cageObj->cageType);
        }

        # Open up an 'edit' window to edit the object
        $winObj = $session->mainWin->createFreeWin(
            $package,
            $session->mainWin,
            $session,
            'Edit \'' . $cageObj->profName . '\' ' . $cageObj->cageType . ' cage',
            $cageObj,
            FALSE,                          # Not temporary
        );

        if (! $winObj) {

            return $self->error(
                $session, $inputString,
                'Could not edit the \'' . $cageObj->profName . '\' ' . $cageObj->cageType . ' cage',
            );

        } else {

            # Open the cage at its second tab (so that the triggers, aliases etc are visible
            #   immediately)
            $winObj->notebook->set_current_page(1);

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the \'' . $cageObj->profName . '\' '
                . $cageObj->cageType . ' cage',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteCage;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletecage', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dcg', 'delcage', 'deletecage'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Removes all instances of an obsolete cage type';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $type,
            $check,
        ) = @_;

        # Local variables
        my $count;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # This command can't be used to delete instances of built-in cages
        if (defined $type && defined $axmud::CLIENT->ivFind('constCageTypeList', $type)) {

            return $self->error(
                $session, $inputString,
                'Instances of built-in cages cannot be deleted using this command',
            );
        }

        # Delete all instances of the cage $type in each session. If $type was not specified, delete
        #   all cages with unrecognised types
        foreach my $otherSession ($axmud::CLIENT->listSessions()) {

            # (The TRUE flag means don't display a message for each deletion. $type can be 'undef')
            my $result = $otherSession->deleteCages($type, TRUE);
            if ($result) {

                $count += $result;
            }
        }

        if (defined $type) {

            if (! $count) {

                return $self->error(
                    $session, $inputString,
                    'No instances of the \'' . $type . '\' cage type found in any session',
                );

            } elsif ($count == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Deleted 1 instance of the \'' . $type . '\' cage type',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Deleted ' . $count . ' instances of the \'' . $type . '\' cage type',
                );
            }

        } else {

            if (! $count) {

                return $self->error(
                    $session, $inputString,
                    'No cages with unrecognised types found in any session',
                );

            } elsif ($count == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Deleted 1 cage with an unrecognised cage type',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Deleted ' . $count . ' cages with an unrecognised cage type',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ListCage;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listcage', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lcg', 'listcage'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists cages';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $type, $cageType, $longFlag, $level,
            @slaveList, @sortedList, @unsortedList,
            %unsortedHash, %slaveHash, %sortedHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If $switch was specified, check it is valid. At the same time, compile a hash of cages to
        #   be listed
        if (defined $switch) {

            if ($switch eq '-c') {

                $type = 'command';
                $cageType = 'cmd';

            } elsif ($switch eq '-t') {

                $type = $cageType = 'trigger';

            } elsif ($switch eq '-a') {

                $type = $cageType = 'alias';

            } elsif ($switch eq '-m') {

                $type = $cageType = 'macro';

            } elsif ($switch eq '-i') {

                $type = $cageType = 'timer';

            } elsif ($switch eq '-h') {

                $type = $cageType = 'hook';

            } elsif ($switch eq '-r') {

                $type = $cageType = 'route';

            } elsif ($switch eq '-x') {

                $type = 'other';
                # Non-standard cages
                $cageType = undef;

            } else {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised switch \'' . $switch . '\' - use -c, -t, -a, -m, -i, -h or -x',
                );
            }

            foreach my $cageObj ($session->ivValues('cageHash')) {

                if (
                    defined $cageType
                    && $cageObj->cageType eq $cageType
                ) {
                    # This cage is of the right type; add it to %unsortedHash
                    $unsortedHash{$cageObj->name} = $cageObj;

                } elsif (
                    ! defined $cageType
                    && ! $cageObj->standardFlag
                ) {
                    # This is a non-standard cage, so add it to %unsortedHash
                    $unsortedHash{$cageObj->name} = $cageObj;
                }
            }

        } else {

            # Use all cages
            $type = 'all';
            %unsortedHash = $session->cageHash;
        }

        # Now, from %unsortedHash, extract all the cages that have no inferior
        foreach my $key (keys %unsortedHash) {

            if (! $session->ivShow('inferiorCageHash', $key)) {

                $slaveHash{$key} = $unsortedHash{$key};
                delete $unsortedHash{$key};
            }
        }

        # Convert %slaveHash - all the cage that have no inferior - into a list, and sort it in
        #   ascending alphabetical order
        @slaveList = sort {lc($a) cmp lc($b)} (keys %slaveHash);

        # Now - for each cage in the slave list, add it to the sorted list, and then add its
        #   inferiors - one by one, in order of inferiority
        OUTER: foreach my $slave (@slaveList) {

            my ($failFlag, $lastCage);

            $failFlag = FALSE;
            $lastCage = $slave;

            push (@sortedList, $slave);
            $sortedHash{$slave} = $slaveHash{$slave};

            # Search the ordinary hash, looking for a cage for which $slave is the inferior. If
            #   found, add it to the sorted list and look for the inferior for THAT cage; if not
            #   found, move on to the next item on the list
            while (! $failFlag) {

                $failFlag = TRUE;
                INNER: foreach my $cageName (keys %unsortedHash) {

                    my $inferiorObj = $session->ivShow('inferiorCageHash', $cageName);

                    if ($inferiorObj && $inferiorObj->name eq $lastCage) {

                        # This cage is the superior of the previous one
                        push (@sortedList, $cageName);
                        $sortedHash{$cageName} = $unsortedHash{$cageName};
                        delete $unsortedHash{$cageName};

                        $lastCage = $cageName;
                        $failFlag = FALSE;
                        last INNER;
                    }
                }
            }
        }

        # Final step - cage names can be up to 42 characters. Go throught the whole sorted list and
        #   work out the length of the longest cage name. If it's 32 characters or less, can display
        #   cages in 2 columns; otherwise we'll have to display them in one column
        OUTER: foreach my $cage (@sortedList) {

            if (length($cage) > 32) {

                $longFlag = TRUE;
                last OUTER;
            }
        }

        # As usual, don't display a list, if the list is empty...
        if (! @sortedList) {

            return $self->complete($session, $standardCmd, 'The cage list is empty');
        }

        # Display header
        $session->writeText(
            'List of ' . $type . ' cages (inferiors listed first) (* = current, x = unattached)',
        );

        if (! $longFlag) {

            $session->writeText('   Cage name                        Inferior cage');
        }

        # Display list
        $level = 0;
        foreach my $cage (@sortedList) {

            my ($obj, $gap, $inferiorObj, $string);

            $obj = $sortedHash{$cage};

            if ($session->ivExists('currentCageHash', $obj->name)) {
                $gap = ' * ';
            } else {
                $gap = '   ';
            }

            $inferiorObj = $session->ivShow('inferiorCageHash', $cage);
            if ($inferiorObj) {

                $level++;
                $string = '';
                # Display strings in a cascade, depending on the number of inferiors they have
                for (my $count = 0; $count < $level; $count++) {

                    $string .= ' ';
                }

                if ($longFlag) {

                    $session->writeText($gap . $string . sprintf('%-42.42s', $cage));

                } else {

                    $session->writeText(
                        $gap . $string . sprintf(
                            '%-32.32s %-32.32s',
                            $cage,
                            $inferiorObj->name,
                        )
                    );
                }

            } else {

                $level = 0;

                if ($longFlag) {
                    $session->writeText($gap . sprintf('%-42.42s', $cage));
                } else {
                    $session->writeText($gap . sprintf('%-32.32s (No inferior cage)', $cage));
                }
            }
        }

        # Display anything remaining in %unsortedHash (just in case)
        @unsortedList = sort {lc($a) cmp lc($b)} (keys %unsortedHash);
        foreach my $cage (@unsortedList) {

            my ($obj, $inferiorObj, $inferiorName);

            $obj = $unsortedHash{$cage};
            $inferiorObj = $session->ivShow('inferiorCageHash', $obj->name);
            if ($inferiorObj) {
                $inferiorName = $inferiorObj->name;
            } else {
                $inferiorName = '';
            }

            $session->writeText(
                ' x ' . sprintf(
                    '%-32.32s %-32.32s',
                    $unsortedHash{$cage}->name,
                    $inferiorName,
                )
            );
        }

        # Display footer
        if (@sortedList == 1) {

            return $self->complete($session, $standardCmd, 'List displayed (1 cage found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'List displayed (' . @sortedList . ' cages found)',
            );
        }

    }
}

{ package Games::Axmud::Cmd::SetCageMask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setcagemask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['smk', 'setmask', 'setcagemask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Manages cage masks';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $initialFlagCount, $switch, $string, $addFlag, $iv, $unsetFlag, $listFlag, $taskName,
            $applyNowFlag, $flagCount, $worldFlag, $guildFlag, $raceFlag, $charFlag, $category,
            $customFlag, $profName, $profFlag, $profObj, $cage, $cageType, $taskObj,
        );

        # Extract switches
        $initialFlagCount = 0;
        ($switch, $string, @args) = $self->extract('-a', 1, @args);
        if (defined $switch) {

            $addFlag = TRUE;
            $initialFlagCount++;
            $iv = $string;
        }

        ($switch, $string, @args) = $self->extract('-u', 1, @args);
        if (defined $switch) {

            $unsetFlag = TRUE;
            $initialFlagCount++;
            $iv = $string;
        }

        ($switch, @args) = $self->extract('-l', 0, @args);
        if (defined $switch) {

            $listFlag = TRUE;
            $initialFlagCount++;
        }

        ($switch, $taskName, @args) = $self->extract('-n', 1, @args);
        if (defined $switch) {

            $applyNowFlag = TRUE;
            $initialFlagCount++;
        }

        $flagCount = 0;
        ($switch, @args) = $self->extract('-w', 0, @args);
        if (defined $switch) {

            $worldFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-g', 0, @args);
        if (defined $switch) {

            $guildFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-r', 0, @args);
        if (defined $switch) {

            $raceFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-c', 0, @args);
        if (defined $switch) {

            $charFlag = TRUE;
            $flagCount++;
        }

        ($switch, $category, @args) = $self->extract('-x', 1, @args);
        if (defined $switch) {

            $customFlag = TRUE;
            $flagCount++;
        }

        ($switch, $profName, @args) = $self->extract('-d', 1, @args);
        if (defined $switch) {

            $profFlag = TRUE;
            $flagCount++;
        }

        # The contents of @args is now (<type>, <data>), though <data> is optional
        $cageType = shift @args;

        # Check for incompatible flags
        if ($initialFlagCount > 1) {

            return $self->error(
                $session, $inputString,
                'The switches -a, -u, -l and -n can\'t be combined',
            );

        } elsif (
            ! defined $cageType
            || ($initialFlagCount == 0)
            || (! $iv && ! $listFlag && ! $applyNowFlag)
            || ($addFlag && ! @args)
            || ($unsetFlag && @args)
            || ($listFlag && ($iv || @args))
            || ($customFlag && ! $category)
            || ($profFlag && ! $profName)
        ) {
            return $self->improper($session, $inputString);

        } elsif (
            ($guildFlag && ! $session->currentGuild)
            || ($raceFlag && ! $session->currentRace)
            || ($charFlag && ! $session->currentChar)
        ) {

            return $self->error(
                $session, $inputString,
                'There is no current profile matching the specified profile',
            );

        } elsif ($customFlag && ! $session->ivExists('currentProfHash', $category)) {

            return $self->error(
                $session, $inputString,
                'The profile category \'' . $category . '\' wasn\'t recognised',
            );

        } elsif ($profFlag && ! $session->ivExists('profHash', $profName)) {

            return $self->error(
                $session, $inputString,
                'The profile \'' . $profName . '\' doesn\'t exist',
            );
        }

        # Find the profile specified. The default profile is the current world profile
        if ($worldFlag) {
            $profObj = $session->currentWorld;
        } elsif ($guildFlag) {
            $profObj = $session->currentGuild;
        } elsif ($raceFlag) {
            $profObj = $session->currentRace;
        } elsif ($charFlag) {
            $profObj = $session->currentChar;
        } elsif ($customFlag) {
            $profObj = $session->ivShow('currentProfHash', $category);
        } elsif ($profFlag) {
            $profObj = $session->ivShow('profHash', $profName);
        } else {
            $profObj = $session->currentWorld;
        }

        if (! $profObj) {

            return $self->error($session, $inputString, 'General error setting the profile');
        }

        $cage = $session->findCage($cageType, $profObj->name);
        if (! $cage) {

            return $self->error(
                $session, $inputString,
                'Couldn\'t find a cage mask of the type \'' . $cageType . '  associated with the'
                . ' profile \'' . $profObj->name . '\'',
            );
        }

        # Check that the specified IV exists
        if (! $listFlag && ! $applyNowFlag && ! $cage->ivExists('maskHash', $iv)) {

            return $self->error(
                $session, $inputString,
                'The mask doesn\'t seem to have the IV \'' . $iv . '\'',
            );
        }

        # ;smk <type> <switch> -a <iv> <data>
        if ($addFlag) {

            my (
                $scalar,
                @list,
                %hash,
            );

            if ($iv =~ m/Hash$/) {

                %hash = @args;
                $cage->ivAdd('maskHash', $iv, \%hash);

                return $self->complete(
                    $session, $standardCmd,
                    'Mask\'s hash IV \'' . $iv . '\' set',
                );

            } elsif ($iv =~ m/List$/) {

                @list = @args;
                $cage->ivAdd('maskHash', $iv, \@list);

                return $self->complete(
                    $session, $standardCmd,
                    'Mask\'s list IV \'' . $iv . '\' set',
                );

            } else {

                # Assume it's a scalar
                if (@args > 1) {

                    return $self->error(
                        $session, $inputString,
                        'The IV \'' . $iv . '\' is a scalar IV, but you specified multiple values',
                    );

                } else {

                    $scalar = $args[0];
                    $cage->ivAdd('maskHash', $iv, \$scalar);

                    return $self->complete(
                        $session, $standardCmd,
                        'Mask\'s scalar IV \'' . $iv . '\' set',
                    );
                }
            }

        # ;smk <type> <switch> -u <iv>
        } elsif ($unsetFlag) {

            $cage->ivPoke('maskHash', $iv, undef);
            return $self->complete($session, $standardCmd, 'Mask\'s IV \'' . $iv . '\' unset');

        # ;smk <type> <switch> -l
        } elsif ($listFlag) {

            my (
                $ref, $refType, $count,
                @list, @contentsList,
            );

            # Display header
            $session->writeText(
                '\'' . $cageType . '\' cage mask for the profile \'' . $profObj->name . '\'',
            );

            # Display list
            @list = $cage->maskList;
            $count = 0;
            foreach $iv (@list) {

                # (Don't specify $session so that the cage doesn't consult other cages)
                $ref = $cage->ivShow('maskHash', $iv);

                if (! defined $ref) {

                    # This IV is not set in the sepcified mask
                    $session->writeText(sprintf(' - %-32.32s <not set>', $iv));

                } else {

                    $count++;
                    $refType = ref $ref;

                    if ($refType eq 'SCALAR') {

                        $session->writeText(sprintf(' S %-32.32s %-32.32s', $iv, $$ref));

                    } elsif ($refType eq 'ARRAY') {

                        $session->writeText(
                            sprintf(' L %-32.32s %-48.48s', $iv, (join(', ', @$ref))),
                        );

                    } elsif ($refType eq 'HASH') {

                        @contentsList = %$ref;
                        $session->writeText(
                            sprintf(' H %-32.32s %-48.48s', $iv, (join(', ', @contentsList))),
                        );
                    }
                }
            }

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of list (' . $count . '/' . scalar @list . ' IVs found)',
            );

        # ;smk <type> <switch> -n <task_name>
        } else {

            # Get the corresponding task
            $taskObj = $session->ivShow('currentTaskNameHash', $taskName);
            if (! $taskObj) {

                return $self->error(
                    $session, $inputString,
                    'Can\'t find the current task called \'' . $taskName . '\'',
                );
            }

            # Apply the mask
            if (! $cage->applyMask($session, $taskObj)) {

                return $self->error(
                    $session, $inputString,
                    'General error applying the \'' . $cageType . '\' cage mask',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Cage mask \'' . $cageType . '\' applied to the task \''. $taskName . '\'',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::EditCageMask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editcagemask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['emk', 'editmask', 'editcagemask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a cage mask';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $cageName,
            $check,
        ) = @_;

        # Local variables
        my ($cageObj, $package, $winObj);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the cage mask exists and is a cage mask, not another kind of cage
        $cageObj = $session->ivShow('cageHash', $cageName);
        if (! defined $cageObj) {

            return $self->error(
                $session, $inputString,
                'Could not edit the \'' . $cageName . '\' cage mask- object does not exist',
            );

        } elsif (! $cageObj->isa('Games::Axmud::Generic::CageMask')) {

            return $self->error(
                $session, $inputString,
                '\'' . $cageName . '\' is not a cage mask - try \';editcage\' instead',
            );
        }

        # Work out the package name of the correct cage 'edit' window
        if ($axmud::CLIENT->ivExists('pluginCageEditWinHash', $cageObj->cageType)) {

            # Cage 'edit' window added by a plugin
            $package = $axmud::CLIENT->ivShow('pluginCageEditWinHash', $cageObj->cageType);

        } else {

            # Built-in cage 'edit' window
            $package = 'Games::Axmud::EditWin::CageMask::' . ucfirst($cageObj->cageType);
        }

        # Open up an 'edit' window to edit the object
        $winObj = $session->mainWin->createFreeWin(
            $package,
            $session->mainWin,
            $session,
            'Edit \'' . $cageObj->profName . '\' ' . $cageObj->cageType . ' cage mask',
            $cageObj,
            FALSE,                          # Not temporary
        );

        if (! $winObj) {

            return $self->error(
                $session, $inputString,
                'Could not edit the \'' . $cageObj->profName . '\' ' . $cageObj->cageType
                . ' cage mask',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the \'' . $cageObj->profName . '\' '
                . $cageObj->cageType . ' cage mask',
            );
        }
    }
}

# Interfaces

{ package Games::Axmud::Cmd::EnableActiveInterface;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('enableactiveinterface', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['eai', 'enableinterface', 'enableactiveinterface'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Enables active interfaces';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my (
            $count, $alreadyCount,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Compile a list of active interfaces to modify
        if (defined $arg) {

            # Enable only one interface
            if ($session->ivExists('interfaceNumHash', $arg)) {

                push (@list, $session->ivShow('interfaceNumHash', $arg));

            } elsif ($session->ivExists('interfaceHash', $arg)) {

                push (@list, $session->ivShow('interfaceHash', $arg));

            } else {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised active interface \'' . $arg . '\'',
                );
            }

        } else {

            # Enable all active interfaces
            @list = $session->ivValues('interfaceHash');
        }

        if (! @list) {

            return $self->error(
                $session, $inputString,
                'No active interfaces found',
            );
        }

        # Enable each interface in turn
        $count = 0;
        $alreadyCount = 0;
        foreach my $obj (@list) {

            if ($obj->enabledFlag) {

                $alreadyCount++;

            } else {

                $count++;
                $obj->set_enabledFlag(TRUE);
            }
        }

        # Display confirmation
        return $self->complete(
            $session, $standardCmd,
            'Active interfaces enabled : ' . $count . ' (interfaces already enabled: '
            . $alreadyCount . ')',
        );
    }
}

{ package Games::Axmud::Cmd::DisableActiveInterface;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('disableactiveinterface', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dai', 'disableinterface', 'disableactiveinterface'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Disables active interfaces';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my (
            $count, $alreadyCount,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Compile a list of active interfaces to modify
        if (defined $arg) {

            # Enable only one interface
            if ($session->ivExists('interfaceNumHash', $arg)) {

                push (@list, $session->ivShow('interfaceNumHash', $arg));

            } elsif ($session->ivExists('interfaceHash', $arg)) {

                push (@list, $session->ivShow('interfaceHash', $arg));

            } else {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised active interface \'' . $arg . '\'',
                );
            }

        } else {

            # Disable all active interfaces
            @list = $session->ivValues('interfaceHash');
        }

        if (! @list) {

            return $self->error(
                $session, $inputString,
                'No active interfaces found',
            );
        }

        # Disable each interface in turn
        $count = 0;
        $alreadyCount = 0;
        foreach my $obj (@list) {

            if (! $obj->enabledFlag) {

                $alreadyCount++;

            } else {

                $obj->set_enabledFlag(FALSE);
                $count++;
            }

        }

        # Display confirmation
        return $self->complete(
            $session, $standardCmd,
            'Active interfaces disabled : ' . $count . ' (interfaces already disabled: '
            . $alreadyCount . ')',
        );
    }
}

{ package Games::Axmud::Cmd::MoveActiveInterface;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('moveactiveinterface', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mai', 'moveinterface', 'moveactiveinterface'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Moves active interfaces';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $interfaceNum, $posn,
            $check,
        ) = @_;

        # Local variables
        my ($obj, $iv);

        # Check for improper arguments
        if (! defined $interfaceNum || ! defined $posn || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that both arguments are positive integers
        if (! $axmud::CLIENT->intCheck($interfaceNum, 1)) {

            return $self->error(
                $session, $inputString,
                'Invalid interface number \'' . $interfaceNum . '\'',
            );

        } elsif (! $axmud::CLIENT->intCheck($posn, 1)) {

            return $self->error(
                $session, $inputString,
                'Invalid position \'' . $interfaceNum . '\' (must be a positive integer)',
            );
        }

        # Check that the interface exists
        if (! $session->ivExists('interfaceNumHash', $interfaceNum)) {

            return $self->error(
                $session, $inputString,
                'Active interface #' . $interfaceNum . ' not found',
            );
        }

        # Get the interface object
        $obj = $session->ivShow('interfaceNumHash', $interfaceNum);
        # Identify the registry which stores this interface
        $iv = $obj->category . 'OrderList';      # e.g. ->triggerOrderList

        # Check that $posn isn't outside the bounds of the registry
        if ($posn > (scalar $session->$iv)) {

            return $self->error(
                $session, $inputString,
                'Invalid position \'' . $interfaceNum . '\' (there are only '
                . scalar $session->$iv . ' active interfaces of the category ' . $obj->category
                . ')',
            );
        }

        # Move the interface to its new position
        if (! $session->move_interface($iv, $obj, $posn)) {

            return $self->error(
                $session, $inputString,
                'Could not move active interface \'' . $obj->name . '\'',
            );

        } else {

            # The IV index 0 corresponds to the 'position' 1, so we add 1 to $posn
            return $self->complete(
                $session, $standardCmd,
                'Active interfaces \'' . $obj->name . '\' moved to position #' . ($posn + 1),
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditActiveInterface;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editactiveinterface', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['eda', 'editactive', 'editactiveinterface'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for an active interface';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my ($editWin, $obj);

        # Check for improper arguments
        if ( defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;eda
        if (! defined $arg) {

            # Open up a 'pref' window to edit the GA::Session
            $editWin = $session->mainWin->createFreeWin(
                'Games::Axmud::PrefWin::Session',
                $session->mainWin,
                $session,
                'Session preferences',
            );

            # Open the notebook's second page, so the user can see the list of active interfaces
            #   immediately
            $editWin->notebook->set_current_page(1);
            if (! $editWin) {

                return $self->error(
                    $session, $inputString,
                    'Could not open the session preference window to show active interfaces',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Opened session preference window to show active interfaces',
                );
            }

        # eda <name>
        # eda <number>
        } else {

            # Check the active interface exists
            if ($session->ivExists('interfaceNumHash', $arg)) {

                $obj = $session->ivShow('interfaceNumHash', $arg);

            } elsif ($session->ivExists('interfaceHash', $arg)) {

                $obj = $session->ivShow('interfaceHash', $arg);

            } else {

                return $self->error(
                    $session, $inputString,
                    'Could not edit the active interface - interface \'' . $arg . '\' does not'
                    . ' exist',
                );
            }

            # Open an 'edit' window for the active interface
            if (
                ! $session->mainWin->createFreeWin(
                    'Games::Axmud::EditWin::Interface::Active',
                    $session->mainWin,
                    $session,
                    'Edit active ' . $obj->category . ' interface \'' . $obj->name . '\'',
                    $obj,
                    FALSE,                  # Not temporary
                )
            ) {
                return $self->error(
                    $session, $inputString,
                    'Could not edit the active interface \'' . $obj->name . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Opened \'edit\' window for the active interface \'' . $obj->name . '\'',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ListActiveInterface;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listactiveinterface', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lai', 'listactive', 'listinterface', 'listactiveinterface'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists active interfaces';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

       # Local variables
        my (
            $switch, $sortOrderFlag, $sortAlphaFlag, $sortNumFlag, $dependentFlag, $independentFlag,
            $verboseFlag, $categoryFlag, $category,
            @tempList, @list, @finalList, @sortedList, @ivList,
            %hash, %posnHash,
        );

        # Extract switches
        ($switch, @args) = $self->extract('-o', 0, @args);
        if (defined $switch) {

            $sortOrderFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-a', 0, @args);
        if (defined $switch) {

            $sortAlphaFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-n', 0, @args);
        if (defined $switch) {

            $sortNumFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-d', 0, @args);
        if (defined $switch) {

            $dependentFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-i', 0, @args);
        if (defined $switch) {

            $independentFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-v', 0, @args);
        if (defined $switch) {

            $verboseFlag = TRUE;
        }

        ($switch, $category, @args) = $self->extract('-c', 1, @args);
        if (defined $switch) {

            $categoryFlag = TRUE;
        }

        # There should be no further arguments
        if (@args || ($categoryFlag && ! defined $category)) {

            return $self->improper($session, $inputString);

        # The switches -o, -a and -n can't be combined
        } elsif (
            ($sortOrderFlag && ($sortAlphaFlag || $sortNumFlag))
            || ($sortAlphaFlag && $sortNumFlag)
        ) {
            return $self->error(
                $session, $inputString,
                'The switches -o, -a and -n can\'t be combined (most other combinations of switches'
                . ' are allowed',
            );
        }

        # Set a list of the registries of active interfaces, stored in the order in which they're
        #   checked
        @ivList = (
            'triggerOrderList',
            'aliasOrderList',
            'macroOrderList',
            'timerOrderList',
            'hookOrderList',
        );

        # Compile a list of active interfaces to be displayed, depending on the switches specified
        if ($sortAlphaFlag) {

            @tempList = sort {lc($a->name) cmp lc($b->name)} ($session->ivValues('interfaceHash'));

        } elsif ($sortNumFlag) {

            @tempList
                = sort {$a->number <=> $b->number} ($session->ivValues('interfaceNumHash'));

        } else {

            foreach my $iv (@ivList) {

                foreach my $number ($session->$iv) {

                    push (@tempList, $session->ivShow('interfaceNumHash', $number));
                }
            }
        }

        # Also compile a hash of active interfaces and their positions in
        #   GA::Session->triggerOrderList (etc), with position 1 being the first position in the
        #   list. Hash in the form
        #   $posnHash{interface_number} = position
        foreach my $iv (@ivList) {

            my @orderedList = $session->$iv;
            my $count = 0;

            foreach my $interfaceNum (@orderedList) {

                $count++;
                $posnHash{$interfaceNum} = $count;
            }
        }

        # Now remove any active interfaces that aren't dependent or independent (if those switches
        #   have been specified)
        if ($dependentFlag && ! $independentFlag) {

            foreach my $obj (@tempList) {

                if (! $obj->indepFlag) {

                    push (@list, $obj);
                }
            }

        } elsif (! $dependentFlag && $independentFlag) {

            foreach my $obj (@tempList) {

                if ($obj->indepFlag) {

                    push (@list, $obj);
                }
            }

        } else {

            # Use all interfaces
            @list = @tempList;
        }

        # Remove all interfaces that are (or aren't) the right category (if those switches have
        #   been specified)
        if ($categoryFlag) {

            foreach my $obj (@list) {

                if ($category eq $obj->category) {

                   push (@finalList, $obj);
                }
            }

        } else {

            # Use all interfaces
            @finalList = @list;
        }

        if (! @finalList) {

            return $self->complete($session, $standardCmd, 'The active interface list is empty');
        }

        # Display header
        $session->writeText('Interface list (* = enabled, D = dependent, I = independent)');
        $session->writeText('       # Order Interface name                   Category Cooldown');

        # Display list
        foreach my $obj (@finalList) {

            my ($string, $name, $iv, $seconds, $format);

            if ($obj->enabledFlag) {
                $string = '*';      # Enabled
            } else {
                $string = ' ';      # Disabled
            }

            if ($obj->indepFlag) {
                $string .= 'I ';    # Independent interface
            } else {
                $string .= 'D ';    # Dependent interface
            }

            # Although interface names can be up to 64 characters, we'll show only the first 48
            $name = $obj->name;
            if (length $name > 48) {

                $name = substr($name, 0, 45) . '...';
            }

            # Show cooldown expire time, if set (NB timers don't have cooldowns)
            if ($obj->category ne 'timer') {

                $iv = $obj->category . 'CooldownHash';
                $seconds = $session->ivShow($iv, $obj->number);
                if (defined $seconds) {

                    $seconds = $seconds - $session->sessionTime;
                    if ($seconds < 0) {

                        # Already expired, but IV not updated yet
                        $seconds = undef;

                    } else {

                        $seconds = sprintf("%.1f", $seconds);
                    }
                }
            }

            if (! defined $seconds) {

                $session->writeText(
                    $string . sprintf (
                        '%5.5s %5.5s %-32.32s %-8.8s',
                        $obj->number,
                        $posnHash{$obj->number},
                        $name,
                        $obj->category,
                    ),
                );

            } else {

                $session->writeText(
                    $string . sprintf (
                        '%5.5s %5.5s %-32.32s %-8.8s %.1f',
                        $obj->number,
                        $posnHash{$obj->number},
                        $name,
                        $obj->category,
                        $seconds,
                    ),
                );
            }

            if ($verboseFlag) {

                $session->writeText(sprintf('           Stimulus: %-64.64s', $obj->stimulus));
                $session->writeText(sprintf('           Response: %-64.64s', $obj->response));

                if (! $obj->indepFlag) {

                    $session->writeText(sprintf('           Class:   %-64.64s', $obj->callClass));
                    $session->writeText(sprintf('           Method:  %-64.64s', $obj->callMethod));

                } elsif (defined $obj->assocProf) {

                    $session->writeText(
                        sprintf(
                            '           Associated profile: %-16.16s (Type: %-16.16s)',
                            $obj->assocProf,
                            $obj->assocProfCategory,
                        )
                    );
                }
            }
        }

        # Display footer
        if (@finalList == 1) {

            return $self->complete(
                $session, $standardCmd,
                   'End of list (1 active interface found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @finalList . ' active interfaces found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditInterfaceModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editinterfacemodel', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['eim', 'editinterfacemodel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for an interface model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $switch || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that $switch is valid
        if (
            $switch ne '-t'  && $switch ne 'trig' && $switch ne 'trigger'
            && $switch ne '-a' && $switch ne 'alias'
            && $switch ne '-m' && $switch ne 'macro'
            && $switch ne '-i' && $switch ne 'timer'
            && $switch ne '-h' && $switch ne 'hook'
        ) {
            return $self->error(
                $session, $inputString,
                'Invalid switch - use -t, -a, -m, -i (for timers) or -h',
            );
        }

        # Get the interface model object
        if ($switch eq '-t' || $switch eq 'trig' || $switch eq 'trigger') {

            $obj = $axmud::CLIENT->ivShow('interfaceModelHash', 'trigger');

        } elsif ($switch eq '-a' || $switch eq 'alias') {

            $obj = $axmud::CLIENT->ivShow('interfaceModelHash', 'alias');

        } elsif ($switch eq '-m' || $switch eq 'macro') {

            $obj = $axmud::CLIENT->ivShow('interfaceModelHash', 'macro');

        } elsif ($switch eq '-i' || $switch eq 'timer') {

            $obj = $axmud::CLIENT->ivShow('interfaceModelHash', 'timer');

        } elsif ($switch eq '-h' || $switch eq 'hook') {

            $obj = $axmud::CLIENT->ivShow('interfaceModelHash', 'hook');

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid switch - use -t, -a, -m, -i (for timers) or -h',
            );
        }

        # Open an 'edit' window for the interface model
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::InterfaceModel',
                $session->mainWin,
                $session,
                'Edit ' . $obj->category . ' interface model',
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the ' . $obj->category . ' interface model',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the ' . $obj->category . ' dictionary',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListInterfaceModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listinterfacemodel', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lim', 'listinterfacemodel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists default interface settings';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If $switch was specified, check it is valid
        if (
            defined $switch
            && $switch ne '-t'  && $switch ne 'trig' && $switch ne 'trigger'
            && $switch ne '-a' && $switch ne 'alias'
            && $switch ne '-m' && $switch ne 'macro'
            && $switch ne '-i' && $switch ne 'timer'
            && $switch ne '-h' && $switch ne 'hook'
        ) {
            return $self->error(
                $session, $inputString,
                'Invalid switch - use -t, -a, -m, -i (for timers) or -h',
            );
        }

        # Compile a list of interface models to use
        if (! defined $switch || $switch eq '-t' || $switch eq 'trig' || $switch eq 'trigger') {

            push (@list, $axmud::CLIENT->ivShow('interfaceModelHash', 'trigger'));
        }
        if (! defined $switch || $switch eq '-a' || $switch eq 'alias') {

            push (@list, $axmud::CLIENT->ivShow('interfaceModelHash', 'alias'));
        }
        if (! defined $switch || $switch eq '-m' || $switch eq 'macro') {

            push (@list, $axmud::CLIENT->ivShow('interfaceModelHash', 'macro'));
        }
        if (! defined $switch || $switch eq '-i' || $switch eq 'timer') {

            push (@list, $axmud::CLIENT->ivShow('interfaceModelHash', 'timer'));
        }
        if (! defined $switch || $switch eq '-h' || $switch eq 'hook') {

            push (@list, $axmud::CLIENT->ivShow('interfaceModelHash', 'hook'));
        }

        # (No header to display)

        # Display list
        foreach my $obj (@list) {

            my (@firstList, @modList, @sortedList);

            $session->writeText('\'' . $obj->category . '\' interface model');
            $session->writeText('   Stimulus: ' . $obj->stimulusName);
            $session->writeText('   Response: ' . $obj->responseName);

            # Use all the optional attributes except 'name' and 'enabled'
            @firstList = $obj->ivKeys('optionalSwitchHash');
            foreach my $item (@firstList) {

                if ($item ne 'name' && $item ne 'enabled') {

                    push (@modList, $item);
                }
            }
            # Sort alphabetically
            @sortedList = sort {$a cmp $b} (@modList);

            if (@sortedList) {

                $session->writeText('   Non-standard attributes:');

                foreach my $item (@sortedList) {

                    $session->writeText(
                        '      ' . $item . ' (type \'' . $obj->ivShow('attribTypeHash', $item)
                        . '\', switch: -' . $obj->ivShow('optionalSwitchHash', $item) . ')',
                    );
                }

            } else {

                $session->writeText('   Non-standard attributes: <none>');
            }
        }

        # Display footer
        if (@list == 1) {

            return $self->complete(
                $session, $standardCmd,
                   'End of list (1 interface model displayed)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @list . ' interface models displayed)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddTrigger;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addtrigger', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['atr', 'addtrig', 'addtrigger'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds an independent trigger';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Add the trigger
        return $self->addInterface(
            $session, $inputString, $standardCmd,
            'trigger',
            'triggers',
            $axmud::CLIENT->ivShow('interfaceModelHash', 'trigger'),
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::ModifyTrigger;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('modifytrigger', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'mtr',
            'modtrig',
            'modtrigger',
            'modifytrig',
            'modifytrigger',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies a trigger';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Modify the trigger
        return $self->modifyInterface(
            $session, $inputString, $standardCmd,
            'trigger',
            'triggers',
            $axmud::CLIENT->ivShow('interfaceModelHash', 'trigger'),
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::DeleteTrigger;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletetrigger', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'dtr',
            'deltrig',
            'deltrigger',
            'deletetrig',
            'deletetrigger',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a trigger';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Delete the trigger
        return $self->deleteInterface(
            $session, $inputString, $standardCmd,
            'trigger',
            'triggers',
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::ListTrigger;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listtrigger', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ltr', 'listtrig', 'listtrigger'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists triggers';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # (No imprope arguments to check)

        # List triggers
        return $self->listInterface(
            $session, $inputString, $standardCmd,
            'trigger',
            'triggers',
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::AddAlias;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addalias', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['aal', 'addal', 'addalias'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds an independent alias';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Add the alias
        return $self->addInterface(
            $session, $inputString, $standardCmd,
            'alias',
            'aliases',
            $axmud::CLIENT->ivShow('interfaceModelHash', 'alias'),
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::ModifyAlias;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('modifyalias', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mal', 'modal', 'modalias', 'modifyal', 'modifyalias'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies an alias';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Modify the alias
        return $self->modifyInterface(
            $session, $inputString, $standardCmd,
            'alias',
            'aliases',
            $axmud::CLIENT->ivShow('interfaceModelHash', 'alias'),
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::DeleteAlias;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletealias', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dal', 'delal', 'delalias', 'deleteal', 'deletealias'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes an alias';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Delete the alias
        return $self->deleteInterface(
            $session, $inputString, $standardCmd,
            'alias',
            'aliases',
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::ListAlias;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listalias', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lal', 'listal', 'listalias'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists aliases';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # (No imprope arguments to check)

        # List aliases
        return $self->listInterface(
            $session, $inputString, $standardCmd,
            'alias',
            'aliases',
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::AddMacro;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addmacro', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ama', 'addma', 'addmacro'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a macro';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Add the macro
        return $self->addInterface(
            $session, $inputString, $standardCmd,
            'macro',
            'macros',
            $axmud::CLIENT->ivShow('interfaceModelHash', 'macro'),
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::ModifyMacro;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('modifymacro', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mma', 'modma', 'modmacro', 'modifyma', 'modifymacro'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies a macro';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Modify the macro
        return $self->modifyInterface(
            $session, $inputString, $standardCmd,
            'macro',
            'macros',
            $axmud::CLIENT->ivShow('interfaceModelHash', 'macro'),
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::DeleteMacro;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletemacro', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dma', 'delma', 'delmacro', 'deletema', 'deletemacro'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a macro';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Delete the macro
        return $self->deleteInterface(
            $session, $inputString, $standardCmd,
            'macro',
            'macros',
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::ListMacro;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listmacro', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lma', 'listma', 'listmacro'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists macros';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # (No imprope arguments to check)

        # List macros
        return $self->listInterface(
            $session, $inputString, $standardCmd,
            'macro',
            'macros',
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::AddTimer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addtimer', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ati', 'addti', 'addtimer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a timer';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Add the timer
        return $self->addInterface(
            $session, $inputString, $standardCmd,
            'timer',
            'timers',
            $axmud::CLIENT->ivShow('interfaceModelHash', 'timer'),
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::ModifyTimer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('modifytimer', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mti', 'modti', 'modtimer', 'modifyti', 'modifytimer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies a timer';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Modify the timer
        return $self->modifyInterface(
            $session, $inputString, $standardCmd,
            'timer',
            'timers',
            $axmud::CLIENT->ivShow('interfaceModelHash', 'timer'),
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::DeleteTimer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletetimer', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dti', 'delti', 'deltimer', 'deleteti', 'deletetimer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a timer';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Delete the timer
        return $self->deleteInterface(
            $session, $inputString, $standardCmd,
            'timer',
            'timers',
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::ListTimer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listtimer', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lti', 'listti', 'listtimer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists timers';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # (No imprope arguments to check)

        # List timers
        return $self->listInterface(
            $session, $inputString, $standardCmd,
            'timer',
            'timers',
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::AddHook;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addhook', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ahk', 'addhk', 'addhook'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a hook';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Add the hook
        return $self->addInterface(
            $session, $inputString, $standardCmd,
            'hook',
            'hooks',
            $axmud::CLIENT->ivShow('interfaceModelHash', 'hook'),
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::ModifyHook;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('modifyhook', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mhk', 'modhk', 'modhook', 'modifyhk', 'modifyhook'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies a hook';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Modify the hook
        return $self->modifyInterface(
            $session, $inputString, $standardCmd,
            'hook',
            'hooks',
            $axmud::CLIENT->ivShow('interfaceModelHash', 'hook'),
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::DeleteHook;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletehook', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dhk', 'delhk', 'delhook', 'deletehk', 'deletehook'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a hook';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Delete the hook
        return $self->deleteInterface(
            $session, $inputString, $standardCmd,
            'hook',
            'hooks',
            @args,
        );
    }
}

{ package Games::Axmud::Cmd::ListHook;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listhook', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lhk', 'listhk', 'listhook'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists hooks';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # (No imprope arguments to check)

        # List hooks
        return $self->listInterface(
            $session, $inputString, $standardCmd,
            'hook',
            'hooks',
            @args,
        );
    }
}

# Keycodes

{ package Games::Axmud::Cmd::ListKeycode;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listkeycode', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lkc', 'listkc', 'listkeycode'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists keycodes for the current system';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if ((defined $switch && $switch ne '-s') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get a list of keycode types in their standard order
        @list = $axmud::CLIENT->constKeycodeList;
        if ($switch) {

            # Sort the list alphabetically
            @list = sort {lc($a) cmp lc($b)} (@list);
        }

        # Display header
        $session->writeText('List of keycodes for the current system');
        $session->writeText('   Standard keycode     System keycode(s)');

        # Display list
        foreach my $standard (@list) {

            $session->writeText(
                sprintf('   %-20.20s', $standard) . ' '
                . $axmud::CLIENT->ivShow('keycodeHash', $standard),
            );
        }

        # Display footer
        return $self->complete(
            $session, $standardCmd,
           'End of list (' . scalar @list . ' standard keycodes found)',
        );
    }
}

{ package Games::Axmud::Cmd::ListKeycodeAlternative;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listkeycodealternative', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lka', 'listka', 'listkeycodealternative'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists alternative forms for keycodes';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Display header
        $session->writeText('List of recognised alternatives for keycode types');
        $session->writeText('   Alternative              Standard keycode');

        # Display list
        @list = sort {lc($a) cmp lc($b)} ($axmud::CLIENT->ivKeys('constAltKeycodeHash'));
        foreach my $alt (@list) {

            $session->writeText(
                sprintf(
                    '   %-24.24s %-24.24s',
                    $alt,
                    $axmud::CLIENT->ivShow('constAltKeycodeHash', $alt),
                )
            );
        }

        # Display footer
        return $self->complete(
            $session, $standardCmd,
           'End of list (' . scalar @list . ' keycodes alternatives found)',
        );
    }
}

# Task package name

{ package Games::Axmud::Cmd::AddTaskPackage;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addtaskpackage', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['atp', 'addtp', 'addtaskpackage'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a task package name';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName, $packageName,
            $check,
        ) = @_;

        # Local variables
        my $text;

        # Check for improper arguments
        if (! defined $taskName || ! defined $packageName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that no tasks of this type are running
        if ($session->ivExists('currentTaskNameHash', $taskName)) {

            return $self->error(
                $session, $inputString,
                'Can\'t change the package name for the task \'' . $taskName . '\' while it is'
                . ' running (try halting the task first)',
            );
        }

        # Check that <task_name> doesn't already have a package name
        if ($axmud::CLIENT->ivExists('taskPackageHash', $taskName)) {
            $text = 'a new';
        } else {
            $text = 'the';
        }

        # Check that <task_name> is not too long
        if (length $taskName > 16) {

            return $self->error(
                $session, $inputString,
                'Task name \'' . $taskName . '\' is too long (max 16 characters)',
            );
        }

        # Add the package name
        $axmud::CLIENT->add_taskPackageName($taskName, $packageName);

        return $self->complete(
            $session, $standardCmd,
            'The \'' . $taskName . '\' task has been assigned ' . $text . ' package name \''
            . $packageName . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::DeleteTaskPackage;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletetaskpackage', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dtp', 'deltp', 'deletetp', 'deletetaskpackage'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a task package name';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName,
            $check,
        ) = @_;

        # Local variables
        my $packageName;

        # Check for improper arguments
        if (! defined $taskName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that a package name for $taskName exists
        if (! $axmud::CLIENT->ivExists('taskPackageHash', $taskName)) {

            return $self->error(
                $session, $inputString,
                'No task called \'' . $taskName . '\' has been assiged a package name',
            );
        }

        # Check that no tasks of this type are running
        if ($session->ivExists('currentTaskNameHash', $taskName)) {

            return $self->error(
                $session, $inputString,
                'Can\'t remove the package name for the task \'' . $taskName . '\' while it is'
                . ' running (try halting the task first)',
            );
        }

        # Delete the package name
        $packageName = $axmud::CLIENT->ivShow('taskPackageHash', $taskName);
        $axmud::CLIENT->del_taskPackageName($taskName);

        return $self->complete(
            $session, $standardCmd,
            'The package name \'' . $packageName . '\' assigned to the task \'' . $taskName
            . '\' has been deleted',
        );
    }
}

{ package Games::Axmud::Cmd::ResetTaskPackage;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resettaskpackage', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rtp', 'resettp', 'resettaskpackage'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets task package names to defaults';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName,
            $check,
        ) = @_;

        # Local variables
        my ($deleteCount, $replaceCount, $packageName);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;rtp <task_name>
        if ($taskName) {

            # Check that <task_name> is a known task
            if (
                ! $axmud::CLIENT->ivExists('taskPackageHash', $taskName)
                && ! $axmud::CLIENT->ivExists('constTaskPackageHash', $taskName)
            ) {
                return $self->error(
                    $session, $inputString,
                    'The \'' . $taskName . '\' task isn\'t recognised. If it exists, use'
                    . ' \';addtaskpackage\' first',
                );
            }

            # If the package name exists, remove it
            if ($axmud::CLIENT->ivExists('taskPackageHash', $taskName)) {

                $axmud::CLIENT->del_taskPackageName($taskName);
                $deleteCount = 1;
            }

            # If a default version exists, use it to replace the old version
            if ($axmud::CLIENT->ivExists('constTaskPackageHash', $taskName)) {

                $axmud::CLIENT->add_taskPackageName(
                    $taskName,
                    $axmud::CLIENT->ivShow('constTaskPackageHash', $taskName),
                );

                $replaceCount = 1;
            }

            if (! $deleteCount && ! $replaceCount) {

                return $self->error(
                    $session, $inputString,
                    'Could not reset the package name for the \'' . $taskName . '\' task - general'
                    . ' error locating the task',
                );

            } elsif ($deleteCount && $replaceCount) {

                return $self->complete(
                    $session, $standardCmd,
                    'Package name for the \'' . $taskName .  '\' task has been replaced with \'' .
                    $axmud::CLIENT->ivShow('taskPackageHash', $taskName) . '\'',
                );

            } elsif ($deleteCount) {

                return $self->complete(
                    $session, $standardCmd,
                    'Package name for the \'' . $taskName .  '\' task has been removed (and not'
                    . ' replaced)',
                );

            } elsif ($replaceCount) {

                return $self->complete(
                    $session, $standardCmd,
                    'The default package name for the \'' . $taskName .  '\' task has been'
                    . ' reinstated',
                );
            }

        # ;rtp
        } else {

            # Just reset the hashes
            $deleteCount = $axmud::CLIENT->ivPairs('taskPackageHash');
            $replaceCount = $axmud::CLIENT->ivPairs('constTaskPackageHash');
            $axmud::CLIENT->reset_taskPackageNames();

            return $self->complete(
                $session, $standardCmd,
                'Operation complete, task packages removed: ' . $deleteCount
                . ', task packages replaced: ' . $replaceCount,
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListTaskPackage;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listtaskpackage', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ltp', 'listtp', 'listtaskpackage'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists task package names';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            @sortedList,
            %hash,
        );

        # Check for improper arguments
        if ((defined $switch && $switch ne '-d') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get a list of task packages
        if (! $switch) {

            # Current task package names
            %hash = $axmud::CLIENT->taskPackageHash;

        } else {

            # Default task package names
            %hash = $axmud::CLIENT->constTaskPackageHash;
        }

        # Convert the hash to a list and sort alphabetically
        @sortedList = sort {lc($a) cmp lc($b)} (keys %hash);
        if (! @sortedList) {

            return $self->complete($session, $standardCmd, 'The task package list is empty');
        }

        # Display header
        if (! $switch) {
            $session->writeText('Current list of task package names');
        } else {
            $session->writeText('Default list of task package names');
        }
        $session->writeText('   Task name            Package name');

        # Display list
        foreach my $task (@sortedList) {

            $session->writeText('   '. sprintf('%-16.16s     %-32.32s', $task, $hash{$task}));
        }

        # Display footer
        if (! $switch) {

            if (@sortedList == 1) {

                return $self->complete($session, $standardCmd, '1 default task package name found');

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    @sortedList . ' default task package names found',
                );
            }

        } else {

            if (@sortedList == 1) {

                return $self->complete($session, $standardCmd, '1 current task package name found');

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    @sortedList . ' current task package names found',
                );
            }
        }
    }
}

# Task label

{ package Games::Axmud::Cmd::AddTaskLabel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addtasklabel', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['atl', 'addtl', 'addlabel', 'addtasklabel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds task labels';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName,
            @labelList,
        ) = @_;

        # Local variables
        my (
            $count,
            %labelHash,
        );

        # Check for improper arguments
        if (! defined $taskName) {

            return $self->improper($session, $inputString);
        }

        # Check that <task_name> is a known task
        if (! $axmud::CLIENT->ivExists('taskPackageHash', $taskName)) {

            return $self->error(
                $session, $inputString,
                'The \'' . $taskName . '\' task isn\'t recognised. If it exists, use'
                . ' \';addtaskpackage\' first',
            );
        }

        # ;atl <task_name> <label_list>
        if (@labelList) {

            # Check that all the labels in <label_list> don't already exist, and that the labels are
            #   allowed
            foreach my $label (@labelList) {

                if ($axmud::CLIENT->ivExists('taskLabelHash', $label)) {

                    return $self->error(
                        $session, $inputString,
                        'The label \'' . $label . '\' already exists (and points to the \''
                        . $axmud::CLIENT->ivShow('taskLabelHash', $label) . '\' task)',
                    );
                }

                if (! $axmud::CLIENT->nameCheck($label, 32)) {

                    return $self->error(
                        $session, $inputString,
                        'Illegal task label name \'' . $label . '\'',
                    );
                }
            }

            # Add each new label in turn
            foreach my $label (@labelList) {

                $count++;
                $axmud::CLIENT->add_taskLabel($label, $taskName);
            }

            if (@labelList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    '1 label added to the \'' . $taskName . '\' task',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    @labelList . ' labels added to the \'' . $taskName . '\' task',
                );
            }

        # ;atl <task_name>
        } else {

            # Import IVs
            %labelHash = $axmud::CLIENT->taskLabelHash;

            # Remove all labels pointing at <task_name>
            $count = 0;
            foreach my $label (keys %labelHash) {

                if ($labelHash{$label} eq $taskName) {

                    $count++;
                    $axmud::CLIENT->del_taskLabel($label);
                }
            }

            if ($count == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    '1 label pointing to the \'' . $taskName . '\' task has been deleted',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    $count . ' labels pointing to the \'' . $taskName . '\' task have been deleted',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::DeleteTaskLabel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletetasklabel', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dtl', 'deltl', 'dellabel', 'deletetasklabel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a task label';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $label,
            $check,
        ) = @_;

        # Local variables
        my $taskName;

        # Check for improper arguments
        if (! defined $label || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that <label> exists
        if (! $axmud::CLIENT->ivExists('taskLabelHash', $label)) {

            return $self->error(
                $session, $inputString,
                'The task label \'' . $label . '\' doesn\'t exist'
            );
        }

        # Delete the label
        $taskName = $axmud::CLIENT->ivShow('taskLabelHash', $label);
        $axmud::CLIENT->del_taskLabel($label);

        return $self->complete(
            $session, $standardCmd,
            'The task label \'' . $label . '\' pointing at the \'' . $taskName . '\' task has been'
                . ' deleted',
        );
    }
}

{ package Games::Axmud::Cmd::ResetTaskLabel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resettasklabel', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rtl', 'resettl', 'resetlabel', 'resettasklabel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets task labels to default values';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName,
            $check,
        ) = @_;

        # Local variables
        my (
            $deleteCount, $replaceCount,
            %labelHash, %standardHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import IVs
        %labelHash = $axmud::CLIENT->taskLabelHash;
        %standardHash = $axmud::CLIENT->constTaskLabelHash;

        # ;rtl <task_name>
        if ($taskName) {

            # Check that <task_name> is a known task
            if (! $axmud::CLIENT->ivExists('taskPackageHash', $taskName)) {

                return $self->error(
                    $session, $inputString,
                    'The \'' . $taskName . '\' task isn\'t recognised. If it exists, use'
                    . ' \';addtaskpackage\' first',
                );
            }

            # Remove all task labels pointing at this task
            $deleteCount = 0;
            foreach my $label (keys %labelHash) {

                if ($labelHash{$label} eq $taskName) {

                    $axmud::CLIENT->del_taskLabel($label);
                    $deleteCount++;
                }
            }

            # Replace all default task labels pointing at this task
            $replaceCount = 0;
            foreach my $label (keys %standardHash) {

                if ($standardHash{$label} eq $taskName) {

                    $axmud::CLIENT->add_taskLabel($label, $taskName);
                    $replaceCount++;
                }
            }

        # ;rtl
        } else {

            # Just replace the whole hash
            $deleteCount = scalar (keys %labelHash);
            $replaceCount = scalar (keys %standardHash);

            $axmud::CLIENT->reset_taskLabels();
        }

        return $self->complete(
            $session, $standardCmd,
            'Operation complete, task labels removed: ' . $deleteCount . ', task labels replaced: '
            . $replaceCount,
        );
    }
}

{ package Games::Axmud::Cmd::ListTaskLabel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listtasklabel', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ltl', 'listtl', 'listlabel', 'listtasklabel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists task labels';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            @sortedList,
            %labelHash, %reversedHash,
        );

        # Check for improper arguments
        if ((defined $switch && $switch ne '-b') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get a list of task packages
        if (! $switch) {

            # Current task package names
            %labelHash = $axmud::CLIENT->taskLabelHash;

        } else {

            # Default task package names
            %labelHash = $axmud::CLIENT->constTaskLabelHash;
        }

        # Reverse the list without losing any non-unique key-value pairs
        %reversedHash = $self->reverseHash(%labelHash);
        # Convert the hash to a list and sort it alphabetically
        @sortedList = sort {lc($a) cmp lc($b)} (keys %reversedHash);
        if (! @sortedList) {

            return $self->complete($session, $standardCmd, 'The task label list is empty');
        }

        # Display header
        $session->writeText('List of task labels');

        # Display list
        foreach my $taskName (@sortedList) {

            $session->writeText('   '. $taskName . ' : ' . $reversedHash{$taskName});
        }

        # Display footer
        if ($switch) {

            if (@sortedList == 1) {

                return $self->complete($session, $standardCmd, '1 built-in task found');

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    @sortedList . ' built-in tasks found',
                );
            }

        } else {

            if (@sortedList == 1) {
                return $self->complete($session, $standardCmd, '1 task found');
            } else {
                return $self->complete($session, $standardCmd, @sortedList . ' tasks found');
            }
        }
    }
}

# Current task

{ package Games::Axmud::Cmd::TaskHelp;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('taskhelp', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['th', 'thelp', 'taskhelp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows help for a task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName,
            $check,
        ) = @_;

        # Local variables
        my (
            $packageName, $prettyName,
            @list,
        );

        # Check for improper arguments
        if (! defined $taskName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get the package name corresponding to $taskName (e.g. 'Games::Axmud::Task::Status',
        #   'Games::Axmud::Task::Divert')
        $packageName = $self->findTaskPackageName($session, $taskName);
        if (! defined $packageName) {

            return $self->error($session, $inputString, 'Unrecognised task \'' . $taskName . '\'');
        }

        # Remove the 'Games::Axmud::Task::' bit...
        if (index($packageName, 'Games::Axmud') != 0) {

            return $self->error(
                $session, $inputString,
                'General error fetching help for the task \'' . $taskName . '\' - invalid package'
                . ' name \'' . $packageName . '\'',
            );

        } else {

            $prettyName = substr($packageName, 20);
        }

        # Load help for this task
        @list = $self->taskHelp($session, $prettyName);
        if (! @list) {

            return $self->error(
                $session, $inputString,
                'General error fetching help for the task \'' . $taskName . '\'',
            );

        } else {

            # Display help
            $session->writeText('Help for the \'' . $prettyName . '\' task:');
            foreach my $line (@list) {

                $session->writeText('   ' . $line);
            }

            $session->writeText(' ');       # Empty line

            return $self->complete($session, $standardCmd, 'Help displayed');
        }
    }
}

{ package Games::Axmud::Cmd::StartTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('starttask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['st', 'starttask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Starts a task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $result, $groupCount, $switch, $check, $newTask, $immediateFlag, $waitTaskExistFlag,
            $waitTaskNoExistFlag, $waitTaskStartStopFlag, $waitMinutesFlag, $unlimitedFlag,
            $runTaskForFlag, $runTaskUntilFlag, $noWindowFlag, $otherTask, $minutes, $runMinutes,
            $timer, $taskName, $packageName,
        );

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Extract the switch options
        (
            $result, $groupCount, $otherTask, $minutes, $runMinutes, $timer, $immediateFlag,
            $waitTaskExistFlag, $waitTaskNoExistFlag, $waitTaskStartStopFlag, $waitMinutesFlag,
            $unlimitedFlag, $runTaskForFlag, $runTaskUntilFlag, $noWindowFlag,
            @args,
        ) = $self->extractTaskSwitches($session, $inputString, $standardCmd, @args);

        if (! $result) {

            # Extraction failed; error message already displayed
            return undef;
        }

        # Only the argument <task> should be left now
        $taskName = shift @args;
        if (! defined $taskName) {

            return $self->error($session, $inputString, 'Start which task?');
        }

        $check = shift @args;
        if (defined $check) {

            return $self->error($session, $inputString, 'Unrecognised arguments/switches');
        }

        # $self->findTaskPackageName recognises unique names of currently running tasks (e.g.
        #   'status_task_57'), so before we consult it, check that $taskName isn't already running
        if ($session->ivExists('currentTaskHash', $taskName)) {

            return $self->error(
                $session, $inputString,
                'A task with the name \'' . $taskName . '\' is already running',
            );
        }

        # Get the package name corresponding to $taskName (e.g. 'Games::Axmud::Task::Status',
        #   'Games::Axmud::Task::Divert')
        $packageName = $self->findTaskPackageName($session, $taskName);
        if (! defined $packageName) {

            return $self->error($session, $inputString, 'Unrecognised task \'' . $taskName . '\'');
        }

        # Create the task object
        $newTask = $packageName->new($session, 'current');
        if (! $newTask) {

            return $self->error(
                $session, $inputString,
                'Couldn\'t create current task \'' . $taskName . '\'',
            );
        }

        # If the task's ->storable flag isn't set, this task shouldn't be started with this command
        #   (e.g. the Advance task should be started with ;advance)
        if (! $newTask->storableFlag) {

            # Mark the task as already finished, so that it never initialises
            $newTask->set_status('finished');

            return $self->error(
                $session, $inputString,
                'The \'' . $newTask->prettyName . '\' task can\'t be started with this command'
                . ' (try \';taskhelp\' to find out why)',
            );
        }

        # Set the task's settings, if necessary
        $result = $self->newTaskSettings(
            $session, $inputString, $standardCmd, $newTask, $otherTask, $minutes, $runMinutes,
            $timer, $immediateFlag, $waitTaskExistFlag, $waitTaskNoExistFlag,
            $waitTaskStartStopFlag, $waitMinutesFlag, $unlimitedFlag, $runTaskForFlag,
            $runTaskUntilFlag, $noWindowFlag,
        );

        if (! $result) {

            # Setup failed (usually because $otherTask should exist, but doesn't);
            #   error message already displayed
            return undef;
        }

        # Task creation complete
        return $self->complete(
            $session, $standardCmd,
            'Started task \'' . $newTask->prettyName . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::HaltTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('halttask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ht', 'halttask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Halts a task (gracefully)';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName,
            $check,
        ) = @_;

        # Local variables
        my @taskList;

        # Check for improper arguments
        if (! defined $taskName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get a list of blessed references to tasks matching the string $taskName
        @taskList = $self->findTask($session, $taskName);

        # Send the shutdown flag to every matching task
        foreach my $taskObj (@taskList) {

            $taskObj->set_shutdownFlag(TRUE);
        }

        if (! @taskList) {

            return $self->error(
                $session, $inputString,
                'There are no tasks matching \'' . $taskName . '\'',
            );

        } elsif (@taskList == 1) {

            return $self->complete($session, $standardCmd, '1 task asked to halt gracefully');

        } else {

            return $self->complete(
                $session, $standardCmd,
                @taskList . ' tasks asked to halt gracefully',
            );
        }
    }
}

{ package Games::Axmud::Cmd::KillTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('killtask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['kt', 'killtask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Kills (stops) a task immediately';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName,
            $check,
        ) = @_;

        # Local variables
        my @taskList;

        # Check for improper arguments
        if (! defined $taskName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get a list of blessed references to tasks matching the string $taskName
        @taskList = $self->findTask($session, $taskName);

        # Set the status of every matching task to 'finished'
        foreach my $taskObj (@taskList) {

            $taskObj->set_status('finished');
            # Close its task window (if open)
            $taskObj->closeWin();
            # Delete GA::Session's shortcut IV for this task (if any)
            $session->del_standardTask($taskObj);

            # Special case: if it's the Locator task and the automapper is running, and if a map is
            #   currently displayed in the Automapper window, tell the automapper to reset (and
            #   tell the automapper to show no region)
            if ($taskObj->name eq 'locator_task' && $session->mapWin) {

                $session->mapWin->setMode('wait');
                $session->mapObj->setCurrentRoom();
            }
        }

        if (! @taskList) {

            return $self->error(
                $session, $inputString,
                'There are no tasks matching \'' . $taskName . '\'',
            );

        } elsif (@taskList == 1) {

            return $self->complete($session, $standardCmd, '1 task told to stop immediately');

        } else {

            return $self->complete(
                $session, $standardCmd,
                @taskList . ' tasks told to stop immediately',
            );
        }
    }
}

{ package Games::Axmud::Cmd::PauseTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('pausetask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['pt', 'pausetask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Pauses a task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName,
            $check,
        ) = @_;

        # Local variables
        my (
            $pauseCount,
            @taskList,
        );

        # Check for improper arguments
        if (! defined $taskName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get a list of blessed references to tasks matching the string $taskName
        @taskList = $self->findTask($session, $taskName);

        # Set the status of every matching task to 'paused' (unless it is already paused)
        $pauseCount = 0;
        foreach my $taskObj (@taskList) {

            if ($taskObj->status ne 'paused') {

                $pauseCount++;
                $taskObj->set_resumeStatus($taskObj->status);
                $taskObj->set_status('paused');
                # Inform the session
                $session->add_pausedTask($taskObj);
            }
        }

        if (! @taskList) {

            return $self->error(
                $session, $inputString,
                'There are no tasks matching \'' . $taskName . '\'',
            );

        } elsif (! $pauseCount) {

            return $self->error(
                $session, $inputString,
                'There are no unpaused tasks matching \'' . $taskName . '\'',
            );

        } elsif ($pauseCount == 1) {

            return $self->complete($session, $standardCmd, '1 task paused');

        } else {

            return $self->complete(
                $session, $standardCmd,
                $pauseCount . ' tasks paused',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ResumeTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resumetask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rt', 'resumetask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resumes a paused task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName,
            $check,
        ) = @_;

        # Local variables
        my (
            $resumeCount,
            @taskList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;rt
        if (! defined $taskName) {

            # Resume all tasks paused with the ;pausetask command (whose blessed references are
            #   stored in GA::Session->pausedTaskList)
            $resumeCount = 0;
            foreach my $taskObj ($session->pausedTaskList) {

                # Only unpause the task, if it is still paused
                if ($taskObj->status eq 'paused') {

                    $resumeCount++;

                    if (defined $taskObj->resumeStatus) {
                        $taskObj->set_status($taskObj->resumeStatus);
                    } else {
                        $taskObj->set_status('running');      # Default status
                    }

                    $taskObj->set_resumeStatus();
                }
            }

            # Empty the resume list
            $session->empty_pausedTask();

            if (! $resumeCount) {

                return $self->error($session, $inputString, 'No paused tasks resumed');

            } elsif ($resumeCount == 1) {

                return $self->complete($session, $standardCmd, '1 paused task resumed');

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    $resumeCount . ' paused tasks resumed',
                );
            }

        # ;rt -a
        # ;rt <task>
        } else {

            # Get a list of blessed references to tasks matching the string $taskName
            @taskList = $self->findTask($session, $taskName);

            # Resume every matching task currently marked as 'paused'
            $resumeCount = 0;
            foreach my $taskObj (@taskList) {

                if ($taskObj->status eq 'paused') {

                    $resumeCount++;

                    if (defined $taskObj->resumeStatus) {
                        $taskObj->set_status($taskObj->resumeStatus);
                    } else {
                        $taskObj->set_status('running');      # Default status
                    }

                    $taskObj->set_resumeStatus();
                    # If this task is in GA::Session's list of paused tasks, ask the session to
                    #   remove it
                    $session->del_pausedTask($taskObj);
                }
            }

            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'There are no tasks matching \'' . $taskName . '\'',
                );

            } elsif (! $resumeCount) {

                return $self->error(
                    $session, $inputString,
                    'There are no unpaused tasks matching \'' . $taskName . '\'',
                );

            } elsif ($resumeCount == 1) {

                return $self->complete($session, $standardCmd, '1 paused task resumed');

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    $resumeCount . ' paused tasks resumed',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ResetTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resettask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rtt', 'resettask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets a task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName,
            $check,
        ) = @_;

        # Local variables
        my (
            $resetCount,
            @taskList,
        );

        # Check for improper arguments
        if (! defined $taskName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;rtt <task>
        # ;rtt -a

        # Get a list of blessed references to tasks matching the string $taskName
        @taskList = $self->findTask($session, $taskName);

        # Reset every matching task whose status is 'running' or 'paused'
        $resetCount = 0;
        foreach my $taskObj (@taskList) {

            if ($taskObj->status eq 'running' || $taskObj->status eq 'paused') {

                $taskObj->set_status('reset');
                $resetCount++;
            }
        }

        if (! @taskList) {

            return $self->error(
                $session, $inputString,
                'There are no tasks matching \'' . $taskName . '\'',
            );

        } elsif (! $resetCount) {

            return $self->error(
                $session, $inputString,
                'There are no active or paused tasks matching \'' . $taskName . '\'',
            );

        } elsif ($resetCount == 1) {

            return $self->complete($session, $standardCmd, '1 task reset');

        } else {

            return $self->complete(
                $session, $standardCmd,
                $resetCount . ' tasks reset',
            );
        }
    }
}

{ package Games::Axmud::Cmd::FreezeTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('freezetask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ft', 'freezetask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Freezes (or unfreezes) the current tasklist';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;ft
        if ($session->freezeTaskLoopFlag) {

            # Unfreeze all tasks
            $session->set_freezeTaskLoopFlag(FALSE);
            return $self->complete(
                $session, $standardCmd,
                'All tasks in the current tasklist unfrozen (the task loop has resumed)',
            );

        } else {

            # Freeze all tasks
            $session->set_freezeTaskLoopFlag(TRUE);
            return $self->complete(
                $session, $standardCmd,
                'All tasks in the current tasklist frozen (the task loop has been suspended)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('edittask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['et', 'edittask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a current task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName,
            $check,
        ) = @_;

        # Local variables
        my (
            $taskObj,
            @taskList,
        );

        # Check for improper arguments
        if (! defined $taskName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Unlike some commands which call GA::Generic::Cmd->findTask, the switch '-a' can't be used
        #   with this command
        if ($taskName eq '-a') {

            return $self->error(
                $session, $inputString,
                'The \'-a\' switch can\'t be used with this command',
            );
        }

        # Get a list of blessed references to tasks matching the string $taskName
        @taskList = $self->findTask($session, $taskName);
        if (! @taskList) {

            return $self->error(
                $session, $inputString,
                'There are no tasks matching \'' . $taskName . '\'',
            );

        } elsif (@taskList > 1) {

            return $self->error(
                $session, $inputString,
                'There is more than one task matching \'' . $taskName . '\' (try using the task\'s'
                . ' unique name, e.g. \'status_task_2\')',
            );

        } else {

            $taskObj = shift @taskList;

            # Open an 'edit' window for the task
            if (
                ! $session->mainWin->createFreeWin(
                    'Games::Axmud::EditWin::Task',
                    $session->mainWin,
                    $session,
                    'Edit ' . $taskObj->prettyName . ' task',
                    $taskObj,
                    FALSE,                          # Not temporary
                    # Config
                    'edit_flag' => FALSE,           # Some IVs for current tasks not editable
                )
            ) {
                return $self->error(
                    $session, $inputString,
                    'Could not edit the ' . $taskObj->prettyName . ' task',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Opened \'edit\' window for the ' . $taskObj->prettyName . ' task',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ListTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listtask', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lt', 'listtask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Displays the current tasklist';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @taskList;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get a list of blessed references to tasks in the current tasklist
        @taskList = sort {lc($a->uniqueName) cmp lc($b->uniqueName)}
                        ($session->ivValues('currentTaskHash'));

        if (@taskList) {

            # Display header
            $session->writeText(
                'Current tasklist (time: ' . sprintf("%.3f", $session->sessionTime)
                 . ') (p = process, a = activity) (* = active mode)',
            );
            $session->writeText(
                '   Task (unique) name       Stage  Task status          Original profile',
            );

            # Display list
            foreach my $obj (@taskList) {

                my ($column, $stage, $profName);

                if ($obj->activeFlag) {
                    $column = '*';
                } else {
                    $column = ' ';
                }

                if ($obj->profName) {
                    $profName = $obj->profName;
                } else {
                    $profName = '<none>';
                }

                if ($obj->category eq 'process') {
                    $stage = $obj->stage;
                } else {
                    $stage = '-';   # Activity tasks
                }

                # Add 'p' for process tasks, 'a' for activity tasks
                $column .= substr($obj->category, 0, 1);

                $session->writeText(
                    sprintf (
                        '%-2.2s %-24.24s %-6.6s %-20.20s %-16.16s',
                        $column,
                        $obj->uniqueName,
                        $stage,
                        $obj->status,
                        $profName,
                    )
                );
            }
        }

        # Display footer
        if (@taskList == 1) {

            return $self->complete($session, $standardCmd, 'Current tasklist contains 1 task');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Current tasklist contains ' . @taskList . ' tasks',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetRunList;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setrunlist', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['srl', 'setrl', 'setrunlist'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets one of the task runlists';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch,
            @nameList,
        );

        # (No improper arguments to check yet)

        # ;srl
        if (! @args) {

            $session->writeText(
                'Current first runlist : ' . join(' ', $axmud::CLIENT->taskRunFirstList),
            );
            $session->writeText(
                '   (Default)          : ' . join(' ', $axmud::CLIENT->constTaskRunFirstList),
            );

            $session->writeText(
                'Current last runlist : ' . join(' ', $axmud::CLIENT->taskRunLastList),
            );
            $session->writeText(
                '   (Default)          : ' . join(' ', $axmud::CLIENT->constTaskRunLastList),
            );

            return $self->complete($session, $standardCmd, 'Task runlists displayed');

        # ;srl -d
        # ;srl -f <list>
        # ;srl -l <list>
        } else {

            # Extract the switch
            $switch = shift @args;
            if (
                ($switch ne '-d' && $switch ne '-f' && $switch ne '-l')
                || ($switch eq '-d' && @args)
            ) {
                return $self->improper($session, $inputString);
            }

            # ;srl -d
            if ($switch eq '-d') {

                # Restore runlists to their defaults
                $axmud::CLIENT->reset_runLists();

                return $self->complete($session, $standardCmd, 'Task runlists set to defaults');
            }

            # For the -f and -l switches, we need to check every element of @args and convert any
            #   task label names into task formal names
            foreach my $task (@args) {

                if ($axmud::CLIENT->ivExists('taskLabelHash', $task)) {

                    # $task is a task label
                    push (@nameList, $axmud::CLIENT->ivShow('taskLabelHash', $task));

                } else {

                    # Assume $task is a task's formal name
                    push (@nameList, $task);
                }
            }

            # ;srl -f <list>
            if ($switch eq '-f') {

                $axmud::CLIENT->set_taskRunFirstList(@nameList);

                if (! @nameList) {

                    return $self->complete(
                        $session, $standardCmd,
                        '\'First\' task runlist has been emptied',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        '\'First\' task runlist has been set',
                    );
                }

            # ;srl -l <list>
            } elsif ($switch eq '-l') {

                $axmud::CLIENT->set_taskRunLastList(@nameList);

                if (! @nameList) {

                    return $self->complete(
                        $session, $standardCmd,
                        '\'Last\' task runlist has been emptied',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        '\'Last\' task runlist has been set',
                    );
                }
            }
        }
    }
}

# Initial tasks

{ package Games::Axmud::Cmd::AddInitialTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addinitialtask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ait', 'addinittask', 'addinitialtask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a task to an initial tasklist';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $result, $groupCount, $switch, $check, $newTask, $immediateFlag, $waitTaskExistFlag,
            $waitTaskNoExistFlag, $waitTaskStartStopFlag, $waitMinutesFlag, $unlimitedFlag,
            $runTaskForFlag, $runTaskUntilFlag, $noWindowFlag, $otherTask, $minutes, $runMinutes,
            $timer, $taskName, $packageName, $profName, $profObj,
        );

        # (No improper arguments to check)

        # Extract the switch options
        (
            $result, $groupCount, $otherTask, $minutes, $runMinutes, $timer, $immediateFlag,
            $waitTaskExistFlag, $waitTaskNoExistFlag, $waitTaskStartStopFlag, $waitMinutesFlag,
            $unlimitedFlag, $runTaskForFlag, $runTaskUntilFlag, $noWindowFlag,
            @args,
        ) = $self->extractTaskSwitches($session, $inputString, $standardCmd, @args);

        if (! $result) {

            # Extraction failed; error message already displayed
            return undef;
        }

        # Only the arguments <task> and <profile> should be left now
        $taskName = shift @args;
        if (! defined $taskName) {

            return $self->error($session, $inputString, 'Start which task?');
        }

        $profName = shift @args;
        if ($profName) {

            if (! $session->ivExists('profHash', $profName)) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised profile \'' . $profName . '\'',
                );

            } else {

                $profObj = $session->ivShow('profHash', $profName);
            }
        }

        $check = shift @args;
        if (defined $check) {

            return $self->error($session, $inputString, 'Unrecognised arguments/switches');
        }

        # $self->findTaskPackageName recognises unique names of currently running tasks (e.g.
        #   'status_task_57'), so before we consult it, check that $taskName isn't already running
        if ($session->ivExists('currentTaskHash', $taskName)) {

            return $self->error(
                $session, $inputString,
                'A task with the name \'' . $taskName . '\' is already running',
            );
        }

        # Get the package name corresponding to $taskName (e.g. 'Games::Axmud::Task::Status',
        #   'Games::Axmud::Task::Divert')
        $packageName = $self->findTaskPackageName($session, $taskName);
        if (! defined $packageName) {

            return $self->error($session, $inputString, 'Unrecognised task \'' . $taskName . '\'');
        }

        # Create the task object
        if ($profName) {

            # Put the task in the profile's initial tasklist
            $newTask = $packageName->new($session, 'initial', $profName, $profObj->category);

        } else {

            # Put the task in the global initial tasklist
            $newTask = $packageName->new($session, 'initial');
        }

        if (! $newTask) {

            return $self->error(
                $session, $inputString,
                'Couldn\'t create initial task \'' . $taskName . '\'',
            );
        }

        # Set the task's settings, if necessary
        $result = $self->newTaskSettings(
            $session, $inputString, $standardCmd, $newTask, $otherTask, $minutes, $runMinutes,
            $timer, $immediateFlag, $waitTaskExistFlag, $waitTaskNoExistFlag,
            $waitTaskStartStopFlag, $waitMinutesFlag, $unlimitedFlag, $runTaskForFlag,
            $runTaskUntilFlag, $noWindowFlag,
        );

        if (! $result) {

            # Setup failed (usually because $otherTask should exist, but doesn't);
            #   error message already displayed
            return undef;
        }

        # Task creation complete
        if (defined $profName) {

            return $self->complete(
                $session, $standardCmd,
                'Inserted task \'' . $newTask->prettyName . '\' into the \'' . $profName
                . '\' profile\'s initial tasklist',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Inserted task \'' . $newTask->prettyName . '\' into the global initial tasklist',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditInitialTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editinitialtask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['eit', 'editinittask', 'editinitialtask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for an initial task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName, $profName,
            $check,
        ) = @_;

        # Local variables
        my (
            $standardName, $taskObj, $profObj,
            @taskList,
        );

        # Check for improper arguments
        if (! defined $taskName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;eit <task>
        if (! $profName) {

            # See if <task> is a task label
            if ($axmud::CLIENT->ivExists('taskLabelHash', $taskName)) {

                # Translate it into the corresponding standard task name
                $standardName = $axmud::CLIENT->ivShow('taskLabelHash', $taskName);
            }

            # Find all tasks in the global initial tasklist whose name, unique name or label matches
            #   <task>
            foreach my $obj ($axmud::CLIENT->ivValues('initTaskHash')) {

                if (
                    $obj->name eq $taskName
                    || $obj->uniqueName eq $taskName
                    || (defined $standardName && $obj->name eq $standardName)
                ) {
                    push (@taskList, $obj);
                }
            }

            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'There are no tasks matching \'' . $taskName . '\' in the global initial'
                    . ' tasklist',
                );

            } elsif (@taskList > 1) {

                return $self->error(
                    $session, $inputString,
                    'There is more than one task matching \'' . $taskName . '\' in the global'
                    . ' initial tasklist',
                    );

            } else {

                $taskObj = shift @taskList;
            }

        # ;eit <task> <profile>
        } else {

            # If the profile was specified, check it exists
            if ($profName) {

                if (! $session->ivExists('profHash', $profName)) {

                    return $self->error(
                        $session, $inputString,
                        'Unrecognised profile \'' . $profName . '\'',
                    );

                } else {

                    $profObj = $session->ivShow('profHash', $profName);
                }
            }

            # See if <task> is a task label
            if ($axmud::CLIENT->ivExists('taskLabelHash', $taskName)) {

                # Translate it into the corresponding standard task name
                $standardName = $axmud::CLIENT->ivShow('taskLabelHash', $taskName);
            }

            # Find all tasks in the profile's initial tasklist whose name, unique name or label
            #   matches <task>
            foreach my $uniqueName ($profObj->initTaskOrderList) {

                my $obj = $profObj->ivShow('initTaskHash', $uniqueName);

                if (
                    $obj->name eq $taskName
                    || $obj->uniqueName eq $taskName
                    || (defined $standardName && $obj->name eq $standardName)
                ) {
                    push (@taskList, $obj);
                }
            }

            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'There are no tasks matching \'' . $taskName . '\' in the \'' . $profName
                    . '\' profile\'s initial tasklist',
                );

            } elsif (@taskList > 1) {

                return $self->error(
                    $session, $inputString,
                    'There is more than one task matching \'' . $taskName . '\' in the \''
                    . $profName . '\' profile\'s initial tasklist',
                    );

            } else {

                $taskObj = shift @taskList;
            }
        }

        # Open an 'edit' window for the task
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Task',
                $session->mainWin,
                $session,
                'Edit ' . $taskObj->prettyName . ' task',
                $taskObj,
                FALSE,                          # Not temporary
                # Config
                'edit_flag' => TRUE,            # Some IVs for initial tasks are editable
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the ' . $taskObj->prettyName . ' task',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the ' . $taskObj->prettyName . ' task',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteInitialTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteinitialtask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'dit',
            'delinittask',
            'deleteinittask',
            'deleteinitialtask',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a task from an initial tasklist';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName, $profName,
            $check,
        ) = @_;

        # Local variables
        my (
            $profObj, $count, $standardName,
            @newTaskList, @newObjList,
            %newTaskHash,
        );

        # Check for improper arguments
        if (! defined $taskName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # If a profile was specifid, check it actually exists
        if (defined $profName) {

            if (! $session->ivExists('profHash', $profName)) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised profile \'' . $profName . '\'',
                );

            } else {

                $profObj = $session->ivShow('profHash', $profName);
            }
        }

        # ;dit -a
        if ($taskName eq '-a' && ! $profName) {

            # Remember how many initial tasks are going to be destroyed
            $count = $axmud::CLIENT->ivPairs('initTaskHash');
            # Empty the global initial tasklist, and set the initial task total back to 0 (we don't
            #   need to avoid name clashes if the list has been emptied)
            $axmud::CLIENT->reset_initTaskHash();

            if (! $count) {

                return $self->error(
                    $session, $inputString,
                    'The global initial tasklist was already empty',
                );

            } elsif ($count == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Removed 1 task from the global initial tasklist',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Removed ' . $count . ' tasks from the global initial tasklist',
                );
            }

        # ;dit -a <profile>
        } elsif ($taskName eq '-a' && $profName) {

            # Remember how many initial tasks are going to be destroyed
            $count = $profObj->ivPairs('initTaskHash');
            # Empty the profile's initial tasklist, and set the initial task total back to 0 (we
            #   don't need to avoid name clashes if the list has been emptied)
            $profObj->ivEmpty('initTaskHash');
            $profObj->ivEmpty('initTaskOrderList');
            $profObj->ivPoke('initTaskTotal', 0);

            if (! $count) {

                return $self->error(
                    $session, $inputString,
                    'The \'' . $profName . '\' profile\'s initial tasklist was already empty',
                );

            } elsif ($count == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Removed 1 task from the \'' . $profName . '\' profile\'s initial tasklist',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Removed ' . $count . ' tasks from the \'' . $profName . '\' profile\'s'
                    . ' initial tasklist',
                );
            }

        # ;dit <task>
        } elsif (! $profName) {

            # See if <task> is a task label
            if ($axmud::CLIENT->ivExists('taskLabelHash', $taskName)) {

                # Translate it into the corresponding standard task name
                $standardName = $axmud::CLIENT->ivShow('taskLabelHash', $taskName);
            }

            # Delete anything in the global initial tasklist whose name, unique name or label
            #   matches <task>
            $count = 0;
            foreach my $obj ($axmud::CLIENT->ivValues('initTaskHash')) {

                if (
                    $obj->name eq $taskName
                    || $obj->uniqueName eq $taskName
                    || (defined $standardName && $obj->name eq $standardName)
                ) {
                    # Delete this task from the global initial tasklist
                    $axmud::CLIENT->del_initTask($obj);
                    $count++;
                }
            }

            # If the global registry is now empty, we can set the initial task total back to 0 (we
            #   don't need to avoid name clashes if the list has been emptied)
            if (! $axmud::CLIENT->initTaskHash) {

                $axmud::CLIENT->reset_initTaskTotal();
            }

            if ($count == 0) {

                return $self->error(
                    $session, $inputString,
                    'The global initial tasklist didn\'t contain anything matching \'' . $taskName
                    . '\'',
                );

            } elsif ($count == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Removed 1 task from the global initial tasklist',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Removed ' . $count . ' tasks from the global initial tasklist',
                );
            }

        # ;dit <task> <profile>
        } else {

            # See if <task> is a task label
            if ($axmud::CLIENT->ivExists('taskLabelHash', $taskName)) {

                # Translate it into the corresponding standard task name
                $standardName = $axmud::CLIENT->ivShow('taskLabelHash', $taskName);
            }

            # Delete anything in the profile's initial tasklist whose name, unique name or label
            #   matches <task>
            $count = 0;
            foreach my $uniqueName ($profObj->initTaskOrderList) {

                my $obj = $profObj->ivShow('initTaskHash', $uniqueName);

                if (
                    $obj->name eq $taskName
                    || $uniqueName eq $taskName
                    || (defined $standardName && $obj->name eq $standardName)
                ) {
                    $count++;

                } else {

                    push (@newTaskList, $uniqueName);
                    push (@newObjList, $obj);
                }
            }

            # Restore the tasklist, minus any deleted tasks
            foreach my $obj (@newObjList) {

                $newTaskHash{$obj->uniqueName} = $obj;
            }

            $profObj->ivPoke('initTaskHash', %newTaskHash);
            $profObj->ivPoke('initTaskOrderList', @newTaskList);

            if ($count == 0) {

                return $self->error(
                    $session, $inputString,
                    'The \'' . $profName . '\' profile\'s initial tasklist didn\t contain anything'
                    . ' matching \'' . $taskName . '\'',
                );

            } elsif ($count == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Removed 1 task from the \'' . $profName . '\' profile\'s initial tasklist',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Removed ' . $count . ' tasks from the \'' . $profName . '\' profile\'s'
                    . ' initial tasklist',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ListInitialTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listinitialtask', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lit', 'listinittask', 'listinitialtask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists initial tasks';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my (
            $profObj, $string,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If a profile was specified, check it exists
        if (defined $arg && $arg ne '-a' && $arg ne '-c') {

            if (! $session->ivExists('profHash', $arg)) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised profile \'' . $arg . '\'',
                );

            } else {

                $profObj = $session->ivShow('profHash', $arg);
            }
        }

        # ;lit
        if (! defined $arg) {

            foreach my $name ($axmud::CLIENT->initTaskOrderList) {

                push (@list, $axmud::CLIENT->ivShow('initTaskHash', $name));
            }

            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'The global initial tasklist is empty',
                );
            }

        # ;lit -c
        } elsif ($arg eq '-c') {

            OUTER: foreach my $obj ($session->ivValues('currentProfHash')) {

                INNER: foreach my $name ($obj->initTaskOrderList) {

                    push (@list, $obj->ivShow('initTaskHash', $name));
                }
            }

            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'The initial tasklists in all current profiles are empty',
                );
            }

        # ;lit -a
        } elsif ($arg eq '-a') {

            OUTER: foreach my $obj ($session->ivValues('profHash')) {

                INNER: foreach my $name ($obj->initTaskOrderList) {

                    push (@list, $obj->ivShow('initTaskHash', $name));
                }
            }

            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'The initial tasklists in all profiles are empty',
                );
            }

        # ;lit <profile>
        } else {

            foreach my $name ($profObj->initTaskOrderList) {

                push (@list, $profObj->ivShow('initTaskHash', $name));
            }

            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'No tasks found in the \'' . $arg . '\' profile\'s initial tasklist',
                );
            }
        }

        # Display header
        $string = ' (p = process, a = activity)';
        if (! defined $arg) {
            $session->writeText('Global initial tasklist' . $string);
        } elsif ($arg eq '-c') {
            $session->writeText('Initial tasklist for current profiles' . $string);
        } elsif ($arg eq '-a') {
            $session->writeText('Initial tasklist for all profiles' . $string);
        } else {
            $session->writeText('Initial tasklist for \'' . $arg . '\'' . $string);
        }

        $session->writeText('   Task name        Unique name              Stage  Status');

        # Display list
        foreach my $obj (@list) {

            $session->writeText(
                sprintf(
                    ' %-1.1s %-16.16s %-24.24s %-5.5s  %-16.16s',
                    substr($obj->category, 0, 1),
                    $obj->prettyName,
                    $obj->uniqueName,
                    $obj->stage,
                    $obj->status,
                )
            );
        }

        # Display footer
        if (@list == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Initial tasklist displayed (found 1 task)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Initial tasklist displayed (found ' . scalar @list . ' tasks)',
            );
        }
    }
}

# Custom tasks

{ package Games::Axmud::Cmd::AddCustomTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addcustomtask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['act', 'addcustomtask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a task to the custom tasklist';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $result, $groupCount, $switch, $check, $newTask, $immediateFlag, $waitTaskExistFlag,
            $waitTaskNoExistFlag, $waitTaskStartStopFlag, $waitMinutesFlag, $unlimitedFlag,
            $runTaskForFlag, $runTaskUntilFlag, $noWindowFlag, $otherTask, $minutes, $runMinutes,
            $timer, $taskName, $customName, $packageName,
        );

        # (No improper arguments to check)

        # Extract the switch options
        (
            $result, $groupCount, $otherTask, $minutes, $runMinutes, $timer, $immediateFlag,
            $waitTaskExistFlag, $waitTaskNoExistFlag, $waitTaskStartStopFlag, $waitMinutesFlag,
            $unlimitedFlag, $runTaskForFlag, $runTaskUntilFlag, $noWindowFlag,
            @args,
        ) = $self->extractTaskSwitches($session, $inputString, $standardCmd, @args);

        if (! $result) {

            # Extraction failed; error message already displayed
            return undef;
        }

        # Only the arguments <task> and <label> should be left now
        $taskName = shift @args;
        if (! defined $taskName) {

            return $self->error($session, $inputString, 'Create which custom task?');
        }

        $customName = shift @args;
        if (! defined $customName) {

            return $self->error($session, $inputString, 'Create custom task with which name?');
        }

        $check = shift @args;
        if (defined $check) {

            return $self->error($session, $inputString, 'Unrecognised arguments/switches');
        }

        # $self->findTaskPackageName recognises unique names of currently running tasks (e.g.
        #   'status_task_57'), so before we consult it, check that $taskName isn't already running
        if ($session->ivExists('currentTaskHash', $taskName)) {

            return $self->error(
                $session, $inputString,
                'A task with the name \'' . $taskName . '\' is already running',
            );
        }

        # Get the package name corresponding to $taskName (e.g. 'Games::Axmud::Task::Status',
        #   'Games::Axmud::Task::Divert')
        $packageName = $self->findTaskPackageName($session, $taskName);
        if (! defined $packageName) {

            return $self->error($session, $inputString, 'Unrecognised task \'' . $taskName . '\'');
        }

        # Need to check that the custom task name isn't already in use
        if ($axmud::CLIENT->ivExists('customTaskHash', $customName)) {

            return $self->error(
                $session, $inputString,
                'A custom task called \'' . $customName . '\' already exists',
            );

        # Check that the custom name is valid
        } elsif (! $axmud::CLIENT->nameCheck($customName, 16)) {

            return $self->error(
                $session, $inputString,
                'Invalid custom task name \'' . $customName . '\'',
            );
        }

        # Create the task object
        $newTask = $packageName->new($session, 'custom', undef, undef, $customName);
        if (! $newTask) {

            return $self->error(
                $session, $inputString,
                'Couldn\'t create a custom task called \'' . $customName . '\'',
            );
        }

        # Set the task's settings, if necessary
        $result = $self->newTaskSettings(
            $session, $inputString, $standardCmd, $newTask, $otherTask, $minutes, $runMinutes,
            $timer, $immediateFlag, $waitTaskExistFlag, $waitTaskNoExistFlag,
            $waitTaskStartStopFlag, $waitMinutesFlag, $unlimitedFlag, $runTaskForFlag,
            $runTaskUntilFlag, $noWindowFlag,
        );

        if (! $result) {

            # Setup failed (usually because $otherTask should exist, but doesn't);
            #   error message already displayed
            return undef;
        }

        # Task creation complete
        return $self->complete(
            $session, $standardCmd,
            'Created custom task \'' . $newTask->name . '\' called \'' . $customName . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::EditCustomTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editcustomtask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ect', 'editcustomtask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a custom task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName,
            $check,
        ) = @_;

        # Local variables
        my $taskObj;

        # Check for improper arguments
        if (! defined $taskName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the task exists
        if (! $axmud::CLIENT->ivExists('customTaskHash', $taskName)) {

            return $self->error(
                $session, $inputString,
                'No custom task called \'' . $taskName . '\' found',
            );

        } else {

            $taskObj = $axmud::CLIENT->ivShow('customTaskHash', $taskName);
        }

        # Open an 'edit' window for the task
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Task',
                $session->mainWin,
                $session,
                'Edit ' . $taskObj->prettyName . ' task',
                $taskObj,
                FALSE,                          # Not temporary
                # Config
                'edit_flag' => TRUE,            # Some IVs for custom tasks are editable
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the ' . $taskObj->prettyName . ' task',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the ' . $taskObj->prettyName . ' task',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteCustomTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletecustomtask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dct', 'delcustomtask', 'deletecustomtask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a task from the custom tasklist';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName,
            $check,
        ) = @_;

        # Local variables
        my ($count, $taskObj);

        # Check for improper arguments
        if (! defined $taskName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;dct -a
        if ($taskName eq '-a') {

            # Remember how many custom tasks are going to be destroyed
            $count = $axmud::CLIENT->ivPairs('customTaskHash');
            # Empty the custom tasklist
            $axmud::CLIENT->reset_customTaskHash();

            if (! $count) {

                return $self->error(
                    $session, $inputString,
                    'The custom tasklist was already empty',
                );

            } elsif ($count == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Removed 1 task from the custom tasklist',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Removed ' . $count . ' tasks from the custom tasklist',
                );
            }

        # ;dct <task>
        } else {

            if (! $axmud::CLIENT->ivExists('customTaskHash', $taskName)) {

                return $self->error(
                    $session, $inputString,
                    'No custom task called \'' . $taskName . '\' found',
                );

            } else {

                $taskObj = $axmud::CLIENT->ivShow('customTaskHash', $taskName);
            }

            # Delete the custom task
            $axmud::CLIENT->del_customTask($taskObj);

            return $self->complete(
                $session, $standardCmd,
                'Deleted the custom task \'' . $taskName . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListCustomTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listcustomtask', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lct', 'listcustomtask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists custom tasks';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (! $axmud::CLIENT->customTaskHash) {

            return $self->complete($session, $standardCmd, 'The custom tasklist is empty');
        }

        @list = $axmud::CLIENT->ivValues('customTaskHash');
        if (! @list) {

            return $self->complete($session, $standardCmd, 'The custom tasklist is empty');
        }

        # Display header
        $session->writeText('Custom tasklist (p = process, a = activity)');
        $session->writeText('   Task name        Custom name');

        # Display list
        foreach my $obj (@list) {

            $session->writeText(
                sprintf(
                    ' %-1.1s %-16.16s %-32.32s',
                    substr($obj->category, 0, 1),
                    $obj->prettyName,
                    $obj->customName,
                )
            );
        }

        # Display footer
        if (@list == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Custom tasklist displayed (1 task found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Custom tasklist displayed (' . scalar @list . ' tasks found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::StartCustomTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('startcustomtask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sct', 'startcustomtask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Starts a custom task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName,
            $check,
        ) = @_;

        # Local variables
        my $taskObj;

        # Check for improper arguments
        if (! defined $taskName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that the custom task exists
        if (! $axmud::CLIENT->ivExists('customTaskHash', $taskName)) {

            return $self->error(
                $session, $inputString,
                'No custom task called \'' . $taskName . '\' exists',
            );

        } else {

            $taskObj = $axmud::CLIENT->ivShow('customTaskHash', $taskName);
        }

        # Clone the initial task, and place the cloned copy into the current tasklist
        if (! $taskObj->clone($session, 'current')) {

            return $self->error(
                $session, $inputString,
                'General error starting the \'' . $taskName . '\' custom task',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                '\'' . $taskName . '\' custom task started',
            );
        }
    }
}

# Initial scripts

{ package Games::Axmud::Cmd::AddInitialScript;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addinitialscript', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ais', 'addinitscript', 'addinitialscript'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a script to an initial scriptlist';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $aloneFlag, $taskFlag, $forcedWinFlag, $flagCount, $scriptName, $profName,
            $profObj,
        );

        # Extract switches
        $flagCount = 0;

        ($switch, @args) = $self->extract('-r', 0, @args);
        if (defined $switch) {

            $aloneFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-t', 0, @args);
        if (defined $switch) {

            $taskFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-c', 0, @args);
        if (defined $switch) {

            $forcedWinFlag = TRUE;
            $flagCount++;
        }

        if ($flagCount > 1) {

            return $self->error(
                $session, $inputString,
                'The switches -r, -t and -c can\'t be combined',
            );
        }

        # Extract <script> and <profile>
        $scriptName = shift @args;
        $profName = shift @args;
        if ($profName) {

            if (! $session->ivExists('profHash', $profName)) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised profile \'' . $profName . '\'',
                );

            } else {

                $profObj = $session->ivShow('profHash', $profName);
            }
        }

        # There should be nothing left in @args now
        if (! $scriptName || @args) {

            return $self->improper($session, $inputString);
        }

        # Add the script to the specified scriptlist
        if ($profName) {

            if (! $flagCount || $aloneFlag) {
                $profObj->ivAdd('initScriptHash', $scriptName, 'no_task');
            } elsif ($taskFlag) {
                $profObj->ivAdd('initScriptHash', $scriptName, 'run_task');
            } elsif ($forcedWinFlag) {
                $profObj->ivAdd('initScriptHash', $scriptName, 'run_task_win');
            }

            # Check that the script isn't already in the list IV, before adding it
            if (! defined $profObj->ivFind('initScriptOrderList', $scriptName)) {

                $profObj->ivPush('initScriptOrderList', $scriptName);
            }

            return $self->complete(
                $session, $standardCmd,
                'Inserted script \'' . $scriptName . '\' into the \'' . $profName
                . '\' profile\'s initial scriptlist',
            );

        } else {

            if (! $flagCount || $aloneFlag) {
                $axmud::CLIENT->add_initScript($scriptName, 'no_task');
            } elsif ($taskFlag) {
                $axmud::CLIENT->add_initScript($scriptName, 'run_task');
            } elsif ($forcedWinFlag) {
                $axmud::CLIENT->add_initScript($scriptName, 'run_task_win');
            }

            return $self->complete(
                $session, $standardCmd,
                'Inserted script \'' . $scriptName . '\' into the global initial scriptlist',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteInitialScript;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteinitialscript', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dis', 'delinitscript', 'deleteinitialscript'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a script from an initial scriptlist';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $scriptName, $profName,
            $check,
        ) = @_;

        # Local variables
        my ($profObj, $index);

        # Check for improper arguments
        if (! defined $scriptName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;dis <script>
        if (! $profName) {

            # Check the script exists
            if (! $axmud::CLIENT->ivExists('initScriptHash', $scriptName)) {

                return $self->error(
                    $session, $inputString,
                    'No initial script called \'' . $scriptName . '\' found in the global'
                    . ' scriptlist',
                );
            }

            # Delete the initial script
            $axmud::CLIENT->del_initScript($scriptName);

            return $self->complete(
                $session, $standardCmd,
                'Deleted the initial script \'' . $scriptName . '\' from the global initial'
                . ' scriptlist',
            );

        # ;dis <script> <profile>
        } else {

            # Check the profile exists
            if (! $session->ivExists('profHash', $profName)) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised profile \'' . $profName . '\'',
                );

            } else {

                $profObj = $session->ivShow('profHash', $profName);
            }

            # Check the script exists
            if (! $profObj->ivExists('initScriptHash', $scriptName)) {

                return $self->error(
                    $session, $inputString,
                    'No initial script called \'' . $scriptName . '\' found in the \''
                    . $profName . '\' profile\'s initial scriptlist',
                );
            }

            # Delete the initial script
            $index = $profObj->ivFind('initScriptOrderList', $scriptName);
            if (defined $index) {

                $profObj->ivSplice('initScriptOrderList', $index, 1);
            }

            $profObj->ivDelete('initScriptHash', $scriptName);

            return $self->complete(
                $session, $standardCmd,
                'Deleted the initial script \'' . $scriptName . '\' from the \'' . $profName
                . '\' profile\'s initial scriptlist',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListInitialScript;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listinitialscript', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lis', 'listinitscript', 'listinitialscript'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists initial scripts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my (
            $profObj, $count,
            @list,
            %modeHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If a profile was specified, check it exists
        if (defined $arg && $arg ne '-a' && $arg ne '-c') {

            if (! $session->ivExists('profHash', $arg)) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised profile \'' . $arg . '\'',
                );

            } else {

                $profObj = $session->ivShow('profHash', $arg);
            }
        }

        # ;lis
        if (! defined $arg) {

            foreach my $script ($axmud::CLIENT->initScriptOrderList) {

                # @list is in the form (script_name, 'global', mode, ...)
                push (
                    @list,
                    $script,
                    '<global>',
                    $axmud::CLIENT->ivShow('initScriptHash', $script),
                );
            }

            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'The global initial scriptlist is empty',
                );
            }

        # ;lis -c
        } elsif ($arg eq '-c') {

            OUTER: foreach my $obj ($session->ivValues('currentProfHash')) {

                INNER: foreach my $script ($obj->initScriptOrderList) {

                    # @list is in the form (script_name, profile_name, mode, ...)
                    push (@list, $script, $obj->name, $obj->ivShow('initScriptHash', $script));
                }
            }

            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'The initial scriptlists in all current profiles are empty',
                );
            }

        # ;lis -a
        } elsif ($arg eq '-a') {

            OUTER: foreach my $obj ($session->ivValues('profHash')) {

                INNER: foreach my $script ($obj->initScriptOrderList) {

                    # @list is in the form (script_name, profile, mode, ...)
                    push (@list, $script, $obj->ivShow('initScriptHash', $script), $obj->name);
                }
            }

            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'The initial scriptlists in all profiles are empty',
                );
            }

        # ;lis <profile>
        } else {

            foreach my $script ($profObj->initScriptOrderList) {

                # @list is in the form (script_name, profile_name, mode, ...)
                push (@list, $script, $arg, $profObj->ivShow('initScriptHash', $script));
            }

            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'No scripts found in the \'' . $arg . '\' profile\'s initial scriptlist',
                );
            }
        }

        # Display header
        if (! defined $arg) {
            $session->writeText('Global initial scriptlist');
        } elsif ($arg eq '-c') {
            $session->writeText('Initial scriptlist for current profiles');
        } elsif ($arg eq '-a') {
            $session->writeText('Initial scriptlist for all profiles');
        } else {
            $session->writeText('Initial scriptlist for \'' . $arg . '\'');
        }

        $session->writeText('   Script name      Owner            Mode');

        # Display list
        %modeHash = (
            'no_task'       => 'Standalone script',
            'run_task'      => 'Run from within a task',
            'run_task_win'  => 'Run from within a task (forced window mode)',
        );

        do {

            my ($script, $owner, $mode);

            $script = shift @list;
            $owner = shift @list,

            $mode = $modeHash{shift @list},
            $count++;

            $session->writeText(sprintf('   %-16.16s %-16.16s %-48.48s', $script, $owner, $mode));

        } until (! @list);

        # Display footer
        if ($count == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Initial scriptlist displayed (found 1 script)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Initial scriptlist displayed (found ' . $count . ' scripts)',
            );
        }
    }
}

# Axbasic

{ package Games::Axmud::Cmd::EditScript;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editscript', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['es', 'editscript'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens ' . $axmud::BASIC_ARTICLE . ' script for editing';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $scriptName,
            $check,
        ) = @_;

        # Local variables
        my ($standardPath, $path, $cmd);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Default script is 'test'
        if (! defined $scriptName) {

            $scriptName = 'test';
        }

        # Check each directory in the GA::Client's list, looking for a file matching $scriptName
        #   (but first check in the standard directory)
        $standardPath = $axmud::DATA_DIR . '/scripts/' . $scriptName . '.bas';
        if (-e $standardPath) {

            $path = $standardPath;

        } else {

            OUTER: foreach my $dir ($axmud::CLIENT->scriptDirList) {

                my $thisPath = $dir . '/' . $scriptName . '.bas';
                if (-e $thisPath) {

                    $path = $thisPath;
                    last OUTER;
                }
            }
        }

        if (! $path) {

            return $self->error(
                $session, $inputString,
                $axmud::BASIC_NAME . ' script \'' . $scriptName . '\' not found',
            );
        }

        # Open the file in the external text editor
        $cmd = $axmud::CLIENT->textEditCmd;
        if (! $cmd || ! ($cmd =~ m/%s/)) {

            return $self->error(
                $session, $inputString,
                'Can\'t edit the ' . $axmud::BASIC_NAME . ' script - invalid external application'
                . ' command \'' . $cmd . '\'',
            );

        } else {

            $cmd =~ s/%s/$path/;

            system $cmd;

            return $self->complete(
                $session, $standardCmd,
                $axmud::BASIC_NAME . ' script \'' .  $scriptName . '\' opened for editing',
            );
        }
    }
}

{ package Games::Axmud::Cmd::CheckScript;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('checkscript', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cs', 'checkscript'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Checks an ' . $axmud::BASIC_NAME . ' script';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $scriptName,
            $check,
        ) = @_;

        # Local variables
        my ($rawScriptObj, $standardPath, $path, $scriptObj);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Default script is 'test'
        if (! defined $scriptName) {

            $scriptName = 'test';
        }

        # Create the raw script object
        $rawScriptObj = Language::Axbasic::RawScript->new($session, $scriptName);
        if (! defined $rawScriptObj) {

            return $self->error(
                $session, $inputString,
                'General error checking the ' . $axmud::BASIC_NAME . ' script \'' . $scriptName
                . '\' (can\'t create' . ' the raw script object)',
            );
        }

        # Check each directory in the GA::Client's list, looking for a file matching $scriptName
        #   (but first check in the standard directory)
        $standardPath = $axmud::DATA_DIR . '/scripts/' . $scriptName . '.bas';
        if (-e $standardPath) {

            $path = $standardPath;

        } else {

            OUTER: foreach my $dir ($axmud::CLIENT->scriptDirList) {

                my $thisPath = $dir . '/' . $scriptName . '.bas';
                if (-e $thisPath) {

                    $path = $thisPath;
                    last OUTER;
                }
            }
        }

        if (! $path) {

            return $self->error(
                $session, $inputString,
                $axmud::BASIC_NAME . ' script \'' . $scriptName . '\' not found',
            );
        }

        # Load the script into the raw script object
        if (! $rawScriptObj->loadFile($path)) {

            return $self->error(
                $session, $inputString,
                'Can\'t load the ' . $axmud::BASIC_NAME . ' script \'' . $path . '\'',
            );
        }

        # Create a script object, which processes the raw script, removing extraneous whitespace,
        #   empty lines, comments, etc
        $scriptObj = Language::Axbasic::Script->new($session, $rawScriptObj);
        if (! defined $scriptObj) {

            return $self->error(
                $session, $inputString,
                'General error checking the ' . $axmud::BASIC_NAME . ' script \'' . $scriptName
                . '\' (can\'t create a script object)',
            );
        }

        # Parse the script
        if (! $scriptObj->parse()) {

            return $self->error(
                $session, $inputString,
                $axmud::BASIC_NAME . ' script \'' . $scriptName . '\' check failed',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                $axmud::BASIC_NAME . ' script \'' .  $scriptName . '\' check complete - no errors',
            );
        }
    }
}

{ package Games::Axmud::Cmd::RunScript;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('runscript', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rs', 'runscript'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Runs ' . $axmud::BASIC_ARTICLE . ' script';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $scriptName,
            $check,
        ) = @_;

        # Local variables
        my ($path, $rawScriptObj, $standardPath, $scriptObj);

        # Check for improper arguments
        if (
            defined $check
            || (defined $switch && defined $scriptName && $switch ne '-p')
        ) {
            return $self->improper($session, $inputString);
        }

        # ;rs -p <path>
        if (defined $switch && defined $scriptName) {

            $path = $scriptName;

            # Check the path is valid
            if (! $path =~ m/\.bas$/i) {

                return $self->error(
                    $session, $inputString,
                    'Invalid ' . $axmud::BASIC_NAME . ' script file (must end \'.bas\')',
                );

            } elsif (! -e $path) {

                return $self->error(
                    $session, $inputString,
                    'Specified filepath \'' . $scriptName . '\' doesn\'t exist',
                );
            }

            # Extract the scriptname from the path
            $path =~ m/([\w\_]+)\.bas$/;
            $scriptName = $1;
            # Sanity check
            if (! defined $scriptName || $scriptName eq '') {

                $scriptName = 'unnamed';
            }

        # ;rs <script_name>
        } elsif (defined $switch) {

            $scriptName = $switch;
            $switch = undef;

        # ;rs
        }  else {

            # Default script name
            $scriptName = 'test';
        }

        # Create the raw script object
        $rawScriptObj = Language::Axbasic::RawScript->new($session, $scriptName);
        if (! defined $rawScriptObj) {

            return $self->error(
                $session, $inputString,
                'General error starting the ' . $axmud::BASIC_NAME . ' script \'' . $scriptName
                . '\' (can\'t create' . ' the raw script object)',
            );
        }

        if (! $path) {

            # Check each directory in the GA::Client's list, looking for a file matching $scriptName
            #   (but first check in the standard directory)
            $standardPath = $axmud::DATA_DIR . '/scripts/' . $scriptName . '.bas';
            if (-e $standardPath) {

                $path = $standardPath;

            } else {

                OUTER: foreach my $dir ($axmud::CLIENT->scriptDirList) {

                    my $thisPath = $dir . '/' . $scriptName . '.bas';
                    if (-e $thisPath) {

                        $path = $thisPath;
                        last OUTER;
                    }
                }
            }

            if (! $path) {

                return $self->error(
                    $session, $inputString,
                    $axmud::BASIC_NAME . ' script \'' . $scriptName . '\' not found',
                );
            }
        }

        # Load the script into the raw script object
        if (! $rawScriptObj->loadFile($path)) {

            return $self->error(
                $session, $inputString,
                'Can\'t load the ' . $axmud::BASIC_NAME . ' script \'' . $path . '\'',
            );
        }

        # Create a script object, which processes the raw script, removing extraneous whitespace,
        #   empty lines, comments, etc
        $scriptObj = Language::Axbasic::Script->new($session, $rawScriptObj);
        if (! defined $scriptObj) {

            return $self->error(
                $session, $inputString,
                'General error starting the ' . $axmud::BASIC_NAME . ' script \'' . $scriptName
                . '\' (can\'t create a script object)',
            );
        }

        # Execute the script
        if (! $scriptObj->implement()) {

            return $self->error(
                $session, $inputString,
                'Execution of the ' . $axmud::BASIC_NAME . ' script \'' . $scriptName . '\' failed',
            );
        }

        # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
        $axmud::CLIENT->desktopObj->restrictWidgets();

        if ($scriptObj->scriptStatus eq 'paused') {

            return $self->complete(
                $session, $standardCmd,
                'Execution of the ' . $axmud::BASIC_NAME . ' script \'' . $scriptName
                . '\' started',
            );

        } else {

            # No error message required - user has already seen 'AXBASIC: Execution complete'
            return 1;
        }
    }
}

{ package Games::Axmud::Cmd::RunScriptTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('runscripttask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rst', 'runscripttask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Runs ' . $axmud::BASIC_ARTICLE . ' script as a task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $maxSteps, $maxStepsFlag, $result, $groupCount, $otherTask, $minutes,
            $runMinutes, $timer, $immediateFlag, $waitTaskExistFlag, $waitTaskNoExistFlag,
            $waitTaskStartStopFlag, $waitMinutesFlag, $unlimitedFlag, $runTaskForFlag,
            $runTaskUntilFlag, $compatModeFlag, $scriptName, $check, $newTask,
        );

        # Extract the switch options that is unique to this command (max number of steps)
        ($switch, $maxSteps, @args) = $self->extract('-m', 1, @args);
        if (defined $switch) {

            $maxStepsFlag = 1;
        }

        # Extract the switch options that have been borrowed from the ;starttask command (unlike
        #   ;starttask, @args for this command might be empty)
        if (@args) {

            (
                $result, $groupCount, $otherTask, $minutes, $runMinutes, $timer, $immediateFlag,
                $waitTaskExistFlag, $waitTaskNoExistFlag, $waitTaskStartStopFlag, $waitMinutesFlag,
                $unlimitedFlag, $runTaskForFlag, $runTaskUntilFlag, $compatModeFlag,
                @args,
            ) = $self->extractTaskSwitches($session, $inputString, $standardCmd, @args);

            if (! $result) {

                # Extraction failed; error message already displayed
                return $result;
            }
        }

        # Only the argument <script> (if anything) should be left now
        $scriptName = shift @args;
        $check = shift @args;

        # Check for improper arguments
        if (defined $check) {

            return $self->error($session, $inputString, 'Unrecognised arguments/switches');
        }

        # Default script is 'test'
        if (! defined $scriptName) {

            $scriptName = 'test';
        }

        # Create the task object
        $newTask = Games::Axmud::Task::Script->new($session, 'current');
        if (! $newTask) {

            return $self->error(
                $session, $inputString,
                'General error creating \'Script\' task',
            );
        }

        # Set the task's settings, if necessary
        $result = $self->newTaskSettings(
            $session, $inputString, $standardCmd, $newTask, $otherTask, $minutes, $runMinutes,
            $timer, $immediateFlag, $waitTaskExistFlag, $waitTaskNoExistFlag,
            $waitTaskStartStopFlag, $waitMinutesFlag, $unlimitedFlag, $runTaskForFlag,
            $runTaskUntilFlag,
            FALSE,      # Equivalent to $noWindowFlag in the ;starttask command
        );

        if (! $result) {

            # Setup failed (usually because $otherTask should exist, but doesn't); error message
            #   already displayed
            return $result;
        }

        # Set the task's parameters
        $newTask->ivPoke('scriptName', $scriptName);
        if ($maxStepsFlag) {

            $newTask->ivPoke('stepMax', $maxSteps);
        }

        # Task creation complete

        if ($compatModeFlag) {

            # Task must open with a task window, and all text generated by PRINT, WINWRITE and
            #   INPUT statements must be diverted to the window
            $newTask->ivPoke('forcedWinFlag', TRUE);

            return $self->complete(
                $session, $standardCmd,
                'Started task \'' . $newTask->prettyName . '\' (running the \'' . $scriptName
                . '\' script in \'forced window\' mode)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Started task \'' . $newTask->prettyName . '\' (running the \'' . $scriptName
                . '\' script)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AxbasicHelp;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('axbasichelp', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['abh', 'abhelp', 'axbasichelp'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows help for ' . $axmud::BASIC_NAME . ' scripts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $topic,
            $check,
        ) = @_;

        # Local variables
        my (
            $scriptObj, $modTopic,
            @list, @showList, @topicList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Create a dummy LA::Script object so we can access the default IVs
        $scriptObj = Language::Axbasic::Script->new($session);
        if (! defined $scriptObj) {

            return $self->error(
                $session, $inputString,
                'General error accessing ' . $axmud::BASIC_NAME . ' help',
            );
        }

        # LA::Script stores keywords/function names in lower case, but in most Axbasic scripts
        #   they're typed in upper case (PRINT "Hello", etc)
        # Reduce $topic to lower-case so that ';axbasichelp PRINT' and ';axbasichelp print' have the
        #   same effect (but don't allow the user to type the switch -X, because that's not allowed
        #   anywhere else)
        if (defined $topic && lc($topic) ne '-x') {

            $topic = lc($topic);
        }

        # ;abh
        # ;abh -x
        if (! defined $topic || $topic eq '-x') {

            # Display header
            push (
                @showList,
                'Help for the ' . $axmud::BASIC_NAME . ' package (version ' . $axmud::BASIC_VERSION
                . ')',
            );

            push (@showList, ' ');

            # Display list
            @list = sort {lc($a) cmp lc($b)} ($scriptObj->keywordList);
            push (@showList, 'Keyword list:');
            push (@showList, join(' ', @list));
            push (@showList, ' ');

            # ;abh -x
            if ($topic) {

                # Display a list of keywords only available in modern-style scripts
                @list = sort {$a cmp $b} ($scriptObj->modernKeywordList);
                push (@showList,
                    'List of keywords only available in scripts without primitive line numbers:',
                );
                push (@showList, join(' ', @list));
                push (@showList, ' ');

                # Display a list of keywords only available in primitive line-numbered scripts
                @list = sort {$a cmp $b} ($scriptObj->primKeywordList);
                push (@showList,
                    'List of keywords only available in scripts with primitive line numbers:',
                );
                push (@showList, join(' ', @list));
                push (@showList, ' ');

                # Display a list of keywords used to set/fetch Axmud's internal variables
                @list = sort {$a cmp $b} ($scriptObj->peekPokeList);
                push (@showList,
                    'List of keywords used to set or import ' . $axmud::SCRIPT
                    . '\'s internal data',
                );
                push (@showList, join(' ', @list));
                push (@showList, ' ');

                # Display a list of weak keywords
                @list = sort {$a cmp $b} ($scriptObj->ivKeys('weakKeywordHash'));
                push (@showList, 'List of keywords that can\'t begin a statement:');
                push (@showList, join(' ', @list));
                push (@showList, ' ');

                # Display a list of keywords that interact with Axmud
                @list = sort {$a cmp $b} ($scriptObj->clientKeywordList);
                push (@showList,
                    'List of keywords that interact directly with ' . $axmud::SCRIPT . ':',
                );
                push (@showList, join(' ', @list));
                push (@showList, ' ');

                # Display a list of task-only keywords
                @list = sort {$a cmp $b} ($scriptObj->taskKeywordList);
                push (@showList,
                    'List of keywords that are ignored unless the ' . $axmud::BASIC_NAME
                    . ' script is being run by a Script task:',
                );
                push (@showList, join(' ', @list));
                push (@showList, ' ');
            }

            # Display a list of intrinsic functions
            @list = sort {$a cmp $b} ($scriptObj->ivKeys('funcArgHash'));
            push (@showList, 'List of intrinsic (built-in) functions:');
            push (@showList, join(' ', @list));
            push (@showList, ' ');

            # Display a list of logical operators
            @list = sort {$a cmp $b} ($scriptObj->logicalOpList);
            push (@showList, 'List of logical operators:');
            push (@showList, join(' ', @list));
            push (@showList, ' ');

            # ;abh -x
            if ($topic) {

                # Display a list of token categories (not sorted alphabetically)
                @list = $scriptObj->categoryList;
                push (@showList, 'List of token categories:');
                push (@showList, join(' ', @list));
                push (@showList, ' ');
            }

            # Display each line in turn
            foreach my $line (@showList) {

                $session->writeText($line);
            }

            # Display footer
            return $self->complete($session, $inputString, $axmud::BASIC_NAME . ' help displayed');

        # ;abh <topic>
        } else {

            # Check <topic> is a recognised keyword or intrinsic function (or perhaps both, in the
            #   case of weak keywords)
            if ($scriptObj->ivExists('keywordHash', $topic)) {

                if ($scriptObj->ivExists('weakKeywordHash', $topic)) {

                    # Instead of displaying help for the weak keyword, display help for the
                    #   corresponding strong keyword (e.g. instead of displaying help for RADIANS,
                    #   display help for OPTION)
                    push (@topicList, $scriptObj->ivShow('weakKeywordHash', $topic), 'keyword');

                } else {

                    push (@topicList, $topic, 'keyword');
                }
            }

            if (
                ! $scriptObj->ivExists('keywordHash', $topic)
                # Weak keywords can be used as function names, e.g. ANGLE / Angle()
                || $scriptObj->ivExists('weakKeywordHash', $topic)
            ) {
                $modTopic = $topic;

                if ($modTopic =~ m/\$$/) {

                    # Remove the final $, if present
                    $modTopic =~ s/\$$//;
                }

                # For getting help on Chr$(), user can specify either 'chr' or 'chr$'. However,
                #   there is a Date() and Date$(), also a Time() and Time$()
                # Deal with Date/Date$, Time/Time$ and any similar pairs
                if (
                    $scriptObj->ivExists('funcArgHash', $modTopic)
                    && $scriptObj->ivExists('funcArgHash', $modTopic . '$')
                ) {
                    # Display help for both Date() and Date$()
                    push (@topicList, $modTopic, 'func', $modTopic . '$', 'func')

                } elsif ($scriptObj->ivExists('funcArgHash', $modTopic)) {

                    push (@topicList, $modTopic, 'func');

                } elsif ($scriptObj->ivExists('funcArgHash', $modTopic . '$')) {

                    push (@topicList, $modTopic . '$', 'func');
                }
            }

            # Call the help function for the topic to fetch the topic-specific text
            if (! @topicList) {

                return $self->error(
                    $session, $inputString,
                    'No ' . $axmud::BASIC_NAME . ' help available for the topic \'' . $topic
                    . '\'',
                );

            } else {

                do {

                    my (
                        $thisTopic, $thisType,
                        @lineList,
                    );

                    $thisTopic = shift @topicList;
                    $thisType = shift @topicList;

                    @lineList = $self->abHelp($session, $thisTopic, $thisType);
                    if (! @lineList) {

                        return $self->error(
                            $session, $inputString,
                            'No ' . $axmud::BASIC_NAME . ' help available for the topic \'' . $topic
                            . '\'',
                        );

                    } else {

                        # Insert an empty line between topics (if there is more than one)
                        push (@showList, @lineList, ' ');
                    }

                } until (! @topicList);
            }

            # Display header and list
            foreach my $line (@showList) {

                $session->writeText($line);
            }

            # Display footer
            if ($scriptObj->ivExists('keywordHash', $topic)) {

                return $self->complete(
                    $session, $inputString,
                    $axmud::BASIC_NAME . ' help for the \'' . uc($topic) . '\' keyword displayed',
                );

            } else {

                return $self->complete(
                    $session, $inputString,
                    $axmud::BASIC_NAME . ' help for the \'' . ucfirst($topic)
                    . '()\' intrinsic function displayed',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::AddDirectory;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('adddirectory', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['adr', 'adddir', 'addfolder', 'adddirectory'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Starts using directory containing ' . $axmud::BASIC_NAME . ' scripts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $dir,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the list of Axbasic directories, and insert '/data/scripts/' at the beginning of it
        @list = ($axmud::DATA_DIR . '/data/scripts', $axmud::CLIENT->scriptDirList);

        if (! $dir) {

            # Prompt the user to choose a directory
            $dir = $session->mainWin->showFileChooser(
                'Select directory to add',
                'select-folder',
            );

            if (! $dir) {

                return $self->complete($session, $standardCmd, 'Add directory operation cancelled');
            }
        }

        # Check that $dir doesn't already appear in the list
        foreach my $otherDir (@list) {

            if ($otherDir eq $dir) {

                return $self->error(
                    $session, $inputString,
                    'The list of ' . $axmud::BASIC_NAME . ' directories already includes \'' . $dir
                    . '\'',
                );
            }
        }

        # Add this directory to the list
        $axmud::CLIENT->add_scriptDir($dir);

        return $self->complete(
            $session, $standardCmd,
            'Added ' . $axmud::BASIC_NAME . ' directory \'' . $dir . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::DeleteDirectory;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletedirectory', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ddr', 'deldir', 'deletefolder', 'deletedirectory'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Stops using directory containing ' . $axmud::BASIC_NAME . ' scripts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $index,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (! defined $index || defined $check) {

            return $self->improper($session, $inputString);
        }
            return $self->improper($session, $inputString);

        # Import the list of Axbasic directories, and insert '/data/scripts/' at the beginning of it
        @list = ($axmud::DATA_DIR . '/data/scripts', $axmud::CLIENT->scriptDirList);

        # Check that $index is a valid integer
        if (! $axmud::CLIENT->intCheck($index, 0, ((scalar @list) - 1))) {

            return $self->error(
                $session, $inputString,
                'Invalid directory number (try \';listdirectory\')',
            );

        } elsif ($index == 0) {

            return $self->error(
                $session, $inputString,
                'The standard ' . $axmud::BASIC_NAME . ' directory, \'' . $list[0]
                . '\', can\'t be deleted',
            );

        }

        # Delete the directory. Subtract 1, because GA::Client->scriptDirList doesn't have the
        #   standard '/data/scripts' directory in it
        $axmud::CLIENT->del_scriptDir($index - 1);

        return $self->complete(
            $session, $standardCmd,
            'Deleted ' . $axmud::BASIC_NAME. ' directory #' . $index . ' (\'' . $list[$index]
            . '\')',
        );
    }
}

{ package Games::Axmud::Cmd::ListDirectory;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listdirectory', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ldr', 'listdir', 'listfolder', 'listdirectory'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists ' . $axmud::BASIC_NAME . ' script directories';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $count,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the list of Axbasic directories, and insert '/data/scripts/' at the beginning of it
        if ($^O eq 'MSWin32') {
            @list = ($axmud::DATA_DIR . '\\data\\scripts', $axmud::CLIENT->scriptDirList);
        } else {
            @list = ($axmud::DATA_DIR . '/data/scripts', $axmud::CLIENT->scriptDirList);
        }

        if (! @list) {

            return $self->complete($session, $standardCmd, 'The script directory list is empty');
        }

        # Display header
        $session->writeText('List of ' . $axmud::BASIC_NAME . ' directories');

        # Display list
        $count = -1;
        foreach my $dir (@list) {

            $count++;

            $session->writeText('   ' . sprintf('%-4.4s', $count) . ' ' . $dir);
        }

        # Display footer
        if (@list == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Directory list displayed (found 1 directory)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Directory list displayed (found ' . scalar @list . ' directories)',
            );
        }
    }
}

# Workspaces

{ package Games::Axmud::Cmd::UseWorkspace;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('useworkspace', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['uws', 'usews', 'useworkspace'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Makes a workspace available for ' . $axmud::SCRIPT . ' to use';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $zonemap, $name, $useWorkspace, $workspaceObj,
            @workspaceList,
        );

        # Extract switches
        ($switch, $zonemap, @args) = $self->extract('-z', 1, @args);
        # Get the remaining argument (if any)
        $name = shift @args;

        # There should be no further arguments
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # In certain circumstances, new workspaces can't be used
        if (! $axmud::CLIENT->desktopObj->newWorkspaceFlag) {

            return $self->error(
                $session, $inputString,
                'Unable to use any new workspaces (try restarting ' . $axmud::SCRIPT . ')',
            );
        }

        # If a zonemap was specified, check it exists
        if (defined $zonemap && ! $axmud::CLIENT->ivExists('zonemapHash', $zonemap)) {

            return $self->error(
                $session, $inputString,
                'Zonemap \'' . $zonemap . '\' doesn\'t exist',
            );
        }

        # Get a list of Gnome2::Workspace objects (which might conceivably be different to the
        #   list found on the original call to GA::Obj::Desktop->detectWorkspaces)
        @workspaceList = $axmud::CLIENT->desktopObj->detectWorkspaces();

        # ;uws
        if (! defined $name) {

            # Find the first available workspace that's not in use
            OUTER: foreach my $workspace (@workspaceList) {

                INNER: foreach $workspaceObj (
                    $axmud::CLIENT->desktopObj->ivValues('workspaceHash')
                ) {
                    if ($workspaceObj->systemNum eq $workspace) {

                        next OUTER;
                    }
                }

                # This workspace is not in use, but is available
                $useWorkspace = $workspace;
                last OUTER;
            }

            if (! $useWorkspace) {

                return $self->error(
                    $session, $inputString,
                    'There are no more workspaces available for ' . $axmud::SCRIPT . ' to use',
                );
            }

        # ;uws <number>
        } else {

            OUTER: foreach my $workspace (@workspaceList) {

                if ($workspace eq $name) {

                    $useWorkspace = $workspace;
                    last OUTER;
                }
            }

            if (! $useWorkspace) {

                return $self->error(
                    $session, $inputString,
                    'System workspace \'' . $name . '\' not found',
                );
            }
        }

        # Add a workspace object (GA::Obj::Workspace) for this workspace
        $workspaceObj = $axmud::CLIENT->desktopObj->useWorkspace($useWorkspace, $zonemap);
        if (! $workspaceObj) {

            return $self->error(
                $session, $inputString,
                'General error adding the system workspace \'' . $name . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Workspace #' . $useWorkspace->get_number() . ' \'' . $useWorkspace->get_name()
                . '\' made available for use (' . $axmud::SCRIPT . ' workspace object #'
                . $workspaceObj->number . ')',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditWorkspace;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editworkspace', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ews', 'editws', 'editworkspace'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a workspace object';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $number || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the workspace object exists
        $obj = $axmud::CLIENT->desktopObj->ivShow('workspaceHash', $number);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Could not edit workspace object #' . $number . ' - object does not exist',
            );
        }

        # Open an 'edit' window for the workspace object
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Workspace',
                $session->mainWin,
                $session,
                'Edit workspace object #' . $number,
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit workspace object #' . $number,
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for workspace object #' . $number,
            );
        }
    }
}

{ package Games::Axmud::Cmd::RemoveWorkspace;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('removeworkspace', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rws', 'removews', 'removeworkspace'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Makes a workspace unavailable for ' . $axmud::SCRIPT . ' use';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $num,
            $check,
        ) = @_;

        # Local variables
        my ($workspaceObj, $gridCount, $mainCount, $msg, $choice);

        # Check for improper arguments
        if (! defined $num || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the workspace object exists
        $workspaceObj = $axmud::CLIENT->desktopObj->ivShow('workspaceHash', $num);
        if (! $workspaceObj) {

            return $self->error(
                $session, $inputString,
                'Workspace #' . $num . ' is not currently in use (try \';listworkspace\')',
            );
        }

        # The default workspace (the one in which Axmud opened) can't be removed
        if ($num == 0) {

            return $self->error(
                $session, $inputString,
                'The default workspace #0 can\'t be removed',
            );
        }

        # Count the number of 'grid' and 'main' windows that will close
        $gridCount = 0;
        $mainCount = 0;
        foreach my $gridObj ($workspaceObj->ivValues('gridHash')) {

            foreach my $winObj ($gridObj->ivValues('gridWinHash')) {

                $gridCount++;
                if ($winObj->winType eq 'main') {

                    $mainCount++;
                }
            }
        }

        if ($mainCount) {

            # Don't close a workspace with a shared 'main' window
            if ($axmud::CLIENT->shareMainWinFlag) {

                return $self->error(
                    $session, $inputString,
                    'Cannot remove workspace #' . $num . ' because it contains a shared \'main\''
                    . ' window',
                );

            } else {

                $msg = 'Do you really want to remove workspace #' . $num . '? (It contains ';

                if ($mainCount == 1) {
                    $msg .= ' \'main\' window';
                } else {
                    $msg .= $mainCount . ' \'main\' windows';
                }

                $msg .= ', which will close too)';

                $choice = $session->mainWin->showMsgDialogue(
                    'Remove workspace',
                    'question',
                    $msg,
                    'yes-no',
                );

                if ($choice ne 'yes') {

                    return $self->complete(
                        $session, $standardCmd,
                        'Remove workspace operation cancelled',
                    );

                }
            }
        }

        # Remove the workspace
        if (! $axmud::CLIENT->desktopObj->del_workspace($workspaceObj)) {

            return $self->error(
                $session, $inputString,
                'General error removing workspace #' . $num,
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Workspace #' . $num . ' made unavailable for use (\'grid\' windows closed: '
                . $gridCount . ')',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListWorkspace;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listworkspace', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lws', 'listws', 'listworkspace'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists all workspaces';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $msg,
            @workspaceList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Display header
        $session->writeText('Workspace status');

        $msg = '   ' . $axmud::SCRIPT . ' is capable of creating workspace grids: ';
        if ($axmud::CLIENT->desktopObj->gridPermitFlag) {
            $msg .= 'yes';
        } else {
            $msg .= 'no';
        }

        $session->writeText($msg);

        $msg = '   Workspace grids are activated in general:    ';
        if ($axmud::CLIENT->activateGridFlag) {
            $msg .= 'yes';
        } else {
            $msg .= 'no';
        }

        $session->writeText($msg);

        $session->writeText('List of workspaces currently used by ' . $axmud::SCRIPT);

        # Display list
        foreach my $workspaceObj (
            sort {$a->number <=> $b->number} ($axmud::CLIENT->desktopObj->ivValues('workspaceHash'))
        ) {
            $session->writeText(
                '   Workspace #' . $workspaceObj->number . ' (system: #'
                . $workspaceObj->systemNum . '\')',
            );

            $session->writeText(
                '      Size: width ' . $workspaceObj->currentWidth . ' height '
                . $workspaceObj->currentHeight,
            );

            $session->writeText(
                '      Panel sizes: left ' . $workspaceObj->panelLeftSize
                . ' right ' . $workspaceObj->panelRightSize . ' top ' . $workspaceObj->panelTopSize
                . ' bottom ' . $workspaceObj->panelBottomSize,
            );

            $session->writeText(
                '      Window controls sizes: left ' . $workspaceObj->controlsLeftSize
                . ' right ' . $workspaceObj->controlsRightSize . ' top '
                . $workspaceObj->controlsTopSize . ' bottom ' . $workspaceObj->controlsBottomSize,
            );

            if (! $workspaceObj->gridEnableFlag) {

                $session->writeText('      Workspace grids enabled: no');

            } else {

                $session->writeText('      Workspace grids enabled: yes');

                if (! $workspaceObj->gridHash) {

                    $session->writeText('         (no workspace grids found)');

                } else {

                $session->writeText('         Num  Session  Zonemap          Zones Windows');

                    foreach my $gridObj (
                        sort {$a->number <=> $b->number} ($workspaceObj->ivValues('gridHash'))
                    ) {
                        my $string;

                        if ($gridObj->owner) {
                            $string = $gridObj->owner->number;
                        } else {
                            $string = '(shared)';
                        }

                        $session->writeText(
                            '         ' . sprintf(
                                '%-4.4s %-8.8s %-16.16s %-6.6s',
                                $gridObj->number,
                                $string,
                                $gridObj->zonemap,
                                $gridObj->ivPairs('zoneHash'),
                            ) . $gridObj->ivPairs('gridWinHash'),
                        );
                    }
                }

                $session->writeText(
                    '      Workspace grids found: ' . $workspaceObj->ivPairs('gridHash')
                    . ', default zonemap: ' . $workspaceObj->defaultZonemap,
                );
            }
        }

        # Get a list of unused system workspace numbers (which might conceivably be different to
        #   the list found on the original call to GA::Obj::Desktop->detectWorkspaces)
        @workspaceList = $axmud::CLIENT->desktopObj->detectUnusedWorkspaces();
        if (@workspaceList) {

            $session->writeText('List of unused (but available) workspaces');
            foreach my $workspace (@workspaceList) {

                $session->writeText('   System #' . $workspace);
            }
        }

        # Display footer
        return $self->complete(
            $session, $standardCmd,
            'End of list (workspaces used: '
            . $axmud::CLIENT->desktopObj->ivPairs('workspaceHash')
            . ', not used but available: ' . (scalar @workspaceList) . ')',
        );
    }
}

{ package Games::Axmud::Cmd::SetWorkspaceDirection;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setworkspacedirection', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['swd', 'setwsdir', 'setworkspacedirection'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the way in which new workspaces are used';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my $dir;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the IV (for convenience)
        $dir = $axmud::CLIENT->initWorkspaceDir;

        # ;swd
        if (! defined $switch) {

            $session->writeText('Direction of workspace use: \'' . $dir . '\'');

            if ($dir eq 'move_left') {

                $session->writeText(
                    '   When adding workspaces, ' . $axmud::SCRIPT . ' moves left from the default'
                    . ' workspace until it reaches the far left (and then stops)',
                );

            } elsif ($dir eq 'move_right') {

                $session->writeText(
                    '   When adding workspaces, ' . $axmud::SCRIPT . ' moves right from the default'
                    . ' workspace until it reaches the far right (and then stops)',
                );

            } elsif ($dir eq 'start_left') {

                $session->writeText(
                    '   When adding workspaces, ' . $axmud::SCRIPT . ' starts with the default'
                    . ' workspace, then uses the first available workspace on the far left, then'
                    . ' moves right',
                );

            } else {

                $session->writeText(
                    '   When adding workspaces, ' . $axmud::SCRIPT . ' starts with the default'
                    . ' workspace, then uses the first available workspace on the far right, then'
                    . ' moves left',
                );
            }

            return $self->complete($session, $standardCmd, 'Workspace direction shown');

        } elsif ($switch ne '-l' && $switch ne '-r' && $switch ne '-b' && $switch ne '-e') {

            return $self->error(
                $session, $inputString,
                'Invalid switch (try \';help setworkspacedirection\')',
            );

        } else {

            if ($switch eq '-l') {
                $axmud::CLIENT->set_initWorkspaceDir('move_left');
            } elsif ($switch eq '-r') {
                $axmud::CLIENT->set_initWorkspaceDir('move_right');
            } elsif ($switch eq '-b') {
                $axmud::CLIENT->set_initWorkspaceDir('start_left');
            } elsif ($switch eq '-e') {
                $axmud::CLIENT->set_initWorkspaceDir('start_right');
            }

            return $self->complete(
                $session, $standardCmd,
                'Workspace direction set to \'' . $axmud::CLIENT->initWorkspaceDir . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddInitialWorkspace;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addinitialworkspace', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['aiw', 'addiw', 'addinitialworkspace'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds an initial workspace';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $zonemap,
            $check,
        ) = @_;

        # Local variables
        my $highest;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Initial workspaces are numbered consecutively, so find the highest-numbered existing one
        #   (the default initial workspace can't be deleted, so there is at least one)
        foreach my $num ($axmud::CLIENT->ivKeys('initWorkspaceHash')) {

            if (! defined $highest || $num > $highest) {

                $highest = $num;
            }
        }

        $highest++;

        if (! $axmud::CLIENT->add_initWorkspace($highest, $zonemap)) {

            return $self->error(
                $session, $inputString,
                'General error adding the initial workspace',
            );

        } elsif (! $zonemap) {

            return $self->complete(
                $session, $standardCmd,
                'Added initial workspace #' . $highest . ' with no default zonemap',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Added initial workspace #' . $highest . ' with the default zonemap \''
                . $zonemap . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ModifyInitialWorkspace;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('modifyinitialworkspace', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['miw', 'modiw', 'modifyinitialworkspace'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies an initial workspace';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $num, $zonemap,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $num || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the specified initial workspace exists
        if (! $axmud::CLIENT->ivExists('initWorkspaceHash', $num)) {

            return $self->error(
                $session, $inputString,
                'Initial workspace #' . $num . ' doesn\'t exist',
            );
        }

        # Modify the initial workspace
        if (! $axmud::CLIENT->add_initWorkspace($num, $zonemap)) {

            return $self->error(
                $session, $inputString,
                'General error modifying the initial workspace',
            );

        } elsif (! defined $zonemap) {

            return $self->complete(
                $session, $standardCmd,
                'Default zonemap for initial workspace #' . $num . ' disabled',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Default zonemap for initial workspace #' . $num . ' set to \'' . $zonemap . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteInitialWorkspace;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteinitialworkspace', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['diw', 'deliw', 'deleteinitialworkspace'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes an initial workspace';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $num,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the specified initial workspace can be deleted
        if (! $axmud::CLIENT->ivExists('initWorkspaceHash', $num)) {

            return $self->error(
                $session, $inputString,
                'Initial workspace #' . $num . ' doesn\'t exist',
            );

        } elsif ($num == 0) {

            return $self->error(
                $session, $inputString,
                'The default initial workspace (#' . $num . ') can\'t be deleted',
            );
        }

        if (! $axmud::CLIENT->del_initWorkspace($num)) {

            return $self->error(
                $session, $inputString,
                'General error deleting the initial workspace',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Initial workspace #' . $num . ' deleted (remaining initial workspaces have been'
                . ' renumbered)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListInitialWorkspace;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listinitialworkspace', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['liw', 'listiw', 'listinitialworkspace'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists initial workspace settings';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my %hash;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the IV (for convenience)
        %hash = $axmud::CLIENT->initWorkspaceHash;

        # Display header
        $session->writeText(
            'List of initial workspaces (direction: \'' . $axmud::CLIENT->initWorkspaceDir . '\')',
        );
        $session->writeText('   Num  Default zonemap');

        # Display list
        foreach my $num (sort {$a <=> $b} (keys %hash)) {

            my $zonemap = $hash{$num};
            if (! defined $zonemap) {

                $zonemap = '(none)';
            }

            $session->writeText('   ' . sprintf('%-4.4s %-16.16s', $num, $zonemap));
        }

        # Display footer
        if (scalar (keys %hash) == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 initial workspace found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar (keys %hash) . ' initial workspaces found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetWindowSize;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setwindowsize', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['swz', 'setwinsize', 'setwindowsize'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the default size of windows';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $width, $height,
            $check,
        ) = @_;

        # Local variables
        my $string;

        # Check for improper arguments
        if (
            (defined $switch && $switch ne '-m' && $switch ne '-g' && $switch ne '-f')
            || (defined $width && ! defined $height)
            || defined $check
        ) {
            return $self->improper($session, $inputString);
        }

        # If sizes are specified, check they are valid
        if (defined $width) {

            if (
                ! $axmud::CLIENT->intCheck(
                    $width,
                    100,
                    $axmud::CLIENT->constWorkspaceMaxWidth,
                )
            ) {
                return $self->error(
                    $session, $inputString,
                    'Invalid window width \'' . $width . '\' - must be in the range 100-'
                    . $axmud::CLIENT->constWorkspaceMaxWidth,
                );

            } elsif (
                ! $axmud::CLIENT->intCheck(
                    $height,
                    100,
                    $axmud::CLIENT->constWorkspaceMaxHeight,
                )
            ) {
                return $self->error(
                    $session, $inputString,
                    'Invalid window height \'' . $height . '\' - must be in the range 100-'
                    . $axmud::CLIENT->constWorkspaceMaxHeight,
                );
            }
        }

        # ;swz
        if (! defined $switch) {

            # Display header
            $session->writeText('List of \'grid\' window default sizes (in pixels)');

            # Display list
            $session->writeText(
                '   \'main\' windows:       width ' . $axmud::CLIENT->customMainWinWidth
                . ' height ' . $axmud::CLIENT->customMainWinHeight,
            );

            $session->writeText(
                '   Other \'grid\' windows: width ' . $axmud::CLIENT->customGridWinWidth
                . ' height ' . $axmud::CLIENT->customGridWinHeight,
            );

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of size list',
            );

        # ;swz -s <width> <height>
        # ;swz -s
        } elsif ($switch eq '-m') {

            if (! defined $width) {

                $axmud::CLIENT->set_customMainWinSize(
                    $axmud::CLIENT->constMainWinWidth,
                    $axmud::CLIENT->constMainWinHeight,
                );

                $string = 'reset';

            } else {

                $axmud::CLIENT->set_customMainWinSize($width, $height);
                $string = 'set';
            }

            return $self->complete(
                $session, $standardCmd,
                'Default \'main\' window size ' . $string . ' (width '
                . $axmud::CLIENT->customMainWinWidth . ' height '
                . $axmud::CLIENT->customMainWinHeight . ')',
            );

        # ;swz -g <width> <height>
        # ;swz -g
        } elsif ($switch eq '-g') {

            if (! defined $width) {

                $axmud::CLIENT->set_customGridWinSize(
                    $axmud::CLIENT->constGridWinWidth,
                    $axmud::CLIENT->constGridWinHeight,
                );

                $string = 'reset';

            } else {

                $axmud::CLIENT->set_customGridWinSize($width, $height);
                $string = 'set';
            }

            return $self->complete(
                $session, $standardCmd,
                'Default \'grid\' window size ' . $string . ' (width '
                . $axmud::CLIENT->customGridWinWidth . ' height '
                . $axmud::CLIENT->customGridWinHeight . ')',
            );

        # ;swz -f <width> <height>
        # ;swz -f
        } elsif ($switch eq '-f') {

            if (! defined $width) {

                $axmud::CLIENT->set_customFreeWinSize(
                    $axmud::CLIENT->constFreeWinWidth,
                    $axmud::CLIENT->constFreeWinHeight,
                );

                $string = 'reset';

            } else {

                $axmud::CLIENT->set_customFreeWinSize($width, $height);
                $string = 'set';
            }

            return $self->complete(
                $session, $standardCmd,
                'Default \'free\' window size ' . $string . ' (width '
                . $axmud::CLIENT->customFreeWinWidth . ' height '
                . $axmud::CLIENT->customFreeWinHeight . ')',
            );
        }
    }
}

{ package Games::Axmud::Cmd::TestWindowControls;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('testwindowcontrols', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['twc', 'testwc', 'testcontrols', 'testwindowcontrols'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Finds the size of window controls';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $num,
            $check,
        ) = @_;

        # Local variables
        my (
            $workspaceObj,
            @sizeList, @sideList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Decide which workspace should perform the test
        if (! defined $num) {

            # If none specified, use the default workspace
            $num = 0;
        }

        # Check the specified workspace exists
        $workspaceObj = $axmud::CLIENT->desktopObj->ivShow('workspaceHash', $num);
        if (! $workspaceObj) {

            return $self->error($session, $inputString, 'Workspace #' . $num . ' doesn\'t exist');
        }

        # Perform the test
        @sizeList = $workspaceObj->findWinControlSize();
        if (! @sizeList) {

            return $self->error(
                $session, $inputString,
                'Window controls test on workspace #' . $num . ' failed',
            );

        } else {

            # Show header
            $session->writeText('Window controls test on workspace #' . $num . ' succeeded');
            $session->writeText('   Side    Size detected    Custom size      Using size');

            # Show list
            @sideList = ('left', 'right', 'top', 'bottom');

            do {

                my ($size, $side, $iv, $iv2, $string);

                $size = shift @sizeList;
                $side = shift @sideList;
                $iv = 'customControls' . ucfirst($side) . 'Size';   # e.g. ->customControlsLeftSize
                $iv2 = 'controls' . ucfirst($side) . 'Size';        # e.g. ->controlsLeftSize

                if (defined $axmud::CLIENT->$iv) {
                    $string = $axmud::CLIENT->$iv;
                } else {
                    $string = '(not set)';
                }

                $session->writeText(
                    '   ' . sprintf(
                        '%-8.8s %-16.16s %-16.16s %-16.16s',
                        $side,
                        $size,
                        $string,
                        $workspaceObj->$iv2,
                    ),
                );

            } until (! @sizeList);

            # Show footer
            return $self->complete($session, $standardCmd, 'End of test');
        }
    }
}

{ package Games::Axmud::Cmd::SetWindowControls;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setwindowcontrols', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['swc', 'setwc', 'setcontrols', 'setwindowcontrols'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets custom window controls sizes';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $left, $leftFlag, $right, $rightFlag, $top, $topFlag, $bottom, $bottomFlag,
            $string,
        );

        # Extract switches
        ($switch, $left, @args) = $self->extract('-l', 1, @args);
        if (defined $switch) {

            $leftFlag = TRUE;
        }

        ($switch, $right, @args) = $self->extract('-r', 1, @args);
        if (defined $switch) {

            $rightFlag = TRUE;
        }

        ($switch, $top, @args) = $self->extract('-t', 1, @args);
        if (defined $switch) {

            $topFlag = TRUE;
        }

        ($switch, $bottom, @args) = $self->extract('-b', 1, @args);
        if (defined $switch) {

            $bottomFlag = TRUE;
        }

        # There should be no further arguments
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # At least one switch option is required
        if (! $leftFlag && ! $rightFlag && ! $topFlag && ! $bottomFlag) {

            return $self->error($session, $inputString, 'Set which window controls sizes?');
        }

        # Check custom window controls
        if ($leftFlag && defined $left && ! $axmud::CLIENT->intCheck($left, 0, 100)) {

            return $self->error(
                $session, $inputString,
                'Invalid window controls size \'' . $left . '\' (must be an integer in the'
                . ' range 0-100)',
            );
        }

        if ($rightFlag && defined $right && ! $axmud::CLIENT->intCheck($right, 0, 100)) {

            return $self->error(
                $session, $inputString,
                'Invalid window controls size \'' . $right . '\' (must be an integer in the'
                . ' range 0-100)',
            );
        }

        if ($topFlag && defined $top && ! $axmud::CLIENT->intCheck($top, 0, 100)) {

            return $self->error(
                $session, $inputString,
                'Invalid window controls size \'' . $top . '\' (must be an integer in the'
                . ' range 0-100)',
            );
        }

        if ($bottomFlag && defined $bottom && ! $axmud::CLIENT->intCheck($bottom, 0, 100)) {

            return $self->error(
                $session, $inputString,
                'Invalid window controls size \'' . $bottom . '\' (must be an integer in the'
                . ' range 0-100)',
            );
        }

        # Update IVs
        if ($leftFlag) {

            $axmud::CLIENT->set_customControlsSize('left', $left);
        }

        if ($rightFlag) {

            $axmud::CLIENT->set_customControlsSize('right', $right);
        }

        if ($topFlag) {

            $axmud::CLIENT->set_customControlsSize('top', $top);
        }

        if ($bottomFlag) {

            $axmud::CLIENT->set_customControlsSize('bottom', $bottom);
        }

        # Show confirmation
        if (defined $axmud::CLIENT->customControlsLeftSize) {
            $string = 'left: ' . $axmud::CLIENT->customControlsLeftSize;
        } else {
            $string = 'left: (not set)';
        }

        if (defined $axmud::CLIENT->customControlsRightSize) {
            $string .= ', right: ' . $axmud::CLIENT->customControlsRightSize;
        } else {
            $string .= ', right: (not set)';
        }

        if (defined $axmud::CLIENT->customControlsTopSize) {
            $string .= ', top: ' . $axmud::CLIENT->customControlsTopSize;
        } else {
            $string .= ', top: (not set)';
        }

        if (defined $axmud::CLIENT->customControlsBottomSize) {
            $string .= ', bottom: ' . $axmud::CLIENT->customControlsBottomSize;
        } else {
            $string .= ', bottom: (not set)';
        }

        return $self->complete(
            $session,
            $standardCmd,
            'Custom win controls sizes set - ' . $string,
        );
    }
}

{ package Games::Axmud::Cmd::ListWindowControls;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listwindowcontrols', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lwc', 'listwc', 'listcontrols', 'listwindowcontrols'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists window controls sizes';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Display header
        $session->writeText('List of window controls sizes');
        $session->writeText('            Custom size      Default size');

        # Display list
        @list = ('left', 'right', 'top', 'bottom');
        foreach my $item (@list) {

            my ($customIV, $customString, $constIV, $constString);

            $customIV = 'customControls' . ucfirst($item) . 'Size';
            if (defined $axmud::CLIENT->$customIV) {
                $customString = $axmud::CLIENT->$customIV;
            } else {
                $customString = 'not set';
            }

            $constIV = 'constControls' . ucfirst($item) . 'Size';
            if (defined $axmud::CLIENT->$constIV) {
                $constString = $axmud::CLIENT->$constIV;
            } else {
                $constString = 'not set';
            }

            $session->writeText(
                '   ' . sprintf(
                    '%-8.8s %-16.16s %-16.16s',
                    ucfirst($item),
                    $customString,
                    $constString,
                ),
            );
        }

        # Display header
        $session->writeText('List of window controls sizes in workspaces');
        $session->writeText('   Wkspace  Left  Right Top   Bottom');

        # Display list
        foreach my $workspaceObj (
            sort {$a->number <=> $b->number} ($axmud::CLIENT->desktopObj->ivValues('workspaceHash'))
        ) {
            $session->writeText(
                '   ' . sprintf(
                    '%-4.4s     %-5.5s %-5.5s %-5.5s %-5.5s',
                    $workspaceObj->number,
                    $workspaceObj->controlsLeftSize,
                    $workspaceObj->controlsRightSize,
                    $workspaceObj->controlsTopSize,
                    $workspaceObj->controlsBottomSize,
                ),
            );
        }

        # Display footer
        return $self->complete($session, $standardCmd, 'End of lists');
    }
}

{ package Games::Axmud::Cmd::TestPanel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('testpanel', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tpn', 'testpn', 'testpanel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Detects sizes of workspace panels (taskbars)';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $num,
            $check,
        ) = @_;

        # Local variables
        my (
            $workspaceObj,
            @sizeList, @sideList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Decide which workspace should perform the test
        if (! defined $num) {

            # If none specified, use the default workspace
            $num = 0;
        }

        # Check the specified workspace exists
        $workspaceObj = $axmud::CLIENT->desktopObj->ivShow('workspaceHash', $num);
        if (! $workspaceObj) {

            return $self->error($session, $inputString, 'Workspace #' . $num . ' doesn\'t exist');
        }

        # Perform the test
        @sizeList = $workspaceObj->findPanelSize();
        if (! @sizeList) {

            return $self->error(
                $session, $inputString,
                'Panel test on workspace #' . $num . ' failed',,
            );

        } else {

            # Show header
            $session->writeText('Panel test on workspace #' . $num . ' succeeded');
            $session->writeText('   Panel    Size detected    Custom size      Using size');

            # Show list
            @sideList = ('left', 'right', 'top', 'bottom');

            do {

                my ($size, $side, $iv, $iv2, $string);

                $size = shift @sizeList;
                $side = shift @sideList;
                $iv = 'customPanel' . ucfirst($side) . 'Size';      # e.g. ->customPanelLeftSize
                $iv2 = 'panel' . ucfirst($side) . 'Size';           # e.g. ->panelLeftSize

                if (defined $axmud::CLIENT->$iv) {
                    $string = $axmud::CLIENT->$iv;
                } else {
                    $string = '(not set)';
                }

                $session->writeText(
                    '   ' . sprintf(
                        '%-8.8s %-16.16s %-16.16s %-16.16s',
                        $side,
                        $size,
                        $string,
                        $workspaceObj->$iv2,
                    ),
                );

            } until (! @sizeList);

            # Show footer
            return $self->complete($session, $standardCmd, 'End of test');
        }
    }
}

{ package Games::Axmud::Cmd::SetPanel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setpanel', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['spn', 'setpn', 'setpanel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets custom workspace panel (taskbar) sizes';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $left, $leftFlag, $right, $rightFlag, $top, $topFlag, $bottom, $bottomFlag,
            $string,
        );

        # Extract switches
        ($switch, $left, @args) = $self->extract('-l', 1, @args);
        if (defined $switch) {

            $leftFlag = TRUE;
        }

        ($switch, $right, @args) = $self->extract('-r', 1, @args);
        if (defined $switch) {

            $rightFlag = TRUE;
        }

        ($switch, $top, @args) = $self->extract('-t', 1, @args);
        if (defined $switch) {

            $topFlag = TRUE;
        }

        ($switch, $bottom, @args) = $self->extract('-b', 1, @args);
        if (defined $switch) {

            $bottomFlag = TRUE;
        }

        # There should be no further arguments
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # At least one switch option is required
        if (! $leftFlag && ! $rightFlag && ! $topFlag && ! $bottomFlag) {

            return $self->error($session, $inputString, 'Set which panel sizes?');
        }

        # Check custom panel sizes
        if (
            $leftFlag
            && defined $left
            && ! $axmud::CLIENT->intCheck($left, 0, $axmud::CLIENT->constWorkspaceMaxWidth)
        ) {
            return $self->error(
                $session, $inputString,
                'Invalid custom panels size \'' . $left . '\' (must be an integer in the range 0-'
                . $axmud::CLIENT->constWorkspaceMaxWidth . ')',
            );
        }

        if (
            $rightFlag
            && defined $right
            && ! $axmud::CLIENT->intCheck($right, 0, $axmud::CLIENT->constWorkspaceMaxWidth)
        ) {
            return $self->error(
                $session, $inputString,
                'Invalid custom panels size \'' . $right . '\' (must be an integer in the range 0-'
                . $axmud::CLIENT->constWorkspaceMaxWidth . ')',
            );
        }

        if (
            $topFlag
            && defined $top
            && ! $axmud::CLIENT->intCheck($top, 0, $axmud::CLIENT->constWorkspaceMaxHeight)
        ) {
            return $self->error(
                $session, $inputString,
                'Invalid custom panels size \'' . $top . '\' (must be an integer in the range 0-'
                . $axmud::CLIENT->constWorkspaceMaxHeight . ')',
            );
        }

        if (
            $bottomFlag
            && defined $bottom
            && ! $axmud::CLIENT->intCheck($bottom, 0, $axmud::CLIENT->constWorkspaceMaxHeight)
        ) {
            return $self->error(
                $session, $inputString,
                'Invalid custom panels size \'' . $bottom . '\' (must be an integer in the range 0-'
                . $axmud::CLIENT->constWorkspaceMaxHeight . ')',
            );
        }

        # Update IVs
        if ($leftFlag) {

            $axmud::CLIENT->set_customPanelSize('left', $left);
        }

        if ($rightFlag) {

            $axmud::CLIENT->set_customPanelSize('right', $right);
        }

        if ($topFlag) {

            $axmud::CLIENT->set_customPanelSize('top', $top);
        }

        if ($bottomFlag) {

            $axmud::CLIENT->set_customPanelSize('bottom', $bottom);
        }

        # Show confirmation
        if (defined $axmud::CLIENT->customPanelLeftSize) {
            $string = 'left: ' . $axmud::CLIENT->customPanelLeftSize;
        } else {
            $string = 'left: (not set)';
        }

        if (defined $axmud::CLIENT->customPanelRightSize) {
            $string .= ', right: ' . $axmud::CLIENT->customPanelRightSize;
        } else {
            $string .= ', right: (not set)';
        }

        if (defined $axmud::CLIENT->customPanelTopSize) {
            $string .= ', top: ' . $axmud::CLIENT->customPanelTopSize;
        } else {
            $string .= ', top: (not set)';
        }

        if (defined $axmud::CLIENT->customPanelBottomSize) {
            $string .= ', bottom: ' . $axmud::CLIENT->customPanelBottomSize;
        } else {
            $string .= ', bottom: (not set)';
        }

        return $self->complete(
            $session,
            $standardCmd,
            'Custom panel sizes set - ' . $string,
        );
    }
}

{ package Games::Axmud::Cmd::ListPanel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listpanel', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lpn', 'listpn', 'listpanel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists workspace panel (taskbar) sizes';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Display header
        $session->writeText('List of workspace panel sizes');
        $session->writeText('            Custom size      Default size');

        # Display list
        @list = ('left', 'right', 'top', 'bottom');
        foreach my $item (@list) {

            my ($customIV, $customString, $constIV, $constString);

            $customIV = 'customPanel' . ucfirst($item) . 'Size';
            if (defined $axmud::CLIENT->$customIV) {
                $customString = $axmud::CLIENT->$customIV;
            } else {
                $customString = 'not set';
            }

            $constIV = 'constPanel' . ucfirst($item) . 'Size';
            if (defined $axmud::CLIENT->$constIV) {
                $constString = $axmud::CLIENT->$constIV;
            } else {
                $constString = 'not set';
            }

            $session->writeText(
                '   ' . sprintf(
                    '%-8.8s %-16.16s %-16.16s',
                    ucfirst($item),
                    $customString,
                    $constString,
                ),
            );
        }

        # Display header
        $session->writeText('List of panel sizes in workspaces');
        $session->writeText('   Wkspace  Left  Right Top   Bottom');

        # Display list
        foreach my $workspaceObj (
            sort {$a->number <=> $b->number} ($axmud::CLIENT->desktopObj->ivValues('workspaceHash'))
        ) {
            $session->writeText(
                '   ' . sprintf(
                    '%-4.4s     %-5.5s %-5.5s %-5.5s %-5.5s',
                    $workspaceObj->number,
                    $workspaceObj->panelLeftSize,
                    $workspaceObj->panelRightSize,
                    $workspaceObj->panelTopSize,
                    $workspaceObj->panelBottomSize,
                ),
            );
        }

        # Display footer
        return $self->complete($session, $standardCmd, 'End of list');
    }
}

# Winmaps and winzones

{ package Games::Axmud::Cmd::AddWinmap;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addwinmap', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['awm', 'addwm', 'addwinmap'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a new winmap';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the winmap doesn't already exist
        if ($axmud::CLIENT->ivExists('winmapHash', $name)) {

            return $self->error(
                $session, $inputString,
                'The winmap \'' . $name . '\' already exists',
            );

        # Check the name is valid
        } elsif (! $axmud::CLIENT->nameCheck($name, 16)) {

            return $self->error($session, $inputString, 'Illegal name \'' . $name . '\'');
        }

        # Add the winmap
        $obj = Games::Axmud::Obj::Winmap->new($name);
        if (!$obj) {

            return $self->error(
                $session, $inputString,
                'Could not add the winmap \'' . $name . '\'',
            );

        } else {

            $axmud::CLIENT->add_winmap($obj);

            return $self->complete($session, $standardCmd, 'Winmap \'' . $name . '\' created');
        }
    }
}

{ package Games::Axmud::Cmd::CloneWinmap;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('clonewinmap', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cwm', 'clonewm', 'clonewinmap'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Clones a winmap';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $oldName, $newName,
            $check,
        ) = @_;

        # Local variables
        my ($oldObj, $newObj);

        # Check for improper arguments
        if (! defined $oldName || ! defined $newName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the (old) winmap exists
        $oldObj = $axmud::CLIENT->ivShow('winmapHash', $oldName);
        if (! $oldObj) {

            return $self->error(
                $session, $inputString,
                'The winmap \'' . $oldName . '\' doesn\'t exist',
            );
        }

        # Check that the new name doesn't exist and is valid
        if ($axmud::CLIENT->ivExists('winmapHash', $newName)) {

            return $self->error(
                $session, $inputString,
                'The winmap \'' . $newName . '\' already exists',
            );

        } elsif (! $axmud::CLIENT->nameCheck($newName, 16)) {

            return $self->error($session, $inputString, 'Illegal name \'' . $newName . '\'');
        }

        # Clone the winmap
        $newObj = $oldObj->clone($newName);
        if (! $newObj) {

            return $self->error(
                $session, $inputString,
                'Could not clone the winmap \'' . $oldName . '\'',
            );

        } else {

            $axmud::CLIENT->add_winmap($newObj);

            return $self->complete(
                $session, $standardCmd,
                'Winmap \'' . $oldName . '\' cloned as the new winmap \'' . $newName . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditWinmap;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editwinmap', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ewm', 'editwm', 'editwinmap'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a winmap';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the winmap exists
        $obj = $axmud::CLIENT->ivShow('winmapHash', $name);
        if (! $obj) {
            return $self->error(
                $session, $inputString,
                'Could not edit the \'' . $name . '\' winmap - object does not exist',
            );
        }

        # Open an 'edit' window for the winmap
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Winmap',
                $session->mainWin,
                $session,
                'Edit winmap \'' . $obj->name . '\'',
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the \'' . $name . '\' winmap',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the \'' . $name . '\' winmap',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ModifyWinmap;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('modifywinmap', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mwm', 'modwm', 'modifywinmap'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies an existing winmap';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name, $switch,
            @args,
        ) = @_;

        # Local variables
        my (
            $winmapObj, $item, $item2, $item3, $count,
            @list,
            %hash,
        );

        # Check for improper arguments
        if (
            ! defined $name
            || ! defined $switch
            || (
                $switch ne '-w' && $switch ne '-o' && $switch ne '-l' && $switch ne '-b'
                && $switch ne '-e' && $switch ne '-u' && $switch ne '-d' && $switch ne '-v'
                && $switch ne '-t' && $switch ne '-s'
            )
        ) {
            return $self->improper($session, $inputString);
        }

        # Check the winmap exists
        $winmapObj = $axmud::CLIENT->ivShow('winmapHash', $name);
        if (! $winmapObj) {

            return $self->error(
                $session, $inputString,
                'The winmap \'' . $name . '\' doesn\'t exist',
            );

        # Check it's not a standard winmap
        } elsif ($axmud::CLIENT->ivExists('standardWinmapHash', $name)) {

            return $self->error(
                $session, $inputString,
                'Standard winmaps like \'' . $name . '\' can\'t be modified',
            );
        }

        # ;mwm -w <world> <world> ...
        # ;mwm -w
        if ($switch eq '-w') {

            foreach my $world (@args) {

                $hash{$world} = undef;
            }

            $winmapObj->set_worldHash(%hash);

            return $self->complete(
                $session, $standardCmd,
                'Winmap \'' . $name . '\' modified (specified worlds: '
                . $winmapObj->ivPairs('worldHash') . ')',
            );

        # ;mwm -o top|bottom|left|right
        } elsif ($switch eq '-o') {

            $item = shift @args;
            if (
                ! defined $item
                || @args
                || ($item ne 'top' && $item ne 'bottom' && $item ne 'left' && $item ne 'right')
            ) {
                return $self->error(
                    $session, $inputString,
                    'Invalid, missing or multiple argument error',
                );

            } else {

                $winmapObj->set_orientation($item);

                return $self->complete(
                    $session, $standardCmd,
                    'Winmap \'' . $name . '\' modified (orientation: ' . $winmapObj->orientation
                    . ')',
                );
            }

        # ;mwm -b <package>
        # ;mwm -b <package> <key> <value> <key> <value> ...
        # ;mwm -e <package>
        # ;mwm -e <package> <key> <value> <key> <value> ...
        } elsif ($switch eq '-b' || $switch eq '-e') {

            $item = shift @args;
            if (! defined $item) {

                return $self->error(
                    $session, $inputString,
                    'Invalid, missing or multiple argument error',
                );
            }

            # Extract <key> <value> <key> <value> ... into a hash
            if (@args) {

                do {

                    my ($key, $value);

                    $key = shift @args;
                    $value = shift @args;

                    $hash{$key} = $value;

                } until (! @args);
            }

            # Update the IV
            @list = $winmapObj->stripInitList;
            if ($switch eq '-b') {
                unshift (@list, $item, \%hash);
            } else {
                push (@list, $item, \%hash);
            }

            $winmapObj->set_stripInitList(@list);

            return $self->complete(
                $session, $standardCmd,
                'Initial strip object list for winmap \'' . $name . '\' set (packages: '
                . ((scalar (@list)) / 2) . ')',
            );

        # ;mwm -u <number>
        # ;mwm -d <number>
        # ;mwm -v <number>
        } elsif ($switch eq '-u' || $switch eq 'd' || $switch eq '-v') {

            $item = shift @args;
            if (! defined $item || ! $axmud::CLIENT->intCheck($item, 0) || @args) {

                return $self->error(
                    $session, $inputString,
                    'Invalid, missing or multiple argument error',
                );
            }

            # Check package <number> exists
            @list = $winmapObj->stripInitList;
            $count = (scalar (@list)) / 2;

            if (! $count) {

                return $self->error(
                    $session, $inputString,
                    'The initial strip object list for winmap \'' . $name . '\' is empty',
                );

            } elsif ($item >= $count) {

                return $self->error(
                    $session, $inputString,
                    'Invalid number; initial strip object list for winmap \'' . $name
                    . '\' contains ' . $count . ' packages',
                );
            }

            # ->stripInitList is in the form
            #   (package_name, hash_reference, package_name, hash_reference...)
            ($item2, $item3) = splice (@list, ($item * 2), 2);

            # Update the IV
            if ($switch eq '-u') {
                unshift (@list, $item2, $item3);
            } elsif ($switch eq '-d') {
                push (@list, $item2, $item3);
            }

            $winmapObj->set_stripInitList(@list);

            return $self->complete(
                $session, $standardCmd,
                'Initial strip object list for winmap \'' . $name . '\' updated (packages: '
                . ((scalar (@list)) / 2) . ')',
            );

        # ;mwm -l
        } elsif ($switch eq '-l') {

            # (Ignore any arguments after the -l switch)

            if (! $winmapObj->stripInitList) {

                return $self->complete(
                    $session, $standardCmd,
                    'Winmap \'' . $name . '\' has no initial strip object list',
                );

            } else {

                # Display header
                $session->writeText(
                    'Initial strip object list for winmap \'' . $name . '\''
                );

                # Display list
                @list = $winmapObj->stripInitList;
                $count = 0;

                do {

                    my ($package, $hashRef);

                    $package = shift @list;
                    $hashRef = shift @list;

                    $session->writeText('   ' . sprintf('%-4.4s ', $count) . $package);
                    if (! defined $hashRef || ! %$hashRef) {

                        $session->writeText('      (no initialisation settings)');

                    } else {

                        foreach my $key (sort {lc($a) cmp lc($b)} (keys %$hashRef)) {

                            my $value = $$hashRef{$key};

                            if (defined $value) {
                                $session->writeText('      ' . $key . ' : ' . $value);
                            } else {
                                $session->writeText('      ' . $key . ' : <undef>');
                            }
                        }
                    }

                    $count++;

                } until (! @list);

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (strip objects found: ' .  $count . ')',
                );
            }

        # ;mwm -s <width> <height>
        } elsif ($switch eq '-s') {

            $item = shift @args;
            $item2 = shift @args;

            if (
                ! defined $item
                || ! $axmud::CLIENT->intCheck($item, 0)
                || ! defined $item2
                || ! $axmud::CLIENT->intCheck($item2, 0)
                || @args
            ) {
                return $self->error(
                    $session, $inputString,
                    'Invalid, missing or multiple argument error',
                );
            }

            if (
                $item < 1
                || $item2 < 1
                || $item > $winmapObj->tableSize
                || $item2 > $winmapObj->tableSize
            ) {
                return $self->error(
                    $session, $inputString,
                    'The <width> and <height> arguments must be in the range 1-60',
                );
            }

            $winmapObj->set_zoneSize($item, $item2);

            return $self->complete(
                $session, $standardCmd,
                'Winmap \'' . $name . '\' modified (standard zone size: '
                . $winmapObj->zoneWidth . 'x' . $winmapObj->zoneHeight . ')',
            );
        }
    }

    ##################
    # Methods
}

{ package Games::Axmud::Cmd::DeleteWinmap;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletewinmap', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dwm', 'delwm', 'deletewm', 'deletewinmap'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a winmap';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the winmap exists
        if (! $axmud::CLIENT->ivExists('winmapHash', $name)) {

            return $self->error(
                $session, $inputString,
                'The winmap \'' . $name . '\' doesn\'t exist',
            );

        # Check it's not a standard winmap
        } elsif ($axmud::CLIENT->ivExists('standardWinmapHash', $name)) {

            return $self->error(
                $session, $inputString,
                'Standard winmaps like \'' . $name . '\' can\'t be deleted',
            );
        }

        # Delete the winmap
        $axmud::CLIENT->del_winmap($name);

        # Operation complete
        return $self->complete($session, $standardCmd, 'Winmap \'' . $name . '\' deleted');
    }
}

{ package Games::Axmud::Cmd::ResetWinmap;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resetwinmap', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rwm', 'resetwm', 'resetwinmap'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets a winmap';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $winmapObj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the winmap exists
        $winmapObj = $axmud::CLIENT->ivShow('winmapHash', $name);
        if (! $winmapObj) {

            return $self->error(
                $session, $inputString,
                'The winmap \'' . $name . '\' doesn\'t exist',
            );

        # Check it's not a standard winmap
        } elsif ($axmud::CLIENT->ivExists('standardWinmapHash', $name)) {

            return $self->error(
                $session, $inputString,
                'Standard winmaps like \'' . $name . '\' can\'t be reset',
            );
        }

        # Reset the winmap
        if (! $winmapObj->resetWinmap()) {

            return $self->error(
                $session, $inputString,
                'The winmap \'' . $name . '\' couldn\'t be reset',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'The winmap \'' . $name . '\' has been reset',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetDefaultWinmap;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setdefaultwinmap', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sdw', 'setdw', 'setwinmap', 'setdefaultwinmap'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets default winmap for \'main\'/\'internal\' windows';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $name,
            $check,
        ) = @_;

        # Check for improper arguments
        if (
            (
                defined $switch && $switch ne '-a' && $switch ne '-d' && $switch ne '-g'
                && $switch ne '-r'
            ) || (defined $switch && $switch eq '-r' && defined $name)
            || (defined $switch && $switch ne '-r' && ! defined $name)
            || defined $check
        ) {
            return $self->improper($session, $inputString);
        }

        # ;sdw
        if (! $switch) {

            return $self->complete(
                $session, $standardCmd,
                'Default winmap for \'main\' windows: grids activated - \''
                . $axmud::CLIENT->defaultEnabledWinmap . '\', disactivated - \''
                . $axmud::CLIENT->defaultDisabledWinmap . '\', for other \'internal\' windows: \''
                . $axmud::CLIENT->defaultInternalWinmap . '\'',
            );

        # ;sdw -r
        } elsif ($switch eq '-r') {

            # Use constant values
            $axmud::CLIENT->set_defaultEnabledWinmap($axmud::CLIENT->constDefaultEnabledWinmap);
            $axmud::CLIENT->set_defaultDisabledWinmap($axmud::CLIENT->constDefaultDisabledWinmap);
            $axmud::CLIENT->set_defaultInternalWinmap($axmud::CLIENT->constDefaultInternalWinmap);

            return $self->complete(
                $session, $standardCmd,
               'Default winmap for \'main\' windows: grids activated - \''
                . $axmud::CLIENT->defaultEnabledWinmap . '\', disactivated - \''
                . $axmud::CLIENT->defaultDisabledWinmap . '\', for other \'internal\' windows: \''
                . $axmud::CLIENT->defaultInternalWinmap . '\'',
            );

        # ;sdw -a <name>
        # ;sdw -d <name>
        # ;sdw -g <name>
        } else {

            # Check the specified winmap exists
            if (! $axmud::CLIENT->ivExists('winmapHash', $name)) {

                return $self->error(
                    $session, $inputString,
                    'The winmap \'' . $name . '\' doesn\'t exist',
                );
            }

            # Update IVs
            if ($switch eq '-a') {

                $axmud::CLIENT->set_defaultEnabledWinmap($name);

                return $self->complete(
                    $session, $standardCmd,
                    'Default winmap for \'main\' windows (when workspace grids activated) set to \''
                     . $name . '\'',
                );

            } elsif ($switch eq '-d') {

                $axmud::CLIENT->set_defaultDisabledWinmap($name);

                return $self->complete(
                    $session, $standardCmd,
                    'Default winmap for \'main\' windows (when workspace grids disactivated)'
                    . ' set to \'' . $name . '\'',
                );

            } else {

                $axmud::CLIENT->set_defaultInternalWinmap($name);

                return $self->complete(
                    $session, $standardCmd,
                    'Default winmap for other \'internal\' windows set to \'' . $name . '\'',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ListWinmap;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listwinmap', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lwm', 'listwm', 'listwinmap'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists all winmaps';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my (
            $count, $winmapObj, $string,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;lwm
        # ;lwm -s
        if (! defined $arg || $arg eq '-s') {

            # Import a list of winmap names, sorted alphabetically
            @list = sort {lc($a) cmp lc($b)} ($axmud::CLIENT->ivKeys('winmapHash'));
            if (! @list) {

                return $self->complete($session, $standardCmd, 'The winmap list is empty');
            }

            # Display header
            $session->writeText(
                'List of winmaps (@ standard, A/D - default for \'main\','
                . ' G - default for \'internal\')',
            );

            $session->writeText('      Name             Winzones Status');

            # Display list
            $count = 0;
            OUTER: foreach my $name (@list) {

                my ($obj, $column, $fullString);

                $obj = $axmud::CLIENT->ivShow('winmapHash', $name);

                if (! $axmud::CLIENT->ivExists('standardWinmapHash', $name)) {

                    if (defined $arg && $arg eq '-s') {

                        # This isn't a standard winmap, so don't list it
                        next OUTER;

                    } else {

                        $column = '  ';
                    }

                } else {

                    $count++;
                    $column = ' @';
                }

                if ($name eq $axmud::CLIENT->defaultEnabledWinmap) {
                    $column .= 'A';
                } else {
                    $column .= ' ';
                }

                if ($name eq $axmud::CLIENT->defaultDisabledWinmap) {
                    $column .= 'D';
                } else {
                    $column .= ' ';
                }

                if ($name eq $axmud::CLIENT->defaultInternalWinmap) {
                    $column .= 'G ';
                } else {
                    $column .= '  ';
                }

                if ($obj->fullFlag) {
                    $fullString = 'Full';
                } else {
                    $fullString = 'Not full';
                }

                $session->writeText(
                    $column . sprintf(
                        '%-16.16s %-8.8s %-8.8s',
                        $name,
                        $obj->ivPairs('zoneHash'),
                        $fullString,
                    )
                );
            }

            # Display footer
            if (defined $arg) {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (standard winmaps: ' . $count . ')',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (winmaps: ' . scalar (@list) . ', standard winmaps: ' . $count
                    . ')',
                );
            }

        # ;lwm <name>
        } else {

            # Check the winmap exists
            $winmapObj = $axmud::CLIENT->ivShow('winmapHash', $arg);
            if (! $winmapObj) {

                return $self->error(
                    $session, $inputString,
                    'The winmap \'' . $arg . '\' doesn\'t exist',
                );
            }

            # Display header
            $session->writeText('Properties of winmap \'' . $arg . '\'');

            # Display list
            if ($winmapObj->fullFlag) {
                $string = 'yes';
            } else {
                $string = 'no';
            }

            $session->writeText(
                '   Winzones: ' . $winmapObj->ivPairs('zoneHash') . ', full: '. $string);

            if (! $winmapObj->worldHash) {
                $string = '(none)';
            } else {
                $string = join(' ', sort {lc($a) cmp lc($b)} ($winmapObj->ivKeys('worldHash')));
            }

            $session->writeText('   Specified worlds: ' . $string);

            $session->writeText('   Strip object order: ' . $winmapObj->orientation);
            if (! $winmapObj->stripInitList) {

                $session->writeText('      (strip object list is empty)');

            } else {

                @list = $winmapObj->stripInitList;
                do {

                    my ($package, $hashRef, $descrip);

                    $package = shift @list,
                    $hashRef = shift @list;

                    $descrip = $axmud::CLIENT->ivShow('customStripHash', $package);
                    if (! $descrip) {
                        $session->writeText('      Not loaded (' . $package . ')');
                    } else {
                        $session->writeText('      ' . $descrip . ' (' . $package . ')');
                    }

                    if (! defined $hashRef || ! %$hashRef) {

                        $session->writeText('         Init: (no settings)');

                    } else {

                        foreach my $key (keys %$hashRef) {

                            $session->writeText('         Init: ' . $key . ' - ' . $$hashRef{$key});
                        }
                    }

                } until (! @list);
            }

            $session->writeText(
                '   Table size: width ' . $winmapObj->tableSize . ' height '
                . $winmapObj->tableSize,
            );

            $session->writeText(
                '   Standard zone size: width ' . $winmapObj->zoneWidth . ' height '
                . $winmapObj->zoneHeight,
            );

            # Display footer
            return $self->complete($session, $standardCmd, 'End of property list');
        }
    }
}

{ package Games::Axmud::Cmd::AddWinzone;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addwinzone', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['awz', 'addwz', 'addwinzone'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a new winzone';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args
        ) = @_;

        # Local variables
        my (
            $successFlag,
            $xPosBlocks, $yPosBlocks, $widthBlocks, $heightBlocks, $packageName, $objName,
            $positionFlag, $sizeFlag, $packageNameFlag, $objNameFlag,
            $initHashRef, $removeHashRef,
            $winmapName, $winmapObj, $winzoneObj,
        );

        # Extract compulsory and optional switch options
        (
            $successFlag,
            $xPosBlocks, $yPosBlocks, $widthBlocks, $heightBlocks, $packageName, $objName,
            $positionFlag, $sizeFlag, $packageNameFlag, $objNameFlag,
            $initHashRef, $removeHashRef,
            @args,
        ) = $self->extractWinzoneSwitches($session, $inputString, $standardCmd, @args);

        if (! $successFlag) {

            # Error message already displayed
            return undef;
        }

        # Process remaining argument
        $winmapName = shift @args;

        # Check for improper arguments
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # Check the specified winmap exists
        if (! defined $winmapName) {

            return $self->error(
                $session, $inputString,
                'Add a winzone to which winmap?',
            );

        } else {

            $winmapObj = $axmud::CLIENT->ivShow('winmapHash', $winmapName);
            if (! $winmapObj) {

                return $self->error(
                    $session, $inputString,
                    'The winmap \'' . $winmapName . '\' doesn\'t exist',
                );
            }
        }

        # Create the new winzone
        $winzoneObj = Games::Axmud::Obj::Winzone->new($winmapObj);
        if (! $winzoneObj) {

            return $self->error($session, $inputString, 'Couldn\'t create a new winzone');
        }

        # Set the new winzone's IVs
        $winzoneObj->set_vars(
            $standardCmd,
            $xPosBlocks, $yPosBlocks, $widthBlocks, $heightBlocks, $packageName, $objName,
            $positionFlag, $sizeFlag, $packageNameFlag, $objNameFlag,
            $initHashRef, $removeHashRef,
        );

        # Try to add the new winzone at the specified position
        if (! $winmapObj->addWinzone($winzoneObj)) {

            return $self->error(
                $session, $inputString,
                'Can\'t add the winzone to the winmap \'' . $winmapName . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Added the winzone #' . $winzoneObj->number . ' to the winmap \''
                . $winmapName . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditWinzone;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editwinzone', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ewz', 'editwz', 'editwinzone'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a winzone';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $winmapName, $winzoneNumber,
            $check,
        ) = @_;

        # Local variables
        my ($winmapObj, $winzoneObj);

        # Check for improper arguments
        if (! defined $winmapName || ! defined $winzoneNumber || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the winmap exists
        $winmapObj = $axmud::CLIENT->ivShow('winmapHash', $winmapName);
        if (! $winmapObj) {
            return $self->error(
                $session, $inputString,
                'The winmap \'' . $winmapName . '\' doesn\'t exist',
            );
        }

        # Check the winzone exists
        $winzoneObj = $winmapObj->ivShow('zoneHash', $winzoneNumber);
        if (! $winzoneObj) {

            return $self->error(
                $session, $inputString,
                'Winzone #. ' . $winzoneNumber . ' doesn\'t exist in the winmap \''
                . $winmapName . '\'',
            );
        }

        # Open an 'edit' window for the winzone
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Winzone',
                $session->mainWin,
                $session,
                'Edit winzone #' . $winzoneObj->number,
                $winzoneObj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit winmap \'' . $winmapObj->name . '\', winzone #'
                . $winzoneObj->number,
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for winmap \'' . $winmapObj->name . '\', winzone #'
                . $winzoneObj->number,
            );
        }
    }
}

{ package Games::Axmud::Cmd::ModifyWinzone;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('modifywinzone', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mwz', 'modwz', 'modifywinzone'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies an existing winzone';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args
        ) = @_;

        # Local variables
        my (
            $successFlag,
            $xPosBlocks, $yPosBlocks, $widthBlocks, $heightBlocks, $packageName, $objName,
            $positionFlag, $sizeFlag, $packageNameFlag, $objNameFlag,
            $initHashRef, $removeHashRef,
            $winmapName, $winzoneNumber, $winmapObj, $winzoneObj,
        );

        # Extract compulsory and optional switch options
        (
            $successFlag,
            $xPosBlocks, $yPosBlocks, $widthBlocks, $heightBlocks, $packageName, $objName,
            $positionFlag, $sizeFlag, $packageNameFlag, $objNameFlag,
            $initHashRef, $removeHashRef,
            @args,
        ) = $self->extractWinzoneSwitches($session, $inputString, $standardCmd, @args);

        if (! $successFlag) {

            # Error message already displayed
            return undef;
        }

        # Process remaining argument
        $winmapName = shift @args;
        $winzoneNumber = shift @args;

        # Check for improper arguments
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # Check the specified winmap exists
        if (! defined $winmapName) {

            return $self->error(
                $session, $inputString,
                'Modify a winzone in which winmap?',
            );

        } else {

            $winmapObj = $axmud::CLIENT->ivShow('winmapHash', $winmapName);
            if (! $winmapObj) {

                return $self->error(
                    $session, $inputString,
                    'The winmap \'' . $winmapName . '\' doesn\'t exist',
                );
            }
        }

        # Check the winzone exists
        if (! defined $winzoneNumber) {

            return $self->error(
                $session, $inputString,
                'Modify which winzone?',
            );

        } else {

            $winzoneObj = $winmapObj->ivShow('zoneHash', $winzoneNumber);
            if (! $winzoneObj) {

                return $self->error(
                    $session, $inputString,
                    'Winzone #. ' . $winzoneNumber . ' doesn\'t exist in the winmap \''
                    . $winmapName . '\'',
                );
            }
        }

        # Set the new winzone's IVs
        $winzoneObj->set_vars(
            $standardCmd,
            $xPosBlocks, $yPosBlocks, $widthBlocks, $heightBlocks, $packageName, $objName,
            $positionFlag, $sizeFlag, $packageNameFlag, $objNameFlag,
            $initHashRef, $removeHashRef,
        );

        return $self->complete(
            $session, $standardCmd,
            'Modified the winzone #' . $winzoneNumber . ' in the winmap \'' . $winmapName . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::DeleteWinzone;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletewinzone', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dwz', 'delwz', 'deletewz', 'deletewinzone'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a winzone';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $winmapName, $winzoneNumber,
            $check,
        ) = @_;

        # Local variables
        my $winmapObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the winmap exists
        if (! defined $winmapName) {

            return $self->error(
                $session, $inputString,
                'Delete a winzone from which winmap?',
            );

        } else {

            $winmapObj = $axmud::CLIENT->ivShow('winmapHash', $winmapName);
            if (! $winmapObj) {

                return $self->error(
                    $session, $inputString,
                    'The winmap \'' . $winmapName . '\' doesn\'t exist',
                );
            }
        }

        # Check the winzone exists
        if (! defined $winzoneNumber) {

            return $self->error(
                $session, $inputString,
                'Delete which winzone?',
            );

        } elsif (! $winmapObj->ivExists('zoneHash', $winzoneNumber)) {

            return $self->error(
                $session, $inputString,
                'Winzone #. ' . $winzoneNumber . ' doesn\'t exist in the winmap \''
                . $winmapName . '\'',
            );
        }

        # Delete the winzone from the winmap
        if (! $winmapObj->deleteWinzone($winzoneNumber)) {

            return $self->error(
                $session, $inputString,
                'Couldn\'t delete winzone #' . $winzoneNumber . ' from the winmap \''
                . $winmapName . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Deleted winzone #' . $winzoneNumber . ' from the winmap \'' . $winmapName
                . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListWinzone;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listwinzone', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lwz', 'listwz', 'listwinzone'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists winzones';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $winmap,
            $check,
        ) = @_;

        # Local variables
        my $winmapObj;

        # Check for improper arguments
        if (! defined $winmap || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the winmap exists
        $winmapObj = $axmud::CLIENT->ivShow('winmapHash', $winmap);
        if (! $winmapObj) {

            return $self->error(
                $session, $inputString,
                'The winmap \'' . $winmap . '\' doesn\'t exist',
            );

        # Check the winmap contains some winzones
        } elsif (! $winmapObj->zoneHash) {

            return $self->complete(
                $session, $standardCmd,
                'The winmap \'' . $winmap . '\' contains no winzones',
            );
        }

        # Display header
        $session->writeText('List of winzones for winmap \'' . $winmap . '\'');
        $session->writeText(
            '   Num  X  Y  Wid Hei Optional name    Uses table object',
        );

        # Display list
        foreach my $winzoneObj (
            sort {$a->number <=> $b->number} ($winmapObj->ivValues('zoneHash'))
        ) {
            my ($objName, $descrip, $packageString);

            if ($winzoneObj->objName) {
                $objName = $winzoneObj->objName;
            } else {
                $objName = '(not set)';
            }

            $descrip = $axmud::CLIENT->ivShow('customTableHash', $winzoneObj->packageName);
            if (! $descrip) {
                $packageString = 'Not loaded (' . $winzoneObj->packageName . ')';
            } else {
                $packageString = $descrip . ' (' . $winzoneObj->packageName . ')';
            }

            $session->writeText(
                '   ' . sprintf(
                    '%-4.4s %-2.2s %-2.2s %-3.3s %-3.3s %-16.16s ',
                    $winzoneObj->number,
                    $winzoneObj->left, $winzoneObj->top,
                    $winzoneObj->width, $winzoneObj->height,
                    $objName,
                ) . $packageString,
            );

            foreach my $key (sort {lc($a) cmp lc($b)} ($winzoneObj->ivKeys('initHash'))) {

                my $value = $winzoneObj->ivShow('initHash', $key);

                if (defined $value) {
                    $session->writeText('        Init: ' . $key . ' - (no value)');
                } else {
                    $session->writeText('        Init: ' . $key . ' - ' . $value);
                }
            }
        }

        # Display footer
        if ($winmapObj->zoneCount == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 winzone found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . $winmapObj->zoneCount . ') winzones found',
            );
        }
    }
}

# Workspace grids

{ package Games::Axmud::Cmd::ActivateGrid;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('activategrid', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['agr', 'actgrid', 'activategrid'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Activates (turns on) workspace grids';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($switch, $zonemap, $num, $zonemapObj, $workspaceObj);

        # Extract switches
        ($switch, $zonemap, @args) = $self->extract('-z', 1, @args);

        # Get the remaining arguments
        $num = shift @args;

        # There should be no further arguments
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # Are workspace grids available at all?
        if (! $axmud::CLIENT->desktopObj->gridPermitFlag) {

            return $self->error(
                $session, $inputString,
                'Workspace grids are not available at all on this system',
            );
        }

        # If a zonemap was specified, perform some checks
        if ($zonemap) {

            $zonemapObj = $axmud::CLIENT->ivShow('zonemapHash', $zonemap);
            if (! $zonemapObj) {

                return $self->error(
                    $session, $inputString,
                    'The zonemap \'' . $zonemap . '\' doesn\'t exist',
                );

            } elsif ($zonemapObj->tempFlag) {

                return $self->error(
                    $session, $inputString,
                    'Workspace grids can\'t be activated with a temporary zonemap',
                );

            } elsif (! $zonemapObj->modelHash) {

                return $self->error(
                    $session, $inputString,
                    'Workspace grids can\'t be activated with the zonemap \'' . $zonemap
                    . '\' because it contains no zone models',
                );
            }
        }

        # ;awg
        # ;awg -z <zonemap>
        if (! defined $num) {

            # Are workspace grids already activated in general?
            if ($axmud::CLIENT->activateGridFlag) {

                return $self->error(
                    $session, $inputString,
                    'Workspace grids are already activated in general',
                );
            }

            # Activate workspace grids in general
            if (! $axmud::CLIENT->desktopObj->activateWorkspaceGrids($zonemap)) {

                return $self->error(
                    $session, $inputString,
                    'General error activating workspace grids',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Workspace grids have been activated generally',
                );
            }

        # ;awg <num>
        # ;awg <num> -z <zonemap>
        # ;awg -z <zonemap> <num>
        } else {

            # Are workspace grids disactivated in general?
            if (! $axmud::CLIENT->activateGridFlag) {

                return $self->error(
                    $session, $inputString,
                    'Workspace grids are disactivated in general',
                );
            }

            # Are workspaces enabled on the specified workspace?
            $workspaceObj = $axmud::CLIENT->desktopObj->ivShow('workspaceHash', $num);
            if (! $workspaceObj) {

                return $self->error(
                    $session, $inputString,
                    'Workspace #' . $num . ' doesn\'t exist',
                );

            } elsif ($workspaceObj->gridEnableFlag) {

                return $self->error(
                    $session, $inputString,
                    'Workspace grids are already activated on workspace #' . $num,
                );
            }

            # Enable workspace grids on the workspace
            if (! $workspaceObj->enableWorkspaceGrids($zonemap)) {

                return $self->error(
                    $session, $inputString,
                    'General error activating workspace grids on workspace #' . $num,
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Workspace grids activated on workspace #' . $num,
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::DisactivateGrid;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('disactivategrid', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dgr', 'disactgrid', 'disactivategrid'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Disactivates (turns off) workspace grids';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $num,
            $check,
        ) = @_;

        # Local variables
        my $workspaceObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Are workspace grids available at all?
        if (! $axmud::CLIENT->desktopObj->gridPermitFlag) {

            return $self->error(
                $session, $inputString,
                'Workspace grids are not available at all on this system',
            );
        }

        # ;dwg
        if (! defined $num) {

            # Are workspace grids already disactivated in general?
            if (! $axmud::CLIENT->activateGridFlag) {

                return $self->error(
                    $session, $inputString,
                    'Workspace grids are already disactivated in general',
                );
            }

            # Disactivate workspace grids in general
            if (! $axmud::CLIENT->desktopObj->disactivateWorkspaceGrids()) {

                return $self->error(
                    $session, $inputString,
                    'General error disactivating workspace grids',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Workspace grids have been disactivated generally',
                );
            }

        # ;dwg <num>
        } else {

            # Are workspace grids disactivated in general?
            if (! $axmud::CLIENT->activateGridFlag) {

                return $self->error(
                    $session, $inputString,
                    'Workspace grids are disactivated in general',
                );
            }

            # Are workspaces disabled on the specified workspace?
            $workspaceObj = $axmud::CLIENT->desktopObj->ivShow('workspaceHash', $num);
            if (! $workspaceObj) {

                return $self->error(
                    $session, $inputString,
                    'Workspace #' . $num . ' doesn\'t exist',
                );

            } elsif (! $workspaceObj->gridEnableFlag) {

                return $self->error(
                    $session, $inputString,
                    'Workspace grids are already disactivated on workspace #' . $num,
                );
            }

            # Disable workspace grids on the workspace
            if (! $workspaceObj->disableWorkspaceGrids()) {

                return $self->error(
                    $session, $inputString,
                    'General error disactivating workspace grids on workspace #' . $num,
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Workspace grids disactivated on workspace #' . $num,
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::SetGrid;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setgrid', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sgr', 'setgrid'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets various workspace grid display settings';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, @args
        ) = @_;

        # Local variables
        my ($string, $resetMsg, $count, $errorCount);

        # Check for improper arguments (every possible switch has its own check, too)
        if (
            defined $switch && $switch ne '-b' && $switch ne '-g' && $switch ne '-a'
            && $switch ne '-e' && $switch ne '-r' && $switch ne '-i'
        ) {
            return $self->improper($session, $inputString);
        }

        # ;sgr
        if (! defined $switch) {

            # Display header
            $session->writeText('List of workspace grid settings');
            $session->writeText(
                '   Gridblock size: ' . $axmud::CLIENT->gridBlockSize . ', grid maximum gap size: '
                . $axmud::CLIENT->gridGapMaxSize,
            );

            # Display list
            $string = '   Grid adjustment flag: ';
            if ($axmud::CLIENT->gridAdjustmentFlag) {
                $string .= 'on';
            } else {
                $string .= 'off';
            }

            $string .= ', edge correction flag: ';
            if ($axmud::CLIENT->gridEdgeCorrectionFlag) {
                $string .= 'on';
            } else {
                $string .= 'off';
            }

            $session->writeText($string);

            $string = '   Window reshuffle flag: ';
            if ($axmud::CLIENT->gridReshuffleFlag) {
                $string .= 'on';
            } else {
                $string .= 'off';
            }

            $string .= ', invisible window flag: ';
            if ($axmud::CLIENT->gridInvisWinFlag) {
                $string .= 'on';
            } else {
                $string .= 'off';
            }

            $session->writeText($string);

            # Display footer
            return $self->complete($session, $standardCmd, 'End of list');

        # ;sgr -b
        # ;sgr -b <size>
        } elsif ($switch eq '-b') {

            my $size = shift @args;

            # Check for improper arguments
            if (@args) {

                return $self->improper($session, $inputString);
            }

            if (! defined $size) {

                $size = $axmud::CLIENT->constGridBlockSize;

            } elsif (! $axmud::CLIENT->intCheck($size, 1, 100)) {

                return $self->error(
                    $session, $inputString,
                    'Workspace gridblock size must be in the range 1-100',
                );
            }

            $axmud::CLIENT->set_gridBlockSize($size);
            $resetMsg = 'Workspace gridblock size set to ' . $size . ' pixels';

        # ;sgr -g
        # ;sgr -g <size>
        } elsif ($switch eq '-g') {

            my $size = shift @args;

            # Check for improper arguments
            if (@args) {

                return $self->improper($session, $inputString);
            }

            if (! defined $size) {

                $size = $axmud::CLIENT->constGridGapMaxSize;

            } elsif (! $axmud::CLIENT->intCheck($size, 0, 100)) {

                return $self->error(
                    $session, $inputString,
                    'Workspace grid maximum gap size must be in the range 0-100',
                );
            }

            $axmud::CLIENT->set_gridGapMaxSize($size);
            $resetMsg = 'Workspace grid maximum gap size set to ' . $size . ' gridblocks';

        # ;sgr -a <flag>
        # ;sgr -e <flag>
        # ;sgr -r <flag>
        # ;sgr -i <flag>
        } elsif ($switch eq '-a' || $switch eq '-e' || $switch eq '-r' || $switch eq '-i') {

            my ($flag, $testFlag, $string);

            $flag = shift @args;

            # Check for improper arguments
            if (! defined $flag || @args) {

                return $self->improper($session, $inputString);
            }

            # Convert <flag> to 1 or 0
            $testFlag = $self->convertTrueFalse($flag);
            if (! defined $testFlag) {

                return $self->error(
                    $session, $inputString,
                    '<flag> must be 1, 0, or any word beginning with \'t-\' (for true) or'
                    . ' \'f-\' (for false)',
                );

            } elsif ($testFlag) {

                $string = 'TRUE';

            } else {

                $string = 'FALSE';
            }

            # ;sgr -a <flag>
            if ($switch eq '-a') {

                if ($axmud::CLIENT->gridAdjustmentFlag eq $testFlag) {

                    return $self->complete(
                        $session, $standardCmd,
                        'Workspace grid adjustment flag was already set to \'' . $string . '\'',
                    );
                } else {

                    $axmud::CLIENT->set_gridAdjustmentFlag($testFlag);
                    $resetMsg = 'Workspace grid adjustment flag set to \'' . $string . '\'',
                }

            # ;sgr -e <flag>
            } elsif ($switch eq '-e') {

                 if ($axmud::CLIENT->gridEdgeCorrectionFlag eq $testFlag) {

                    return $self->complete(
                        $session, $standardCmd,
                        'Workspace grid edge correction flag was already set to \'' . $string
                        . '\'',
                    );
                } else {

                    $axmud::CLIENT->set_gridEdgeCorrectionFlag($testFlag);
                    $resetMsg = 'Workspace grid edge correction flag set to \'' . $string . '\'',
                }

            # ;sgr -r <flag>
            } elsif ($switch eq '-r') {

                 if ($axmud::CLIENT->gridReshuffleFlag eq $testFlag) {

                    return $self->complete(
                        $session, $standardCmd,
                        'Workspace grid window reshuffle flag was already set to \'' . $string
                        . '\'',
                    );
                } else {

                    $axmud::CLIENT->set_gridReshuffleFlag($testFlag);
                    $resetMsg = 'Workspace grid window reshuffle flag set to \'' . $string . '\'',
                }

            # ;sgr -i <flag>
            } else {

                 if ($axmud::CLIENT->gridInvisWinFlag eq $testFlag) {

                    return $self->complete(
                        $session, $standardCmd,
                        'Workspace grid invisible window flag was already set to \'' . $string
                        . '\'',
                    );
                } else {

                    $axmud::CLIENT->set_gridInvisWinFlag($testFlag);
                    $resetMsg = 'Workspace grid invisible window flag set to \'' . $string . '\'',
                }
            }
        }

        # If this function hasn't yet used $self->error or $self->complete, then all workspace grids
        #   must be reset
        foreach my $workspaceObj (
            sort {$a->number <=> $b->number} ($axmud::CLIENT->desktopObj->ivValues('workspaceHash'))
        ) {
            foreach my $gridObj (
                sort {$a->number <=> $b->number} ($workspaceObj->ivValues('gridHash'))
            ) {
                $gridObj->applyZonemap();
            }
        }

        return $self->complete($session, $standardCmd, $resetMsg);
     }
}

{ package Games::Axmud::Cmd::ResetGrid;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resetgrid', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rgr', 'resetgrid'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets workspace grids';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $sessionFlag, $workspaceFlag, $num, $zonemap, $zonemapObj, $gridObj,
            $workspaceObj, $count, $errorCount, $haltNumber,
            @list,
        );

        # Extract switches
        ($switch, @args) = $self->extract('-s', 0, @args);
        if (defined $switch) {

            $sessionFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-w', 0, @args);
        if (defined $switch) {

            $workspaceFlag = TRUE;
        }

        # Get the remaining arguments
        if ($sessionFlag) {

            $zonemap = shift @args;

        } else {

            $num = shift @args;
            $zonemap = shift @args;
        }

        # There should be no further arguments
        if (
            (! $sessionFlag && ! $workspaceFlag && ! defined $num)
            || ($workspaceFlag && ! defined $num)
            || @args
        ) {
            return $self->improper($session, $inputString);
        }

        # If a zonemap was specified, check it exists, is not temporary and contains some zone
        #   models
        if ($zonemap) {

            $zonemapObj = $axmud::CLIENT->ivShow('zonemapHash', $zonemap);
            if (! $zonemapObj) {

                return $self->error(
                    $session, $inputString,
                    'The zonemap \'' . $zonemap . '\' doesn\'t exist',
                );

            } elsif ($zonemapObj->tempFlag) {

                return $self->error(
                    $session, $inputString,
                    'Can\'t reset workspace grid(s) using a temporary zonemap',
                );

            } elsif (! $zonemapObj->modelHash) {

                return $self->error(
                    $session, $inputString,
                    'Can\'t reset the workspace grid(s) with the zonemap \'' . $zonemap
                    . '\' because it contains no zone models',
                );
            }
        }

        # ;rgr <num>
        # ;rgr <num> <zonemap>
        if (! $sessionFlag && ! $workspaceFlag) {

            # Check the specified workspace grid objects exists
            $gridObj = $axmud::CLIENT->desktopObj->ivShow('gridHash', $num);
            if (! $gridObj) {

                return $self->error(
                    $session, $inputString,
                    'Workspace grid #' . $num . ' doesn\'t exist',
                );
            }

            # Only one workspace grid to reset
            push (@list, $gridObj);

        # ;rgr -s
        # ;rgr -s <zonemap>
        } elsif ($sessionFlag) {

            # All workspace grid objects controlled by this session should be reset
            foreach my $thisGridObj (
                sort {$a->number <=> $b->number} ($axmud::CLIENT->desktopObj->ivValues('gridHash'))
            ) {
                if ($thisGridObj->owner && $thisGridObj->owner eq $session) {

                    push (@list, $thisGridObj);
                }
            }

        # ;rgr -w <num>
        # ;rgr -w <num> <zonemap>
        } elsif ($workspaceFlag) {

            # Check the specified workspace object exists
            $workspaceObj = $axmud::CLIENT->desktopObj->ivShow('workspaceHash', $num);
            if (! $workspaceObj) {

                return $self->error(
                    $session, $inputString,
                    'Workspace #' . $num . ' doesn\'t exist',
                );
            }

            # Set the workspace's new default zonemap
            if ($zonemapObj) {

                $workspaceObj->set_defaultZonemap($zonemapObj);
            }

            # All workspace grid objects on this workspace should be reset
            push (@list, sort {$a->number <=> $b->number} ($workspaceObj->ivValues('gridHash')));
        }

        if (! @list) {

            return $self->error(
                $session, $inputString,
                'No matching workspace grid(s) found',
            );
        }

        # If a zonemap was specified, apply it to all affected workspace grid objects (otherwise use
        #   the existing zonemap)
        $count = 0;
        $errorCount = 0;
        OUTER: foreach my $thisGridObj (@list) {

            my $thisWorkspaceObj = $thisGridObj->workspaceObj;

            if (! $thisGridObj->applyZonemap($zonemapObj)) {

                $errorCount++;
                # There are two types of error in the call to ->applyZonemap, one of which disables
                #   workspace grids on the workspace. Must halt if that happens
                if (! $thisWorkspaceObj->gridEnableFlag) {

                    $haltNumber = $thisWorkspaceObj->number;
                    last OUTER;
                }

            } else {

                $count++;
            }
        }

        if (defined $haltNumber) {

            return $self->error(
                $session, $inputString,
                'Could not reset specified workspace grids; in addition, grids have been'
                . ' disabled in workspace #' . $haltNumber,
            );

        } elsif (! $count) {

            return $self->error(
                $session, $inputString,
                'Could not reset workspace grids (errors: ' . $errorCount . ')',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Workspace grids reset: ' . $count . ', errors: ' . $errorCount,
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditGrid;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editgrid', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['egr', 'editgrid'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a workspace grid';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $number || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the workspace grid object exists
        $obj = $axmud::CLIENT->desktopObj->ivShow('gridHash', $number);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Could not edit workspace grid #' . $number . ' - object does not exist',
            );
        }

        # Open an 'edit' window for the workspace grid
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::WorkspaceGrid',
                $session->mainWin,
                $session,
                'Edit workspace grid #' . $number,
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit workspace grid #' . $number,
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for workspace grid #' . $number,
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListGrid;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listgrid', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lgr', 'listgrid'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists workspace grids';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $num,
            $check,
        ) = @_;

        # Local variables
        my (
            $workspaceObj, $thisSession, $string,
            @list,
        );

        # Check for improper arguments
        if (
            (defined $switch && $switch ne '-s' && $switch ne '-w')
            || defined $check
        ) {
            return $self->improper($session, $inputString);
        }

        # Compile a list of workspace grids
        # ;lwg
        if (! defined $switch) {

            push (@list,
                sort {$a->number <=> $b->number} ($axmud::CLIENT->desktopObj->ivValues('gridHash'))
            );

            # Don't modify confirmation message
            $string = '';

        # ;lwg -s
        # ;lwg -s <num>
        } elsif ($switch eq '-s') {

            if (! defined $num) {

                $num = $session->number;
            }

            $thisSession = $axmud::CLIENT->ivShow('sessionHash', $num);
            if (! $thisSession) {

                return $self->error(
                    $session, $inputString,
                    'Session #' . $num . ' doesn\'t exist',
                );

            } else {

                foreach my $thisWorkspaceObj (
                    sort {$a->number <=> $b->number}
                    ($axmud::CLIENT->desktopObj->ivValues('workspaceHash'))
                ) {
                    my $thisGridObj = $thisWorkspaceObj->findWorkspaceGrid($thisSession);
                    if ($thisGridObj) {

                        push (@list, $thisGridObj);
                    }
                }
            }

            # Modify confirmation message
            $string = 'matching';

        # ;lwg -w <num>
        } elsif ($switch eq '-w') {

            if (! defined $num) {

                $num = 0;
            }

            $workspaceObj = $axmud::CLIENT->desktopObj->ivShow('workspaceHash', $num);
            if (! $workspaceObj) {

                return $self->error(
                    $session, $inputString,
                    'Workspace #' . $num . ' doesn\'t exist',
                );

            } else {

                push (@list,
                    sort {$a->number <=> $b->number} ($workspaceObj->ivValues('gridHash')),
                );
            }

            # Modify confirmation message
            $string = 'matching';
        }

        if (! @list) {

            return $self->error(
                $session, $inputString,
                'No ' . $string . ' workspace grids found',
            );
        }

        # Display header
        if (! defined $switch) {
            $session->writeText('List of all workspace grids');
        } elsif ($switch eq '-s') {
            $session->writeText('List of workspace grids for session #' . $session->number);
        } elsif ($switch eq '-w') {
            $session->writeText('List of workspace grids for workspace #' . $num);
        }

        # Display list
        foreach my $gridObj (@list) {

            my $string;

            $string = '   Workspace grid #' . $gridObj->number . ' (workspace #'
                        . $gridObj->workspaceObj->number . ', ';

            if ($gridObj->owner) {
                $string .= 'session #' . $gridObj->owner->number . ')';
            } else {
                $string .= 'shared between sessions)';
            }

            $session->writeText($string);

            $session->writeText('      Zonemap: ' . $gridObj->zonemap);

            $string = '      Layers: Max ' . $gridObj->maxLayers . ' default '
                        . $gridObj->defaultLayer . ' current ';

            if (defined $gridObj->currentLayer) {

                $string .= $gridObj->currentLayer;
            } else {
                $string .= '(not set)';
            }

            $session->writeText($string);

            $session->writeText(
                '      Size in blocks: width ' . $gridObj->widthBlocks . ' height '
                . $gridObj->heightBlocks,
            );

            $session->writeText(
                '      Position on workspace in pixels: x ' . $gridObj->xPosPixels . ' y '
                . $gridObj->yPosPixels,
            );

            $session->writeText(
                '      Zones (windows on this grid: ' . $gridObj->ivPairs('gridWinHash')
                . ')',
            );

            $session->writeText('         Num  XPos YPos Wid  Hei  Reserved Num. wins');

            foreach my $zoneObj (
                sort {$a->number <=> $b->number} ($gridObj->ivValues('zoneHash'))
            ) {
                my ($string);

                if ($zoneObj->reservedFlag) {
                    $string = 'yes';
                } else {
                    $string = 'no';
                }

                $session->writeText(
                    '         ' . sprintf(
                        '%-4.4s %-4.4s %-4.4s %-4.4s %-4.4s %-8.8s ',
                        $zoneObj->number,
                        $zoneObj->xPosBlocks,
                        $zoneObj->yPosBlocks,
                        $zoneObj->widthBlocks,
                        $zoneObj->heightBlocks,
                        $string,
                    ) . $zoneObj->ivPairs('areaHash'),
                );
            }
        }

        # Display footer
        if (scalar (@list) == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 ' . $string . ' workspace grid found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar (@list) . ' ' . $string . ' workspace grids found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetLayer;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setlayer', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['layer', 'setlayer'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the session\'s workspace grid layer';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my (
            $count, $errorCount,
            @gridList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get a list of workspace grids for this session
        foreach my $workspaceObj (
            sort {$a->number <=> $b->number} ($axmud::CLIENT->desktopObj->ivValues('workspaceHash'))
        ) {
            my $gridObj = $workspaceObj->findWorkspaceGrid($session);
            if ($gridObj) {

                push (@gridList, $gridObj);
            }
        }

        if (! @gridList) {

            return $self->error(
                $session, $inputString,
                'No workspace grids found for this session',
            );
        }

        # ;layer
        if (! defined $number) {

            # Display header
            $session->writeText('Workspace grid layers for this session');
            $session->writeText('   Grid Current  Default  Max');
            foreach my $gridObj (@gridList) {

                $session->writeText(
                    sprintf(
                        '   %-4.4s %-8.8s %-8.8s %-8.8s',
                        $gridObj->number,
                        $gridObj->currentLayer,
                        $gridObj->defaultLayer,
                        $gridObj->maxLayers,
                    ),
                );
            }

            if (scalar (@gridList) == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (1 workspace grid found)',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (' . (scalar @gridList) . ' workspace grids found)',
                );
            }

        # ;layer <number>
        } else {

            # Check <number> is a valid integer, at least
            if (! $axmud::CLIENT->intCheck($number, 0)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid layer number \'' . $number . '\'',
                );
            }

            # Set the grid layer and restack windows in all zones
            $count = 0;
            $errorCount = 0;

            foreach my $gridObj (@gridList) {

                if ($number >= $gridObj->maxLayers) {

                    $errorCount++;

                } else {

                    $count++;
                    $gridObj->set_currentLayer($number);

                    foreach my $zoneObj ($gridObj->ivValues('zoneHash')) {

                        $zoneObj->restackWin();
                    }
                }
            }

            # Restore focus to the session's 'main' window
            $session->mainWin->restoreFocus();

            if (! $count) {

                return $self->error(
                    $session, $inputString,
                    'Unable to set the workspace grid layer to \'' . $number . '\' in any'
                    . ' workspace grid',
                );

            } elsif ($count == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Set the layer to \'' . $number . '\' in 1 workspace grid (errors: '
                    . $errorCount . ')',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Set the layer to \'' . $number . '\' in ' . $count . ' workspace grids'
                    . ' (errors: ' . $errorCount . ')',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::LayerUp;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('layerup', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lup', 'layerup'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Moves up the session\'s workspace grid layer';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $count, $errorCount, $mainLayer,
            @gridList,
        );

        # Check for improper arguments
        if ((defined $switch && $switch ne '-l') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get a list of workspace grids for this session
        foreach my $workspaceObj (
            sort {$a->number <=> $b->number} ($axmud::CLIENT->desktopObj->ivValues('workspaceHash'))
        ) {
            my $gridObj = $workspaceObj->findWorkspaceGrid($session);
            if ($gridObj) {

                push (@gridList, $gridObj);
            }
        }

        if (! @gridList) {

            return $self->error(
                $session, $inputString,
                'No workspace grids found for this session',
            );
        }

        # Set the grid layer and restack windows in all zones
        $count = 0;
        $errorCount = 0;

        foreach my $gridObj (@gridList) {

            # Check this grid isn't already at its highest layer
            if ($gridObj->currentLayer >= ($gridObj->maxLayers - 1)) {

                $errorCount++;

            } else {

                $count++;
                $gridObj->inc_currentLayer();

                foreach my $zoneObj ($gridObj->ivValues('zoneHash')) {

                    $zoneObj->restackWin();
                }

                if ($session->mainWin->workspaceGridObj eq $gridObj) {

                    $mainLayer = $gridObj->currentLayer;
                }
            }
        }

        # Restore focus to the session's 'main' window
        $session->mainWin->restoreFocus();

        if (! $count) {

            if ($switch) {

                return $self->error(
                    $session, $inputString,
                    'Unable to set the workspace grid layer in any workspace grid',
                );

            } else {

                return $self->complete($session, $standardCmd, 'Window layer is unchanged');
            }

        } elsif (! defined $mainLayer) {

            # No reason why this should happen, but we'll check anyway
            return $self->complete(
                $session, $standardCmd,
                'Grid layers changed: ' . $count . ', not changed: ' . $errorCount . ')',
            );

        } else {

            if ($switch) {

                return $self->complete(
                    $session, $standardCmd,
                    'Set the layer to ' . $mainLayer . ' in the \'main\' window\'s workspace grid'
                    . ' (grid layers changed: ' . $count . ', not changed: ' . $errorCount . ')',
                );

            } else {

                return $self->complete($session, $standardCmd, 'Set window layer to ' . $mainLayer);
            }
        }
    }
}

{ package Games::Axmud::Cmd::LayerDown;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('layerdown', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ldn', 'ldown', 'layerdown'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Moves down the session\'s workspace grid layer';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $count, $errorCount, $mainLayer,
            @gridList,
        );

        # Check for improper arguments
        if ((defined $switch && $switch ne '-l') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get a list of workspace grids for this session
        foreach my $workspaceObj (
            sort {$a->number <=> $b->number} ($axmud::CLIENT->desktopObj->ivValues('workspaceHash'))
        ) {
            my $gridObj = $workspaceObj->findWorkspaceGrid($session);
            if ($gridObj) {

                push (@gridList, $gridObj);
            }
        }

        if (! @gridList) {

            return $self->error(
                $session, $inputString,
                'No workspace grids found for this session',
            );
        }

        # Set the grid layer and restack windows in all zones
        $count = 0;
        $errorCount = 0;

        foreach my $gridObj (@gridList) {

            # Check this grid isn't already at its highest layer
            if ($gridObj->currentLayer == 0) {

                $errorCount++;

            } else {

                $count++;
                $gridObj->dec_currentLayer();

                foreach my $zoneObj ($gridObj->ivValues('zoneHash')) {

                    $zoneObj->restackWin();
                }

                if ($session->mainWin->workspaceGridObj eq $gridObj) {

                    $mainLayer = $gridObj->currentLayer;
                }
            }
        }

        # Restore focus to the session's 'main' window
        $session->mainWin->restoreFocus();

        if (! $count) {

            if ($switch) {

                return $self->error(
                    $session, $inputString,
                    'Unable to set the workspace grid layer in any workspace grid',
                );

            } else {

                return $self->complete($session, $standardCmd, 'Window layer is unchanged');
            }

        } elsif (! defined $mainLayer) {

            # No reason why this should happen, but we'll check anyway
            return $self->complete(
                $session, $standardCmd,
                'Grid layers changed: ' . $count . ', not changed: ' . $errorCount . ')',
            );

        } elsif ($switch) {

            return $self->complete(
                $session, $standardCmd,
                'Set the layer to ' . $mainLayer . ' in the \'main\' windows workspace grid'
                . ' (grid layers changed: ' . $count . ', not changed: ' . $errorCount . ')',
            );

        } else {

            return $self->complete($session, $standardCmd, 'Set window layer to ' . $mainLayer);
        }
    }
}

{ package Games::Axmud::Cmd::ToggleWindowStorage;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('togglewindowstorage', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tws', 'togglewinstore', 'togglewindowstorage'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles storage of \'grid\' window sizes/positions';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Toggle the flag
        if (! $axmud::CLIENT->storeGridPosnFlag) {

            $axmud::CLIENT->set_storeGridPosnFlag(TRUE);

            # Store the size/position of all 'grid' windows for this session
            $axmud::CLIENT->desktopObj->storeGridWinPosn($session);

            return $self->complete(
                $session, $standardCmd,
                'Storage of \'grid\' window sizes/positions has been turned ON',
            );

        } else {

            $axmud::CLIENT->set_storeGridPosnFlag(FALSE);

            return $self->complete(
                $session, $standardCmd,
                'Storage of \'grid\' window sizes/positions has been turned OFF',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ApplyWindowStorage;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('applywindowstorage', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['aws', 'applywinstore', 'applywindowstorage'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Stores size/position of all \'grid\' windows';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Store the size/position of all 'grid' windows for this session
        $axmud::CLIENT->desktopObj->storeGridWinPosn($session);

        return $self->complete(
            $session, $standardCmd,
            'Sizes/positions of this session\'s \'grid\' windows have been stored',
        );
    }
}

{ package Games::Axmud::Cmd::ClearWindowStorage;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('clearwindowstorage', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cws', 'clearwinstore', 'clearwindowstorage'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Clears stored \'grid\' window sizes/positions';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $winName,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;cws
        if (! defined $winName) {

            if (! $axmud::CLIENT->storeGridPosnHash) {

                return $self->complete(
                    $session, $standardCmd,
                    'There are no stored \'grid\' window sizes/positions',
                );

            } else {

                $axmud::CLIENT->reset_storeGridPosn();

                return $self->complete(
                    $session, $standardCmd,
                    'All stored \'grid\' window sizes/positions have been cleared',
                );
            }

        # ;cw <name>
        } else {

            if (! $axmud::CLIENT->ivExists('storeGridPosnHash', $winName)) {

                return $self->error(
                    $session, $inputString,
                    'There is no stored size/position for the \'grid\' window \'' . $winName . '\'',
                );

            } else {

                $axmud::CLIENT->del_storeGridPosn($winName);

                return $self->complete(
                    $session, $standardCmd,
                    'The stored size/position for the \'' . $winName . '\' window has been cleared',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::DumpWindowStorage;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('dumpwindowstorage', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dws', 'dumpwinstore', 'dumpwindowstorage'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Displays stored \'grid\' window sizes/positions';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my %hash;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        %hash = $axmud::CLIENT->storeGridPosnHash;
        if (! %hash) {

            return $self->complete(
                $session, $standardCmd,
                'There are no stored \'grid\' window sizes/positions',
            );
        }

        # Display header
        if ($axmud::CLIENT->storeGridPosnFlag) {

            $session->writeText(
                'List of stored \'grid\' window sizes/positions (storage turned ON)',
            );

        } else {

            $session->writeText(
                'List of stored \'grid\' window sizes/positions (storage turned OFF)',
            );
        }

        $session->writeText('   Window name      X     Y     Width Height');

        # Display list
        foreach my $winName (sort {lc($a) cmp lc($b)} (keys %hash)) {

            my ($listRef, $xPos, $yPos, $width, $height);

            $listRef = $hash{$winName};

            if (defined $$listRef[0]) {
                $xPos = $$listRef[0];
            } else {
                $xPos = '-';
            }

            if (defined $$listRef[1]) {
                $yPos = $$listRef[1];
            } else {
                $yPos = '-';
            }

            if (defined $$listRef[2]) {
                $width = $$listRef[2];
            } else {
                $width = '-';
            }

            if (defined $$listRef[3]) {
                $height = $$listRef[3];
            } else {
                $height = '-';
            }

            $session->writeText(
                sprintf(
                    '   %-16.16s %-5.5s %-5.5s %-5.5s %-5.5s',
                    $winName,
                    $xPos,
                    $yPos,
                    $width,
                    $height,
                ),
            );
        }

        # Display footer
        if (scalar (keys %hash) == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 stored window found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . (scalar (keys %hash)) . ') stored windows found',
            );
        }
    }
}

# Zonemaps and zone models

{ package Games::Axmud::Cmd::AddZonemap;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addzonemap', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['azm', 'addzm', 'addzonemap'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a new zonemap';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the zonemap name doesn't already exist
        if ($axmud::CLIENT->ivExists('zonemapHash', $name)) {

            return $self->error(
                $session, $inputString,
                'The zonemap \'' . $name . '\' already exists',
            );

        # Check the name is valid
        } elsif (! $axmud::CLIENT->nameCheck($name, 16)) {

            return $self->error($session, $inputString, 'Illegal name \'' . $name . '\'');
        }

        # Add the zonemap
        $obj = Games::Axmud::Obj::Zonemap->new($name);
        if (!$obj) {

            return $self->error(
                $session, $inputString,
                'Could not add the zonemap \'' . $name . '\'',
            );

        } else {

            $axmud::CLIENT->add_zonemap($obj);

            return $self->complete($session, $standardCmd, 'Zonemap \'' . $name . '\' created');
        }
    }
}

{ package Games::Axmud::Cmd::CloneZonemap;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('clonezonemap', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['czm', 'clonezm', 'clonezonemap'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Clones a zonemap';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $oldName, $newName,
            $check,
        ) = @_;

        # Local variables
        my ($oldObj, $newObj);

        # Check for improper arguments
        if (! defined $oldName || ! defined $newName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the (old) zonemap exists
        $oldObj = $axmud::CLIENT->ivShow('zonemapHash', $oldName);
        if (! $oldObj) {

            return $self->error(
                $session, $inputString,
                'The zonemap \'' . $oldName . '\' doesn\'t exist',
            );
        }

        # Check that the new name doesn't exist and is valid
        if ($axmud::CLIENT->ivExists('zonemapHash', $newName)) {

            return $self->error(
                $session, $inputString,
                'The zonemap \'' . $newName . '\' already exists',
            );

        } elsif (! $axmud::CLIENT->nameCheck($newName, 16)) {

            return $self->error($session, $inputString, 'Illegal name \'' . $newName . '\'');
        }

        # Temporary zonemaps can't be cloned
        if ($oldObj->tempFlag) {

            return $self->error(
                $session, $inputString,
                'The zonemap \'' . $oldName . '\' is a temporary zonemap which cannot be cloned',
            );
        }

        # Clone the zonemap
        $newObj = $oldObj->clone($newName);
        if (! $newObj) {

            return $self->error(
                $session, $inputString,
                'Could not clone the zonemap \'' . $oldName . '\'',
            );

        } else {

            $axmud::CLIENT->add_zonemap($newObj);

            return $self->complete(
                $session, $standardCmd,
                'Zonemap \'' . $oldName . '\' cloned as the new zonemap \'' . $newName . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditZonemap;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editzonemap', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ezm', 'editzm', 'editzonemap'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a zonemap';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the zonemap exists
        $obj = $axmud::CLIENT->ivShow('zonemapHash', $name);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Could not edit the \'' . $name . '\' zonemap - object does not exist',
            );
        }

        # Open an 'edit' window for the zonemap
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Zonemap',
                $session->mainWin,
                $session,
                'Edit zonemap \'' . $obj->name . '\'',
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the \'' . $name . '\' zonemap',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the \'' . $name . '\' zonemap',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteZonemap;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletezonemap', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dzm', 'delzm', 'deletezm', 'deletezonemap'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a zonemap';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the zonemap exists
        if (! $axmud::CLIENT->ivExists('zonemapHash', $name)) {

            return $self->error(
                $session, $inputString,
                'The zonemap \'' . $name . '\' doesn\'t exist',
            );

        # Check it's not a standard zonemap
        } elsif ($axmud::CLIENT->ivExists('standardZonemapHash', $name)) {

            return $self->error(
                $session, $inputString,
                'Standard zonemaps like \'' . $name . '\' can\'t be deleted',
            );
        }

        # Check the zonemap isn't in use by any workspace grid
        foreach my $gridObj (
            sort {$a->number <=> $b->number} ($axmud::CLIENT->desktopObj->ivValues('gridHash'))
        ) {
            if ($gridObj->zonemap && $gridObj->zonemap eq $name) {

                return $self->error(
                    $session, $inputString,
                    'The zonemap \'' . $name . '\' is currently in use by workspace grid #'
                    . $gridObj->number . ' (try using \';resetgrid\' first)',
                );
            }
        }

        # Delete the zonemap
        $axmud::CLIENT->del_zonemap($name);

        # Any workspaces using the zonemap as their default zonemap (unlikely after the check above,
        #   but possible) should be given a new default zonemap
        foreach my $workspaceObj ($axmud::CLIENT->desktopObj->ivValues('workspaceHash')) {

            if ($workspaceObj->defaultZonemap && $workspaceObj->defaultZonemap eq $name) {

                if ($axmud::CLIENT->shareMainWinFlag) {
                    $workspaceObj->set_defaultZonemap('basic');
                } else {
                    $workspaceObj->set_defaultZonemap('single');
                }
            }
        }

        # Operation complete
        return $self->complete($session, $standardCmd, 'Zonemap \'' . $name . '\' deleted');
    }
}

{ package Games::Axmud::Cmd::ResetZonemap;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resetzonemap', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rzm', 'resetzm', 'resetzonemap'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets a zonemap';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $zonemapObj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the zonemap exists
        $zonemapObj = $axmud::CLIENT->ivShow('zonemapHash', $name);
        if (! $zonemapObj) {

            return $self->error(
                $session, $inputString,
                'The zonemap \'' . $name . '\' doesn\'t exist',
            );

        # Check it's not a standard zonemap
        } elsif ($axmud::CLIENT->ivExists('standardZonemapHash', $name)) {

            return $self->error(
                $session, $inputString,
                'Standard zonemaps like \'' . $name . '\' can\'t be reset',
            );
        }

        # Check the zonemap isn't in use by any workspace grid
        foreach my $gridObj (
            sort {$a->number <=> $b->number} ($axmud::CLIENT->desktopObj->ivValues('gridHash'))
        ) {
            if ($gridObj->zonemap && $gridObj->zonemap eq $name) {

                return $self->error(
                    $session, $inputString,
                    'The zonemap \'' . $name . '\' is currently in use by workspace grid #'
                    . $gridObj->number . ' (try using \';resetgrid\' first)',
                );
            }
        }

        # Reset the zonemap
        if (! $zonemapObj->resetZonemap()) {

            return $self->error(
                $session, $inputString,
                'The zonemap \'' . $name . '\' couldn\'t be reset',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'The zonemap \'' . $name . '\' has been reset',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListZonemap;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listzonemap', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lzm', 'listzm', 'listzonemap'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists all zonemaps';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $count,
            @list,
            %defaultHash, %inUseHash,
        );

        # Check for improper arguments
        if ((defined $switch && $switch ne '-s') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import a list of zonemap names, sorted alphabetically
        @list = sort {lc($a) cmp lc($b)} ($axmud::CLIENT->ivKeys('zonemapHash'));
        if (! @list) {

            return $self->complete($session, $standardCmd, 'The zonemap list is empty');
        }

        # Create two hashes to show which zonemaps are the default zonemap in a workspace, and
        #   which zonemaps are in use in a workspace grid
        foreach my $workspaceObj ($axmud::CLIENT->desktopObj->ivValues('workspaceHash')) {

            if ($workspaceObj->defaultZonemap) {

                $defaultHash{$workspaceObj->defaultZonemap} = undef;
            }

            foreach my $gridObj ($workspaceObj->ivValues('gridHash')) {

                if ($gridObj->zonemap) {

                    $inUseHash{$gridObj->zonemap} = undef;
                }
            }
        }

        # Display header
        $session->writeText('List of zonemaps (* in use, D default, @ standard, T temporary)');
        $session->writeText('     Name             Zones    Status');

        # Display list
        $count = 0;
        OUTER: foreach my $name (@list) {

            my ($obj, $standardFlag, $string, $fullString);

            $obj = $axmud::CLIENT->ivShow('zonemapHash', $name);

            if (! $axmud::CLIENT->ivExists('standardZonemapHash', $name)) {

                if ($switch) {

                    # This isn't a standard zonemap, so don't list it
                    next OUTER;
                }

            } else {

                $count++;
                $standardFlag = TRUE;
            }

            if (exists $inUseHash{$name}) {
                $string = ' *';
            } else {
                $string = '  ';
            }

            if (exists $defaultHash{$name}) {
                $string .= 'D';
            } else {
                $string .= ' ';
            }

            if ($standardFlag) {
                $string .= '@ ';
            } elsif ($obj->tempFlag) {
                $string .= 'T ';
            } else {
                $string .= '  ';
            }

            if ($obj->fullFlag) {
                $fullString = 'Full';
            } else {
                $fullString = 'Not full';
            }

            $session->writeText(
                $string . sprintf(
                    '%-16.16s %-8.8s %-8.8s',
                    $name,
                    $obj->ivPairs('modelHash'),
                    $fullString,
                )
            );
        }

        # Display footer
        if ($switch) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (standard zonemaps: ' . $count . ')',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (zonemaps: ' . @list . ', standard zonemaps: ' . $count . ')',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddZoneModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addzonemodel', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['azl', 'addzl', 'addzonemodel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a new zone model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args
        ) = @_;

        # Local variables
        my (
            $successFlag,
            $xPosBlocks, $yPosBlocks, $widthBlocks, $heightBlocks, $startCorner, $orientation,
            $maxWindows, $maxVisibleWindows, $defaultWidthBlocks, $defaultHeightBlocks, $ownerID,
            $enabledWinmap, $disabledWinmap, $gridWinmap,
            $positionFlag, $sizeFlag, $singleLayerFlag, $startCornerFlag, $orientationFlag,
            $maxWindowFlag, $maxVisibleWindowFlag, $defaultWidthFlag, $defaultHeightFlag,
            $ownerIDFlag, $enabledWinmapFlag, $disabledWinmapFlag, $gridWinmapFlag,
            $exitFlag, $reservedFlag, $zonemapName, $zonemapObj, $zoneModelObj,
            %reservedWinHash,
        );

        # Extract compulsory and optional switch options
        (
            $successFlag,
            $xPosBlocks, $yPosBlocks, $widthBlocks, $heightBlocks, $startCorner, $orientation,
            $maxWindows, $maxVisibleWindows, $defaultWidthBlocks, $defaultHeightBlocks, $ownerID,
            $enabledWinmap, $disabledWinmap, $gridWinmap,
            $positionFlag, $sizeFlag, $singleLayerFlag, $startCornerFlag, $orientationFlag,
            $maxWindowFlag, $maxVisibleWindowFlag, $defaultWidthFlag, $defaultHeightFlag,
            $ownerIDFlag, $enabledWinmapFlag, $disabledWinmapFlag, $gridWinmapFlag,
            @args,
        ) = $self->extractZoneModelSwitches($session, $inputString, $standardCmd, @args);

        if (! $successFlag) {

            # Error message already displayed
            return undef;
        }

        # Process reserved windows
        while (! $exitFlag) {

            my ($switch, $winName, $winType);

            ($switch, $winName, $winType, @args) = $self->extract('-r', 2, @args);
            if (defined $switch) {

                if (! defined $winName || ! defined $winType) {

                    return $self->error(
                        $session, $inputString,
                        'Can\'t add the zone model, reserved window name or type missing',
                    );

                } else {

                    $reservedFlag = TRUE;
                    $reservedWinHash{$winName} = $winType;
                }

            } else {

                $exitFlag = TRUE;
            }
        }

        # Check reserved windows
        if ($reservedFlag) {

            foreach my $winName (keys %reservedWinHash) {

                my $winType = $reservedWinHash{$winName};

                if (
                    ! defined $winType
                    || ! $axmud::CLIENT->ivExists('constGridWinTypeHash', $winType)
                ) {
                    return $self->error(
                        $session, $inputString,
                        'Can\'t add the zone model, invalid reserved window type \'' . $winType
                        . '\'',
                    );

                } elsif (
                    ($winType eq 'main' && $winName ne 'main')
                    || ($winType eq 'map' && $winName ne 'map')
                ) {
                    return $self->error(
                        $session, $inputString,
                        'Can\'t modify the zone model, invalid reserved window (type \''
                        . $winType . '\', but name \'' . $winName . '\')',
                    );
                }
            }
        }

        # Process remaining arguments
        $zonemapName = shift @args;

        # Check for improper arguments
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # Check the specified zonemap exists
        if (! defined $zonemapName) {

            return $self->error(
                $session, $inputString,
                'Add a zone model to which zonemap?',
            );

        } else {

            $zonemapObj = $axmud::CLIENT->ivShow('zonemapHash', $zonemapName);
            if (! $zonemapObj) {

                return $self->error(
                    $session, $inputString,
                    'The zonemap \'' . $zonemapName . '\' doesn\'t exist',
                );
            }
        }

        # Create the new zone model
        $zoneModelObj = Games::Axmud::Obj::ZoneModel->new($zonemapObj);
        if (! $zoneModelObj) {

            return $self->error($session, $inputString, 'Couldn\'t create a new zone model');
        }

        # Set the new model's IVs
        $zoneModelObj->set_vars(
            $standardCmd,
            $xPosBlocks, $yPosBlocks, $widthBlocks, $heightBlocks, $startCorner, $orientation,
            $maxWindows, $maxVisibleWindows, $defaultWidthBlocks, $defaultHeightBlocks, $ownerID,
            $enabledWinmap, $disabledWinmap, $gridWinmap,
            $positionFlag, $sizeFlag, $singleLayerFlag, $startCornerFlag, $orientationFlag,
            $maxWindowFlag, $maxVisibleWindowFlag, $defaultWidthFlag, $defaultHeightFlag,
            $ownerIDFlag, $enabledWinmapFlag, $disabledWinmapFlag, $gridWinmapFlag,
            $reservedFlag,
            %reservedWinHash,
        );

        # Try to add the new zone model at the specified position
        if (! $zonemapObj->addZoneModel($zoneModelObj)) {

            return $self->error(
                $session, $inputString,
                'Can\'t add the zone model to the zonemap \'' . $zonemapName . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Added the zone model #' . $zoneModelObj->number . ' to the zonemap \''
                . $zonemapName . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditZoneModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editzonemodel', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ezl', 'editzl', 'editzonemodel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a zone model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $zonemapName, $modelNumber,
            $check,
        ) = @_;

        # Local variables
        my ($zonemapObj, $zoneModelObj);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the zonemap exists
        if (! defined $zonemapName) {

            return $self->error(
                $session, $inputString,
                'Edit a zone model in which zonemap?',
            );

        } else {

            $zonemapObj = $axmud::CLIENT->ivShow('zonemapHash', $zonemapName);
            if (! $zonemapObj) {

                return $self->error(
                    $session, $inputString,
                    'The zonemap \'' . $zonemapName . '\' doesn\'t exist',
                );
            }
        }

        # Check the zone model <number> exists
        if (! defined $modelNumber) {

            return $self->error(
                $session, $inputString,
                'Edit which zone model?',
            );

        } else {

            $zoneModelObj = $zonemapObj->ivShow('modelHash', $modelNumber);
            if (! $zoneModelObj) {

                return $self->error(
                    $session, $inputString,
                    'Zone model #. ' . $modelNumber . ' doesn\'t exist in the zonemap \''
                    . $zonemapName . '\'',
                );
            }
        }

        # Open an 'edit' window for the zone model
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::ZoneModel',
                $session->mainWin,
                $session,
                'Edit zone model #' . $modelNumber,
                $zoneModelObj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit zone model #' . $modelNumber,
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for zone model #' . $modelNumber,
            );
        }
    }
}

{ package Games::Axmud::Cmd::ModifyZoneModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('modifyzonemodel', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'mzl',
            'modzl',
            'modzonemodel',
            'modifyzl',
            'modifyzonemodel',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies an existing zone model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args
        ) = @_;

        # Local variables
        my (
            $successFlag,
            $xPosBlocks, $yPosBlocks, $widthBlocks, $heightBlocks, $startCorner, $orientation,
            $maxWindows, $maxVisibleWindows, $defaultWidthBlocks, $defaultHeightBlocks, $ownerID,
            $enabledWinmap, $disabledWinmap, $gridWinmap,
            $positionFlag, $sizeFlag, $singleLayerFlag, $startCornerFlag, $orientationFlag,
            $maxWindowFlag, $maxVisibleWindowFlag, $defaultWidthFlag, $defaultHeightFlag,
            $ownerIDFlag, $enabledWinmapFlag, $disabledWinmapFlag, $gridWinmapFlag,
            $exitFlag, $reservedFlag, $zonemapName, $modelNumber, $zonemapObj, $modelObj,
            %reservedWinHash,
        );

        # Extract compulsory and optional switch options
        (
            $successFlag,
            $xPosBlocks, $yPosBlocks, $widthBlocks, $heightBlocks, $startCorner, $orientation,
            $maxWindows, $maxVisibleWindows, $defaultWidthBlocks, $defaultHeightBlocks, $ownerID,
            $enabledWinmap, $disabledWinmap, $gridWinmap,
            $positionFlag, $sizeFlag, $singleLayerFlag, $startCornerFlag, $orientationFlag,
            $maxWindowFlag, $maxVisibleWindowFlag, $defaultWidthFlag, $defaultHeightFlag,
            $ownerIDFlag, $enabledWinmapFlag, $disabledWinmapFlag, $gridWinmapFlag,
            @args,
        ) = $self->extractZoneModelSwitches($session, $inputString, $standardCmd, @args);

        if (! $successFlag) {

            # Error message already displayed
            return undef;
        }

        # Process reserved windows
        while (! $exitFlag) {

            my ($switch, $winName, $winType);

            ($switch, $winName, $winType, @args) = $self->extract('-r', 2, @args);
            if (defined $switch) {

                if (! defined $winName || ! defined $winType) {

                    return $self->error(
                        $session, $inputString,
                        'Can\'t modify the zone model, reserved window name or type missing',
                    );

                } else {

                    $reservedFlag = TRUE;
                    $reservedWinHash{$winName} = $winType;
                }

            } else {

                $exitFlag = TRUE;
            }
        }

        # Check reserved windows
        if ($reservedFlag) {

            foreach my $winName (keys %reservedWinHash) {

                my $winType = $reservedWinHash{$winName};

                if (
                    ! defined $winType
                    || ! $axmud::CLIENT->ivExists('constGridWinTypeHash', $winType)
                ) {
                    return $self->error(
                        $session, $inputString,
                        'Can\'t add the zone model, invalid reserved window type \'' . $winType
                        . '\'',
                    );

                } elsif (
                    ($winType eq 'main' && $winName ne 'main')
                    || ($winType eq 'map' && $winName ne 'map')
                ) {
                    return $self->error(
                        $session, $inputString,
                        'Can\'t modify the zone model, invalid reserved window (type \''
                        . $winType . '\', but name \'' . $winName . '\')',
                    );
                }
            }
        }

        # Extract remaining arguments
        $zonemapName = shift @args;
        $modelNumber = shift @args;

        # Check for improper arguments
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # Check the specified zonemap exists
        if (! defined $zonemapName) {

            return $self->error(
                $session, $inputString,
                'Modify a zone model in which zonemap?',
            );

        } else {

            $zonemapObj = $axmud::CLIENT->ivShow('zonemapHash', $zonemapName);
            if (! $zonemapObj) {

                return $self->error(
                    $session, $inputString,
                    'The zonemap \'' . $zonemapName . '\' doesn\'t exist',
                );
            }
        }

        # Check the zone model <number> exists
        if (! defined $modelNumber) {

            return $self->error(
                $session, $inputString,
                'Modify which zone model?',
            );

        } else {

            $modelObj = $zonemapObj->ivShow('modelHash', $modelNumber);
            if (! $modelObj) {

                return $self->error(
                    $session, $inputString,
                    'Zone model #. ' . $modelNumber . ' doesn\'t exist in the zonemap \''
                    . $zonemapName . '\'',
                );
            }
        }

        # If a new size and position are specified, check that the zonemap is unoccupied in that
        #   area
        if (
            (
                $positionFlag
                && ! $sizeFlag
                && ! $zonemapObj->checkPosnInMap(
                    $xPosBlocks,
                    $yPosBlocks,
                    $modelObj->width,
                    $modelObj->height,
                    $modelObj,
                )
            ) || (
                ! $positionFlag
                && $sizeFlag
                && ! $zonemapObj->checkPosnInMap(
                    $modelObj->left,
                    $modelObj->top,
                    $widthBlocks,
                    $heightBlocks,
                    $modelObj,
                )
            ) || (
                $positionFlag
                && $sizeFlag
                && ! $zonemapObj->checkPosnInMap(
                    $modelObj->left,
                    $modelObj->top,
                    $modelObj->width,
                    $modelObj->height,
                    $modelObj,
                )
            )
        ) {
            return $self->error(
                $session, $inputString,
                'The zonemap \'' . $zonemapName . '\' is occupied at the specified position',
            );
        }

        # Modify the new model's IVs
        $modelObj->set_vars(
            $standardCmd,
            $xPosBlocks, $yPosBlocks, $widthBlocks, $heightBlocks, $startCorner, $orientation,
            $maxWindows, $maxVisibleWindows, $defaultWidthBlocks, $defaultHeightBlocks, $ownerID,
            $enabledWinmap, $disabledWinmap, $gridWinmap,
            $positionFlag, $sizeFlag, $singleLayerFlag, $startCornerFlag, $orientationFlag,
            $maxWindowFlag, $maxVisibleWindowFlag, $defaultWidthFlag, $defaultHeightFlag,
            $ownerIDFlag, $enabledWinmapFlag, $disabledWinmapFlag, $gridWinmapFlag,
            $reservedFlag,
            %reservedWinHash,
        );

        return $self->complete(
            $session, $standardCmd,
            'Modified the zone model #' . $modelNumber . ' in the zonemap \'' . $zonemapName . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::DeleteZoneModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletezonemodel', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dzl', 'delzl', 'deletezl', 'deletezonemodel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a zone model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $zonemapName, $modelNumber,
            $check,
        ) = @_;

        # Local variables
        my $zonemapObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the zonemap exists
        if (! defined $zonemapName) {

            return $self->error(
                $session, $inputString,
                'Delete a zone model from which zonemap?',
            );

        } else {

            $zonemapObj = $axmud::CLIENT->ivShow('zonemapHash', $zonemapName);
            if (! $zonemapObj) {

                return $self->error(
                    $session, $inputString,
                    'The zonemap \'' . $zonemapName . '\' doesn\'t exist',
                );
            }
        }

        # Check the zone model <number> exists
        if (! defined $modelNumber) {

            return $self->error(
                $session, $inputString,
                'Delete which zone model?',
            );

        } elsif (! $zonemapObj->ivExists('modelHash', $modelNumber)) {

            return $self->error(
                $session, $inputString,
                'Zone model #. ' . $modelNumber . ' doesn\'t exist in the zonemap \''
                . $zonemapName . '\'',
            );
        }

        # Delete the zone model from the zonemap
        if (! $zonemapObj->deleteZoneModel($modelNumber)) {

            return $self->error(
                $session, $inputString,
                'Couldn\'t delete zone model #' . $modelNumber . ' from the zonemap \''
                . $zonemapName . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Deleted zone model #' . $modelNumber . ' from the zonemap \'' . $zonemapName
                . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListZoneModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listzonemodel', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lzl', 'listzl', 'listzonemodel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists zone models';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $zonemap,
            $check,
        ) = @_;

        # Local variables
        my (
            $zonemapObj, $count,
            @modeList,
        );

        # Check for improper arguments
        if (! defined $zonemap || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the zonemap exists
        $zonemapObj = $axmud::CLIENT->ivShow('zonemapHash', $zonemap);
        if (! $zonemapObj) {

            return $self->error(
                $session, $inputString,
                'The zonemap \'' . $zonemap . '\' doesn\'t exist',
            );

        # Check the zonemap contains some zone models
        } elsif (! $zonemapObj->modelHash) {

            return $self->complete(
                $session, $standardCmd,
                'The zonemap \'' . $zonemap . '\' contains no zone models',
            );
        }

        # Display header
        $session->writeText(
            'List of zone models for zonemap \'' . $zonemap . '\' (R = reserved,'
            . ' M = multiple layers,',
        );
        $session->writeText(
            '    X Y Wid Hei - position & size on 60x60 grid, Orient - zone filling direction,',
        );
        $session->writeText(
            '    Max = max areas, VMax = max areas in layer, DefW DefH = default area size,',
        );
        $session->writeText(
            '    ID = owner ID (in range 0-9999)',
        );

        $session->writeText(' ');
        $session->writeText(
            '    Num  X     Y     Wid Hei Start        Orient     Max VMax DefW DefH ID',
        );

        # Display list
        foreach my $modelObj (
            sort {$a->number <=> $b->number} ($zonemapObj->ivValues('modelHash'))
        ) {
            my ($string, $owner);

            if ($modelObj->reservedFlag) {
                $string = ' R';
            } else {
                $string = '  ';
            }

            if ($modelObj->multipleLayerFlag) {
                $string .= 'M ';
            } else {
                $string .= 'M ';
            }

            if (defined $modelObj->ownerString) {
                $owner = $modelObj->ownerString;
            } else {
                $owner = 'n/a';
            }

            $session->writeText(
                $string . sprintf(
                    '%-4.4s %-2.2s %-2.2s %-2.2s %-2.2s %-3.3s %-3.3s %-12.12s %-10.10s %-3.3s'
                    . ' %-4.4s %-4.4s %-4.4s %-4.4s',
                    $modelObj->number,
                    $modelObj->left, $modelObj->right,
                    $modelObj->top, $modelObj->bottom,
                    $modelObj->width, $modelObj->height,
                    $modelObj->startCorner,
                    $modelObj->orientation,
                    $modelObj->areaMax,
                    $modelObj->visibleAreaMax,
                    $modelObj->defaultAreaWidth,
                    $modelObj->defaultAreaHeight,
                    $owner,
                )
            );

            if ($modelObj->reservedFlag) {

                foreach my $winName (sort {lc($a) cmp lc($b)} ($modelObj->ivKeys('reservedHash'))) {

                    $session->writeText(
                        '         Reserved windows: ' . $winName . ' (type: '
                        . $modelObj->ivShow('reservedHash' , $winName) . ')',
                    );
                }
            }

            $string = '         Default winmaps: \'main\' windows, grids enabled - ';
            if (defined $modelObj->defaultEnabledWinmap) {
                $string .= '\'' . $modelObj->defaultEnabledWinmap . '\'';
            } else {
                $string .= '(not set)';
            }

            $string .= ', not enabled - ';
            if (defined $modelObj->defaultDisabledWinmap) {
                $string .= '\'' . $modelObj->defaultDisabledWinmap . '\'';
            } else {
                $string .= '(not set)';
            }

            $string .= ', \'internal\' windows - ';
            if (defined $modelObj->defaultInternalWinmap) {
                $string .= '\'' . $modelObj->defaultInternalWinmap . '\'';
            } else {
                $string .= '(not set)';
            }

            $session->writeText($string);
        }

        # Display footer
        $count = $zonemapObj->ivPairs('modelHash');
        if ($count == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 zone model found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . $count . ') zone models found',
            );
        }
    }
}

# 'Grid' windows

{ package Games::Axmud::Cmd::ToggleShare;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('toggleshare', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tsh', 'togglesh', 'toggleshare'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles sharing of \'main\' windows on or off';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if ($axmud::CLIENT->restartShareMainWinMode eq 'default') {

            if (! $axmud::CLIENT->shareMainWinFlag) {
                $axmud::CLIENT->ivPoke('restartShareMainWinMode', 'off');
            } else {
                $axmud::CLIENT->ivPoke('restartShareMainWinMode', 'on');
            }
        }

        # Toggle the flag
        if ($axmud::CLIENT->restartShareMainWinMode eq 'off') {

            $axmud::CLIENT->set_restartShareMainWinMode(TRUE);

            return $self->complete(
                $session, $standardCmd,
                'When ' . $axmud::SCRIPT . ' restarts, all sessions will have a single \'main\''
                . ' window',
            );

        } else {

            $axmud::CLIENT->set_restartShareMainWinMode(FALSE);

            return $self->complete(
                $session, $standardCmd,
                'When ' . $axmud::SCRIPT . ' restarts, all sessions will use their own \'main\''
                . ' window',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SwapWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('swapwindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['swn', 'swapwin', 'swapwindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Swaps the size and position of two windows';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $win1, $win2,
            $check,
        ) = @_;

        # Local variables
        my ($winObj1, $winObj2);

        # Check for improper arguments
        if (! defined $win1 || ! defined $win2 || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the window objects exist
        $winObj1 = $axmud::CLIENT->desktopObj->ivShow('gridWinHash', $win1);
        if (! $winObj1) {

            return $self->error(
                $session, $inputString,
                'Window #' . $win1 . ' doesn\'t seem to exist (try \';listwindow\')',
            );
        }

        $winObj2 = $axmud::CLIENT->desktopObj->ivShow('gridWinHash', $win2);
        if (! $winObj2) {

            return $self->error(
                $session, $inputString,
                'Window #' . $win2 . ' doesn\'t seem to exist (try \';listwindow\')',
            );
        }

        # Windows can't be swapped with themselves
        if ($win1 == $win2) {

            return $self->error(
                $session, $inputString,
                $axmud::SCRIPT . ' can\'t muster the enthusiasm for swapping windows with'
                . ' themselves',
            );
        }

        # 'grid' windows in a workspace without workspace grids can't be swapped
        if (! $winObj1->workspaceObj->gridEnableFlag || ! $winObj2->workspaceObj->gridEnableFlag) {

            return $self->error(
                $session, $inputString,
                'Windows can\'t be swapped when they\'re not arranged on a workspace grid',
            );
        }

        # Swap the windows
        if (! $axmud::CLIENT->desktopObj->swapGridWin($winObj1, $winObj2)) {

            return $self->error(
                $session, $inputString,
                'Windows #' . $win1 . ' and #' . $win2 . ' could not be swapped',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Windows #' . $win1 . ' and #' . $win2 . ' have been swapped',
            );
        }
    }
}

{ package Games::Axmud::Cmd::MoveWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('movewindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mwn', 'movewin', 'movewindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Moves a window into a new zone';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args
        ) = @_;

        # Local variables
        my (
            $switch, $resizeFlag, $winNum, $workspaceNum, $zoneNum, $winObj, $workspaceObj,
            $gridObj, $zoneObj,
        );

        # Extract switches
        ($switch, @args) = $self->extract('-r', 0, @args);
        if (defined $switch) {
            $resizeFlag = TRUE;
        } else {
            $resizeFlag = FALSE;
        }

        # Extract remaining arguments
        $winNum = shift @args;
        $workspaceNum = shift @args;
        $zoneNum = shift @args;

        # Check for improper arguments
        if (! defined $winNum || ! defined $workspaceNum || @args) {

            return $self->improper($session, $inputString);
        }

        # Check the window exists
        $winObj = $axmud::CLIENT->desktopObj->ivShow('gridWinHash', $winNum);
        if (! $winObj) {

            return $self->error(
                $session, $inputString,
                'Window #' . $winNum . ' doesn\'t exist (try \';listwindow\' first)',
            );
        }

        # Check the workspace exists
        $workspaceObj = $axmud::CLIENT->desktopObj->ivShow('workspaceHash', $workspaceNum);
        if (! $workspaceObj) {

            return $self->error(
                $session, $inputString,
                'Workspace #' . $workspaceNum . ' doesn\'t exist (try \';listworkspace\' first)',
            );
        }

        # Find the session's workspace grid on the specified workspace (this also checks that
        #   workspaces grids are enabled there at all)
        $gridObj = $workspaceObj->findWorkspaceGrid($winObj->session);
        if (! $gridObj) {

            if (! $workspaceObj->gridEnableFlag) {

                return $self->error(
                    $session, $inputString,
                    'Workspace grids are not activated in workspace #' . $workspaceNum . ', so the'
                    . ' window can\'t be moved there',
                );

            } else {

                return $self->error(
                    $session, $inputString,
                    'General error moving window to workspace #' . $workspaceNum,
                );
            }
        }

        # If it was specified, check the zone exists on the workspace grid
        if (defined $zoneNum) {

            $zoneObj = $gridObj->ivShow('zoneHash', $zoneNum);
            if (! $zoneObj) {

                return $self->error(
                    $session, $inputString,
                    'Workspace grid #' . $gridObj->number . ' doesn\'t contain a zone #' . $zoneNum,
                );
            }

        } else {

            # Choose a zone for ourselves. First check that the workspace grid actually contains
            #   some zones
            if (! $gridObj->zoneHash) {

                return $self->error(
                    $session, $inputString,
                    'Workspace grid #' . $gridObj->number . ' doesn\'t contain any zones',
                );
            }

            $zoneObj = $workspaceObj->chooseZone(
                $gridObj,
                $winObj->winType,
                $winObj->winName,
                $winObj->winWidget,
                undef,
                $winObj->owner,
                $winObj->session,
            );

            if (! $zoneObj) {

                return $self->error(
                    $session, $inputString,
                    'Workspace grid #' . $gridObj->number . ' is full, so can\'t move the window'
                    . ' to its workspace',
                );

            } else {

                $zoneNum = $zoneObj->number;
            }
        }

        # Try to move the window
        if (! $gridObj->changeWinzone($winObj, $zoneObj, $resizeFlag)) {

            return $self->error(
                $session, $inputString,
                'Window #' . $winNum . ' could not be moved to workspace #' . $workspaceNum
                . ', zone #' . $zoneObj->number,
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Window #' . $winNum . ' has been moved to workspace #' . $workspaceNum . ', zone #'
                . $zoneNum,
            );
        }
    }
}

{ package Games::Axmud::Cmd::RestoreWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('restorewindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rwn', 'restorewin', 'restorewindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Restores windows to their former size/position';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $winNum,
            $check,
        ) = @_;

        # Local variables
        my (
            $winObj,
            @winList,
            %zoneHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;rwn
        if (! defined $winNum) {

            # Use all 'grid' windows
            @winList = $axmud::CLIENT->desktopObj->listGridWins();
            if (! @winList) {

                return $self->error(
                    $session, $inputString,
                    'No \'grid\' windows found',
                );
            }

        # ;rwn <number>
        } else {

            $winObj = $axmud::CLIENT->desktopObj->ivShow('gridWinHash', $winNum);
            if (! $winObj) {

                return $self->error(
                    $session, $inputString,
                    'Window #' . $winNum . ' doesn\'t exist (try \';listwindow\' first)',
                );

            } else {

                push (@winList, $winObj);
            }
        }

        # Restore each window on the list in turn
        foreach my $thisWinObj (@winList) {

            $thisWinObj->workspaceObj->moveResizeWin(
                $thisWinObj,
                $thisWinObj->areaObj->xPosPixels,
                $thisWinObj->areaObj->yPosPixels,
                $thisWinObj->areaObj->widthPixels,
                $thisWinObj->areaObj->heightPixels,
            );

            # Windows in any affected zones most be restacked
            $zoneHash{$thisWinObj->areaObj->zoneObj} = $thisWinObj->areaObj->zoneObj;
        }

        # Re-stack windows
        foreach my $zoneObj (values %zoneHash) {

            $zoneObj->restackWin();
        }

        # Operation complete
        if ((scalar @winList) == 1) {

            $winObj = $winList[0];

            return $self->complete(
                $session, $standardCmd,
                'Window #' . $winObj->number . ' restored to its original size and position',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                scalar (@winList) . ' windows restored to their original size and position',
            );
        }

    }
}

{ package Games::Axmud::Cmd::GrabWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('grabwindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['gwn', 'grabwin', 'grabwindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Grabs an \'external\' window onto a workspace grid';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $allFlag, $workspaceNum, $workspaceFlag, $pattern, $workspaceObj, $gridObj,
            $failFlag,
            @matchList, @finalList,
        );

        # Extract switches
        ($switch, @args) = $self->extract('-a', 0, @args);
        if (defined $switch) {

            $allFlag = TRUE;
        }

        ($switch, $workspaceNum, @args) = $self->extract('-w', 1, @args);
        if (defined $switch) {

            $workspaceFlag = TRUE;
        }

        # Extract remaining arguments
        $pattern = shift @args;

        # Check for improper arguments
        if (! defined $pattern || ($workspaceFlag && ! defined $workspaceNum) || @args) {

            return $self->improper($session, $inputString);
        }

        # Check window grabbing is possible
        if (! $axmud::CLIENT->desktopObj->wmCtrlObj) {

            return $self->error(
                $session, $inputString,
                'Sorry, ' . $axmud::SCRIPT . ' is unable to handle external windows on this system',
            );
        }

        # Check $pattern is valid
        if ($axmud::CLIENT->regexCheck($pattern)) {

            return $self->error(
                $session, $inputString,
                'The pattern \'' . $pattern . '\' isn\'t a valid regular expression',
            );
        }

        # Set the workspace grid onto which the 'external' window should be grabbed
        if ($workspaceFlag) {

            $workspaceObj = $axmud::CLIENT->desktopObj->ivShow('workspaceHash', $workspaceNum);
            if (! $workspaceObj) {

                return $self->error(
                    $session, $inputString,
                    'Workspace #' . $workspaceNum . ' doesn\'t exist',
                );
            }

            # Find the session's workspace grid (this also checks ->gridEnableFlag)
            $gridObj = $workspaceObj->findWorkspaceGrid($session);

        } else {

            # Use the workspace for this session's 'main' window
            $workspaceObj = $session->mainWin->workspaceObj;
            $gridObj = $session->mainWin->workspaceGridObj;
        }

        if (! $gridObj) {

            return $self->error(
                $session, $inputString,
                'Workspace grids are disactivated on workspace #' . $workspaceNum,
            );
        }

        # Get a list of windows that match the $pattern. List in groups of two, in the form
        #   (window_title, window_internal_id)
        @matchList = $workspaceObj->matchWinList(0, $pattern);
        if (! @matchList) {

            return $self->error(
                $session, $inputString,
                'No \'external\' windows matching the pattern \'' . $pattern . '\' found',
            );
        }

        # Now, go through @matchList and remove any windows that have the same name as an Axmud
        #   'grid' or 'free' window (which will include any 'external' windows that have already
        #   been grabbed)
        do {

            my ($title, $id, $matchFlag);

            $title = shift @matchList;
            $id = shift @matchList;

            OUTER: foreach my $winObj (
                $axmud::CLIENT->desktopObj->ivValues('gridWinHash'),
                $axmud::CLIENT->desktopObj->ivValues('freeWinHash'),
            ) {
                if ($winObj->winWidget->get_title() eq $title) {

                    $matchFlag = TRUE;
                    last OUTER;
                }
            }

            if (! $matchFlag) {

                push (@finalList, $title, $id);
            }

        } until (! @matchList);

        # Display an error if there are no windows in @finalList
        if (! @finalList) {

            return $self->error(
                $session, $inputString,
                'No \'external\' windows matching the pattern \'' . $pattern . '\' found',
            );
        }

        # Incorporate all the windows in @finalList into the workspace grid
        do {

            my ($title, $id);

            $title = shift @finalList;
            $id = shift @finalList;

            if (
                ! $workspaceObj->createGridWin(
                    'external',
                    $title,                     # Window name
                    $title,                     # Window title
                    undef,                      # No winmap
                    undef,                      # Default package name
                    undef,                      # Gtk3::Window currently unknown...
                    $id,                        # ...but we know the X11::WMCtrl internal ID
                    $session,                   # Owner
                    $session,                   # Session
                    $gridObj->number,
                )
            ) {
                $failFlag = TRUE;
                last OUTER;
            }

        } until (! @finalList);

        if ($failFlag) {

            if (@finalList == 1) {

                return $self->error(
                    $session, $inputString,
                    'Failed to grab 1 window matching the pattern \'' . $pattern . '\'',
                );

            } else {

                return $self->error(
                    $session, $inputString,
                    'Failed to grab ' . (scalar @finalList) . ' windows matching the pattern \''
                    . $pattern . '\'',
                );
            }

        } else {

            if (@finalList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Grabbed 1 window matching the pattern \'' . $pattern . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Grabbed ' . (scalar @finalList) . ' windows matching the pattern \''
                    . $pattern . '\'',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::BanishWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('banishwindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['bwn', 'banwin', 'banishwin', 'banishwindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Removes an \'external\' window from a workspace grid';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $winNum,
            $check,
        ) = @_;

        # Local variables
        my (
            $winObj,
            @winList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;bw
        if (! defined $winNum) {

            # Get a list of all 'external' windows
            @winList = $axmud::CLIENT->desktopObj->listGridWins('external');

        # ;bw <number>
        } else {

            # Check that $winNum is a valid number (the error message if the window doesn't
            #   exist would read 'Window #firefox doesn't seem to exist, if we don't perform this
            #   check')
            if (! $axmud::CLIENT->intCheck($winNum, 0)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid window number \'' . $winNum . '\' (try \';listwindow\' first)',
                );
            }

            # Check the window exists and is an 'external' window
            $winObj = $axmud::CLIENT->desktopObj->ivShow('gridWinHash', $winNum);
            if (! $winObj) {

                return $self->error(
                    $session, $inputString,
                    'Window #' . $winNum . ' doesn\'t exist (try \';listwindow\' first)',
                );

            } elsif ($winObj->winType ne 'external') {

                return $self->error(
                    $session, $inputString,
                    'Window #' . $winNum . ' isn\'t an \'external\' window',
                );
            }

            # Compile a list of 1 window
            @winList = ($winObj);
        }

        # Banish each window in the list in turn
        foreach my $thisWinObj (@winList) {

            # Destroy the window object, and update the zone's internal grid and the zone's other
            #   variables
            $thisWinObj->winDisengage();
        }

        if (defined $winNum) {

            return $self->complete(
                $session, $standardCmd,
                'Window #' . $winNum . ' removed from ' . $axmud::SCRIPT . '\'s workspace grids',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                scalar @winList . ' windows removed from ' . $axmud::SCRIPT . '\'s workspace grids',
            );
        }
    }
}

{ package Games::Axmud::Cmd::FixWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('fixwindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['fwn', 'fixwin', 'fixwindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Fixes a window at its current size/position';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args
        ) = @_;

        # Local variables
        my (
            $switch, $resizeFlag, $winNum, $winObj, $xPosPixels, $yPosPixels,
            $widthPixels, $heightPixels, $zoneObj, $result,
        );

        # Extract switches
        ($switch, @args) = $self->extract('-r', 0, @args);
        if (defined $switch) {

            $resizeFlag = TRUE;
        }

        # Extract remaining arguments
        $winNum = shift @args;

        # Check for improper arguments
        if (! defined $winNum || @args) {

            return $self->improper($session, $inputString);
        }

        # Check the window exists
        $winObj = $axmud::CLIENT->desktopObj->ivShow('gridWinHash', $winNum);
        if (! $winObj) {

            return $self->error(
                $session, $inputString,
                'Window #' . $winNum . ' doesn\'t exist (try \';listwindow\' first)',
            );

        # Can't fix windows in workspaces where workspace grids are disabled
        } elsif (! $winObj->workspaceObj->gridEnableFlag) {

            return $self->error(
                $session, $inputString,
                'Window #' . $winNum . ' can\'t be fixed in place on workspace #'
                . $winObj->workspaceObj->number . ' because workspace grids are not activated'
                . ' there',
            );
        }

        # Get the window's actual size and position
        ($xPosPixels, $yPosPixels, $widthPixels, $heightPixels)
            = $winObj->workspaceObj->getWinGeometry($winObj->winWidget->get_window());

        # Find which zone occupies this position
        $zoneObj = $winObj->workspaceGridObj->findZone($xPosPixels, $yPosPixels);
        if (! $zoneObj) {

            return $self->error(
                $session, $inputString,
                'Window #' . $winNum . ' can\'t be fixed in place (zone not found)',
            );
        }

        # Try to fix the window
        if ($resizeFlag) {

            # Move and resize the window
            $result = $winObj->workspaceGridObj->changeWinzone(
                $winObj,
                $zoneObj,
                TRUE,           # Use default size
            );

        } else {

            # Move the window without resizing
            $result = $winObj->workspaceGridObj->changeWinzone(
                $winObj,
                $zoneObj,
                FALSE,          # Use current size and width
                $widthPixels,
                $heightPixels,
            );
        }

        if (! $result) {

            return $self->error(
                $session, $inputString,
                'Window #' . $winNum . ' could not be fixed in place in zone #' . $zoneObj->number,
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Window #' . $winNum . ' has been fixed in place in zone #' . $zoneObj->number,
            );
        }
    }
}

{ package Games::Axmud::Cmd::FlashWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('flashwindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['flw', 'flashwin', 'flashwindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets a \'grid\' window\'s urgency hint';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my $winObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;flw
        if (! defined $number) {

            $winObj = $session->mainWin;

        # ;flw <number>
        } else {

            # Check the window <number> exists
            $winObj = $axmud::CLIENT->desktopObj->ivShow('gridWinHash', $number);
            if (! $winObj) {

                return $self->error(
                    $session, $inputString,
                    'Window #' . $number . ' doesn\'t exist (try \';listwindow\' first)',
                );
            }
        }

        # Set the window's urgency hint. Forcing the setting by using the TRUE flag seems to work
        #   more reliably, in this case
        $winObj->setUrgent(TRUE);

        if (! defined $number) {

            return $self->complete(
                $session, $standardCmd,
                '\'Main\' window\'s urgency hint set',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                '\'Grid\' window #' . $number . ' urgency hint set',
            );
        }
    }
}

{ package Games::Axmud::Cmd::UnflashWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('unflashwindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ufw', 'unflashwin', 'unflashwindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets a \'grid\' window\'s urgency hint';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my $winObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;ufw
        if (! defined $number) {

            $winObj = $session->mainWin;

        # ;ufw <number>
        } else {

            # Check the window <number> exists
            $winObj = $axmud::CLIENT->desktopObj->ivShow('gridWinHash', $number);
            if (! $winObj) {

                return $self->error(
                    $session, $inputString,
                    'Window #' . $number . ' doesn\'t exist (try \';listwindow\' first)',
                );
            }
        }

        # Set the window's urgency hint. Forcing the setting by using the TRUE flag seems to work
        #   more reliably, in this case
        $winObj->resetUrgent(TRUE);

        if (! defined $number) {

            return $self->complete(
                $session, $standardCmd,
                '\'Main\' window\'s urgency hint reset',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                '\'Grid\' window #' . $number . ' urgency hint reset',
            );
        }
    }
}

{ package Games::Axmud::Cmd::CloseWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('closewindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cwn', 'closewin', 'closewindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Closes a \'grid\' window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my ($winObj, $result);

        # Check for improper arguments
        if (! defined $number || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the window <number> exists
        $winObj = $axmud::CLIENT->desktopObj->ivShow('gridWinHash', $number);
        if (! $winObj) {

            return $self->error(
                $session, $inputString,
                'Window #' . $number . ' doesn\'t exist (try \';listwindow\' first)',
            );
        }

        # Close the window (if allowed)
        if ($winObj->winType eq 'main') {

            return $self->error(
                $session, $inputString,
                '\'main\' windows can\'t be closed with this command',
            );

        } else {

            $result = $winObj->winDestroy();
        }

        if (! $result) {

            return $self->error(
                $session, $inputString,
                'Window #' . $number . ' (type \'' . $winObj->winType . '\') can\'t be closed',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Window #' . $number . ' (type \'' . $winObj->winType . '\') closed',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editwindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ewn', 'editwin', 'editwindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a \'grid\' window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number, $switch, $objNumber,
            $check,
        ) = @_;

        # Local variables
        my ($winObj, $stripObj, $tableObj);

        # Check for improper arguments
        if (
            ! defined $number
            || (defined $switch && $switch ne '-p' && $switch ne '-t')
            || (defined $switch && ! defined $objNumber)
            || defined $check
        ) {
            return $self->improper($session, $inputString);
        }

        # Check the window object exists
        $winObj = $axmud::CLIENT->desktopObj->ivShow('gridWinHash', $number);
        if (! $winObj) {

            return $self->error(
                $session, $inputString,
                'The \'grid\' window #' . $number . ' doesn\'t exist',
            );
        }

        # ;ewn <number>
        if (! $switch) {

            # Open an 'edit' window for the 'grid' window
            if (
                ! $session->mainWin->createFreeWin(
                    'Games::Axmud::EditWin::Window',
                    $session->mainWin,
                    $session,
                    'Edit \'grid\' window #' . $number,
                    $winObj,
                    FALSE,                  # Not temporary
                )
            ) {
                return $self->error(
                    $session, $inputString,
                    'Could not edit \'grid\' window #' . $number,
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Opened \'edit\' window for \'grid\' window #' . $number,
                );
            }

        # ;ewn <number> -p <strip>
        # ;ewn <number> -t <strip>
        } else {

            # Check it's an 'internal' window
            if (
                $winObj->winType ne 'main'
                && $winObj->winType ne 'protocol'
                && $winObj->winType ne 'custom'
            ) {
                return $self->error(
                    $session, $inputString,
                    'Window #' . $number . ' is a \'' . $winObj->winType . '\' window; the switches'
                    . ' -p and -t can only be used with \'internal\' windows',
                );
            }

            # ;ewn <number> -p <strip>
            if ($switch eq '-p') {

                # Check the strip object exists
                $stripObj = $winObj->ivShow('stripHash', $objNumber);
                if (! $stripObj) {

                    return $self->error(
                        $session, $inputString,
                        'Strip object #' . $objNumber . ' doesn\'t exist in \'grid\' window #'
                        . $number,
                    );
                }

                # Open an 'edit' window for the strip object
                if (
                    ! $session->mainWin->createFreeWin(
                        'Games::Axmud::EditWin::Strip',
                        $session->mainWin,
                        $session,
                        'Edit strip object #' . $objNumber,
                        $stripObj,
                        FALSE,                  # Not temporary
                    )
                ) {
                    return $self->error(
                        $session, $inputString,
                        'Could not edit strip object #' . $objNumber,
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'Opened \'edit\' window for strip object #' . $objNumber,
                    );
                }

            # ;ewn <number> -t <strip>
            } else {

                # Check the table object exists
                $tableObj = $winObj->tableStripObj->ivShow('tableObjHash', $objNumber);
                if (! $tableObj) {

                    return $self->error(
                        $session, $inputString,
                        'Table object #' . $objNumber . ' doesn\'t exist in \'grid\' window #'
                        . $number,
                    );
                }

                # Open an 'edit' window for the table object
                if (
                    ! $session->mainWin->createFreeWin(
                        'Games::Axmud::EditWin::Table',
                        $session->mainWin,
                        $session,
                        'Edit table object #' . $objNumber,
                        $tableObj,
                        FALSE,                  # Not temporary
                    )
                ) {
                    return $self->error(
                        $session, $inputString,
                        'Could not edit table object #' . $objNumber,
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'Opened \'edit\' window for table object #' . $objNumber,
                    );
                }
            }
        }
    }
}

{ package Games::Axmud::Cmd::ListWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listwindow', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lwn', 'listwin', 'listwindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists \'grid\' windows';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $sessionFlag, $flagCount, $workspaceFlag, $gridFlag, $zoneFlag,
            @winObjList,
        );

        # Extract switches
        ($switch, @args) = $self->extract('-s', 0, @args);
        if (defined $switch) {

            $sessionFlag = TRUE;
        }

        $flagCount = 0;

        ($switch, @args) = $self->extract('-w', 0, @args);
        if (defined $switch) {

            $workspaceFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-g', 0, @args);
        if (defined $switch) {

            $gridFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-z', 0, @args);
        if (defined $switch) {

            $zoneFlag = TRUE;
            $flagCount++;
        }

        # @args should now contain exactly one argument
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # Some switches can't be combined
        if ($flagCount > 1) {

            return $self->error(
                $session, $inputString,
                'The switches -w, -g and -z can\'t be combined',
            );
        }

        # Get a list of 'grid' window objects, removing any not controlled by this session if that
        #   switch was specified
        foreach my $winObj (
            sort {$a->number <=> $b->number} ($axmud::CLIENT->desktopObj->ivValues('gridWinHash'))
        ) {
            if (! $sessionFlag || ($winObj->session && $winObj->session eq $session)) {

                push (@winObjList, $winObj);
            }
        }

        if (! @winObjList) {

            if (! $sessionFlag) {

                return $self->error(
                    $session, $inputString,
                    'No \'grid\' windows found',
                );

            } else {

                return $self->error(
                    $session, $inputString,
                    'No \'grid\' windows for this session found',
                );
            }
        }

        # Display header
        if (! $flagCount || $workspaceFlag) {

            $session->writeText(
                'List of \'grid\' windows (+ enabled, * visible; position in pixels)',
            );

        } elsif ($gridFlag) {

            $session->writeText(
                'List of \'grid\' windows (+ enabled, * visible; grid position in blocks)',
            );

        } else {

            $session->writeText(
                'List of \'grid\' windows (+ enabled, * visible; zone internal grid position in'
                . ' blocks)',
            );
        }

        $session->writeText(
            '   Num  Type     Name             Winmap           Sesn Wksp Grid Zone Layer'
            . ' X/Y          Wid/Hei',
        );

        # Display list
        foreach my $winObj (@winObjList) {

            my (
                $column, $sessionString, $workspaceString, $gridString, $winmapString, $mainString,
                $zoneString, $layerString, $posnString, $sizeString,
            );

            if ($winObj->enabledFlag) {
                $column = '+';
            } else {
                $column = ' ';
            }

            if ($winObj->visibleFlag) {
                $column .= '* ';
            } else {
                $column .= '  ';
            }

            if ($winObj->winmap) {
                $winmapString = $winObj->winmap;
            } else {
                $winmapString = 'n/a';
            }

            if ($winObj->session) {
                $sessionString = $winObj->session->number;
            } else {
                $sessionString = 'n/a';
            }

            if ($winObj->workspaceObj) {
                $workspaceString = $winObj->workspaceObj->number;
            } else {
                $workspaceString = 'n/a';
            }

            if ($winObj->workspaceGridObj) {
                $gridString = $winObj->workspaceGridObj->number;
            } else {
                $gridString = 'n/a';
            }

            $mainString = sprintf(
                '%-4.4s %-8.8s %-16.16s %-16.16s %-4.4s %-4.4s %-4.4s ',
                $winObj->number,
                $winObj->winType,
                $winObj->winName,
                $winmapString,
                $sessionString,
                $workspaceString,
                $gridString,
            );

            if ($winObj->workspaceObj->gridEnableFlag) {

                $zoneString = $winObj->areaObj->zoneObj->number;
                $layerString = $winObj->areaObj->layer;

                # ;lwn
                # ;lwn -w
                if (! $flagCount || $workspaceFlag) {

                    $posnString = $winObj->areaObj->xPosPixels . '/' . $winObj->areaObj->yPosPixels;
                    $sizeString
                        = $winObj->areaObj->widthPixels . '/' . $winObj->areaObj->heightPixels;

                # ;lwn -g
                } elsif ($gridFlag) {

                    $posnString = (
                                    $winObj->areaObj->zoneObj->xPosBlocks
                                    + $winObj->areaObj->leftBlocks
                                  ) . '/' . (
                                    $winObj->areaObj->zoneObj->yPosBlocks
                                    + $winObj->areaObj->topBlocks
                                  );
                    $sizeString = $winObj->areaObj->widthBlocks
                                    . '/' . $winObj->areaObj->heightBlocks;

                # ;lwn -z
                } else {

                    $posnString = $winObj->areaObj->leftBlocks . '/'
                                    . $winObj->areaObj->topBlocks;
                    $sizeString = $winObj->areaObj->widthBlocks
                                    . '/' . $winObj->areaObj->heightBlocks;
                }

                $session->writeText(
                    $column . $mainString . sprintf(
                        '%-4.4s %-5.5s %-12.12s %-12.12s',
                        $zoneString,
                        $layerString,
                        $posnString,
                        $sizeString,
                    )
                );
            }
        }

        # Display footer
        if (@winObjList == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 \'grid\' window found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . @winObjList . ' \'grid\' windows found)',
            );
        }
    }
}

# 'Internal' windows

{ package Games::Axmud::Cmd::EditWindowStrip;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editwindowstrip', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['est', 'editstrip', 'editwindowstrip'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a strip object';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $winNum, $stripNum,
            $check,
        ) = @_;

        # Local variables
        my ($winObj, $stripObj);

        # Check for improper arguments
        if (! defined $winNum || ! defined $stripNum || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the window object exists
        $winObj = $axmud::CLIENT->desktopObj->ivShow('gridWinHash', $winNum);
        if (! $winObj) {

            return $self->error(
                $session, $inputString,
                'The \'grid\' window #' . $winNum . ' doesn\'t exist',
            );
        }

        # Check it's an 'internal' window
        if (
            $winObj->winType ne 'main'
            && $winObj->winType ne 'protocol'
            && $winObj->winType ne 'custom'
        ) {
            return $self->error(
                $session, $inputString,
                'Window #' . $winNum . ' is a \'' . $winObj->winType . '\' window; this command can'
                . ' only be used with \'internal\' windows',
            );
        }

        # Check the strip object exists
        $stripObj = $winObj->ivShow('stripHash', $stripNum);
        if (! $stripObj) {

            return $self->error(
                $session, $inputString,
                'Strip object #' . $stripNum . ' doesn\'t exist in \'grid\' window #'
                . $winNum,
            );
        }

        # Open an 'edit' window for the strip object
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Strip',
                $session->mainWin,
                $session,
                'Edit strip object #' . $stripNum,
                $stripObj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit strip object #' . $stripNum,
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for strip object #' . $stripNum,
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListWindowStrip;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listwindowstrip', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lst', 'liststrip', 'listwindowstrip'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists strip objects in an \'internal\' window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($switch, $initFlag, $winNum, $winObj, $count);

        # Extract switches
        ($switch, @args) = $self->extract('-i', 0, @args);
        if (defined $switch) {

            $initFlag = TRUE;
        }

        # Extract remaining arguments
        $winNum = shift @args;

        # There should be nothing left in @args
        if (! defined $winNum || @args) {

            return $self->improper($session, $inputString);
        }

        # Check the window object exists
        $winObj = $axmud::CLIENT->desktopObj->ivShow('gridWinHash', $winNum);
        if (! $winObj) {

            return $self->error(
                $session, $inputString,
                'The \'grid\' window #' . $winNum . ' doesn\'t exist',
            );
        }

        # Check it's an 'internal' window
        if (
            $winObj->winType ne 'main'
            && $winObj->winType ne 'protocol'
            && $winObj->winType ne 'custom'
        ) {
            return $self->error(
                $session, $inputString,
                'Window #' . $winNum . ' is a \'' . $winObj->winType . '\' window; this command can'
                . ' only be used with \'internal\' windows',
            );
        }

        # Display header
        $session->writeText(
            'List of strip objects in \'grid\' window #' . $winNum . ' (* visible, ^ jealous)',
        );
        $session->writeText('   Num  Type             Package name');

        # Display list
        $count = scalar ($winObj->stripList);

        foreach my $stripObj ($winObj->stripList) {

            my $column;

            if ($stripObj->visibleFlag) {
                $column = '*';
            } else {
                $column = ' ';
            }

            if ($stripObj->jealousyFlag) {
                $column .= '^ ';
            } else {
                $column .= '  ';
            }

            $session->writeText(
                $column . sprintf(
                    '%-4.4s %-16.16s %-64.64s',
                    $stripObj->number,
                    $stripObj->type,
                    $stripObj->_objClass,
                ),
            );

            if ($switch && $stripObj->initHash) {

                $session->writeText('      Initialisation settings');

                foreach my $key (sort {lc($a) cmp lc($b)} ($stripObj->ivKeys('initHash'))) {

                    my $value = $stripObj->ivShow('initHash', $key);

                    if (! defined $value) {
                        $session->writeText('         Key: ' . $key . ' , value: <undef>');
                    } else {
                        $session->writeText('         Key: ' . $key . ' , value: ' . $value);
                    }
                }
            }
        }

        # Display footer
        if ($count == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 strip object found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . $count . ' strip object found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditWindowTable;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editwindowtable', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ewt', 'edittable', 'editwindowtable'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a table object';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $winNum, $tableNum,
            $check,
        ) = @_;

        # Local variables
        my ($winObj, $tableObj);

        # Check for improper arguments
        if (! defined $winNum || ! defined $tableNum || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the window object exists
        $winObj = $axmud::CLIENT->desktopObj->ivShow('gridWinHash', $winNum);
        if (! $winObj) {

            return $self->error(
                $session, $inputString,
                'The \'grid\' window #' . $winNum . ' doesn\'t exist',
            );
        }

        # Check it's an 'internal' window
        if (
            $winObj->winType ne 'main'
            && $winObj->winType ne 'protocol'
            && $winObj->winType ne 'custom'
        ) {
            return $self->error(
                $session, $inputString,
                'Window #' . $winNum . ' is a \'' . $winObj->winType . '\' window; this command can'
                . ' only be used with \'internal\' windows',
            );
        }

        # Check the table object exists
        $tableObj = $winObj->tableStripObj->ivShow('tableObjHash', $tableNum);
        if (! $tableObj) {

            return $self->error(
                $session, $inputString,
                'Table object #' . $tableNum . ' doesn\'t exist in \'grid\' window #'
                . $winNum,
            );
        }

        # Open an 'edit' window for the table object
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Table',
                $session->mainWin,
                $session,
                'Edit table object #' . $tableNum,
                $tableObj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit table object #' . $tableNum,
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for table object #' . $tableNum,
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListWindowTable;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listwindowtable', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lwt', 'listtable', 'listwindowtable'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists table objects in an \'internal\' window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($switch, $initFlag, $winNum, $winObj, $count);

        # Extract switches
        ($switch, @args) = $self->extract('-i', 0, @args);
        if (defined $switch) {

            $initFlag = TRUE;
        }

        # Extract remaining arguments
        $winNum = shift @args;

        # There should be nothing left in @args
        if (! defined $winNum || @args) {

            return $self->improper($session, $inputString);
        }

        # Check the window object exists
        $winObj = $axmud::CLIENT->desktopObj->ivShow('gridWinHash', $winNum);
        if (! $winObj) {

            return $self->error(
                $session, $inputString,
                'The \'grid\' window #' . $winNum . ' doesn\'t exist',
            );
        }

        # Check it's an 'internal' window
        if (
            $winObj->winType ne 'main'
            && $winObj->winType ne 'protocol'
            && $winObj->winType ne 'custom'
        ) {
            return $self->error(
                $session, $inputString,
                'Window #' . $winNum . ' is a \'' . $winObj->winType . '\' window; this command can'
                . ' only be used with \'internal\' windows',
            );
        }

        # Display header
        $session->writeText(
            'List of table objects in \'grid\' window #' . $winNum
            . ' (m removeable, s resizeable)',
        );
        $session->writeText('   Num  Type             Name             Package name');

        # Display list
        $count = $winObj->tableStripObj->ivPairs('tableObjHash');

        foreach my $tableObj (
            sort {$a->number <=> $b->number} ($winObj->tableStripObj->ivValues('tableObjHash'))
        ) {
            my $column;

            if ($tableObj->allowRemoveFlag) {
                $column = 'm';
            } else {
                $column = ' ';
            }

            if ($tableObj->allowResizeFlag) {
                $column .= 's ';
            } else {
                $column .= '  ';
            }

            $session->writeText(
                $column . sprintf(
                    '%-4.4s %-16.16s %-16.16s %-64.64s',
                    $tableObj->number,
                    $tableObj->type,
                    $tableObj->name,
                    $tableObj->_objClass,
                ),
            );

            if ($switch && $tableObj->initHash) {

                $session->writeText('      Initialisation settings');

                foreach my $key (sort {lc($a) cmp lc($b)} ($tableObj->ivKeys('initHash'))) {

                    my $value = $tableObj->ivShow('initHash', $key);

                    if (! defined $value) {
                        $session->writeText('         Key: ' . $key . ' , value: <undef>');
                    } else {
                        $session->writeText('         Key: ' . $key . ' , value: ' . $value);
                    }
                }
            }
        }

        # Display footer
        if ($count == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 table object found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . $count . ' table object found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::OpenTaskWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('opentaskwindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['otw', 'opentaskwin', 'opentaskwindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens a task\'s own window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName,
            $check,
        ) = @_;

        # Local variables
        my (
            $successCount, $failCount,
            @taskList,
        );

        # Check for improper arguments
        if (! defined $taskName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get a list of blessed references to tasks matching the string $taskName
        @taskList = $self->findTask($session, $taskName);

        # For every matching task that's allowed a task window, but which doesn't have its window
        #   open - open the window
        $successCount = 0;
        $failCount = 0;
        foreach my $taskObj (@taskList) {

            if ($taskObj->allowWinFlag && ! $taskObj->taskWinFlag) {

                if ($taskObj->toggleWin()) {
                    $successCount++;
                } else {
                    $failCount++;
                }
            }
        }

        if (! @taskList) {

            return $self->error(
                $session, $inputString,
                'There are no tasks matching \'' . $taskName . '\'',
            );

        } elsif (! $successCount && ! $failCount) {

            if (@taskList == 1) {

                return $self->error(
                    $session, $inputString,
                    'Cannot open task windows for 1 matching task (window already open, or no'
                    . ' window allowed)',
                );

            } else {

                return $self->error(
                    $session, $inputString,
                    'Cannot open task windows for ' . @taskList . ' matching tasks (windows'
                    . ' already open, or no windows allowed)',
                );
            }

        } elsif (@taskList == 1) {

            if ($successCount) {

                return $self->complete(
                    $session, $standardCmd,
                    'Opened 1 task window successfully',
                );

            } else {

                return $self->error(
                    $session, $inputString,
                    'Tried to open 1 task window, but failed',
                );
            }

        } else {

            if (! $failCount) {

                return $self->complete(
                    $session, $standardCmd,
                    'Opened ' . @taskList . ' task windows',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Tried to open ' . @taskList . ' task windows, successes: '
                    . $successCount . ', failures: ' . $failCount,
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::CloseTaskWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('closetaskwindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ctw', 'closetaskwin', 'closetaskwindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Closes a task\'s own window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $taskName,
            $check,
        ) = @_;

        # Local variables
        my (
            $successCount, $failCount,
            @taskList,
        );

        # Check for improper arguments
        if (! defined $taskName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get a list of blessed references to tasks matching the string $taskName
        @taskList = $self->findTask($session, $taskName);

        # For every matching task whose task window is open, close it
        $successCount = 0;
        $failCount = 0;
        foreach my $taskObj (@taskList) {

            if ($taskObj->taskWinFlag) {

                if ($taskObj->toggleWin()) {
                    $successCount++;
                } else {
                    $failCount++;
                }
            }
        }

        # (A simpler message tree than for ';opentaskwindow', because the close window operation is
        #   almost always successful
        if (! @taskList) {

            return $self->error(
                $session, $inputString,
                'There are no tasks matching \'' . $taskName . '\'',
            );

        } elsif (@taskList == 1) {

            if ($successCount) {

                return $self->complete(
                    $session, $standardCmd,
                    'Closed 1 task window successfully',
                );

            } else {

                return $self->error(
                    $session, $inputString,
                    'Tried to close 1 task window, but failed',
                );
            }

        } else {

            if (! $failCount) {

                return $self->complete(
                    $session, $standardCmd,
                    'Closed ' . @taskList . ' task windows',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Tried to close ' . @taskList . ' task windows, successes: '
                    . $successCount . ', failures: ' . $failCount,
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::EditToolbar;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('edittoolbar', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['etb', 'edittool', 'edittoolbar'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a toolbar button';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check an object for the toolbar button exists
        $obj = $axmud::CLIENT->ivShow('toolbarHash', $name);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'The toolbar button \'' . $name . '\' doesn\'t exist',
            );
        }

        # Open an 'edit' window for the toolbar button
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Toolbar',
                $session->mainWin,
                $session,
                'Edit toolbar button \'' . $name . '\'',
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the toolbar button \'' . $name . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the toolbar button \'' . $name . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListToolbar;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listtoolbar', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ltb', 'listtool', 'listtoolbar'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists toolbar buttons used in \'internal\' windows';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $count;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Display header
        $session->writeText(
            'List of toolbar buttons for \'internal\' windows (S - require session, C - require'
            . ' connection, U - custom button)',
        );

        # Display list
        $count = 0;
        foreach my $item ($axmud::CLIENT->toolbarList) {

            my ($obj, $column);

            if ($item eq 'separator') {

                $session->writeText('    <separator>');

            } else {

                # (Don't count separators)
                $count++;

                $obj = $axmud::CLIENT->ivShow('toolbarHash', $item);
                if ($obj) {

                    if ($obj->requireSessionFlag) {
                        $column = 'S';
                    } else {
                        $column = ' ';
                    }

                    if ($obj->requireConnectFlag) {
                        $column .= 'C';
                    } else {
                        $column .= ' ';
                    }

                    if ($obj->customFlag) {
                        $column .= 'U ';
                    } else {
                        $column .= '  ';
                    }

                    $session->writeText($column . $obj->name);
                    $session->writeText('       Description:  ' . $obj->descrip);
                    $session->writeText('       Instruction:  ' . $obj->instruct);
                    $session->writeText('       Icon path:    ' . $obj->iconPath);
                }
            }
        }

        # Display footer
        if ($count == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 toolbar button found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . $count . ' toolbar buttons found)',
            );
        }
    }
}

# 'Free' windows

{ package Games::Axmud::Cmd::EditFreeWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editfreewindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['efw', 'editfreewin', 'editfreewindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a \'free\' window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my $winObj;

        # Check for improper arguments
        if (! defined $number || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the window object exists
        $winObj = $axmud::CLIENT->desktopObj->ivShow('freeWinHash', $number);
        if (! $winObj) {

            return $self->error(
                $session, $inputString,
                'The \'free\' window #' . $number . ' doesn\'t exist',
            );
        }

        # Open an 'edit' window for the 'free' window
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Window',
                $session->mainWin,
                $session,
                'Edit \'free\' window #' . $number,
                $winObj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit \'free\' window #' . $number,
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for \'free\' window #' . $number,
            );
        }
    }
}

{ package Games::Axmud::Cmd::CloseFreeWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('closefreewindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cfw', 'closefreewin', 'closefreewindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Closes a \'free\' window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my @winObjList;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;cfw
        # ;cfw -s
        if (! defined $number || $number eq '-s') {

            # Get a list of 'free' window objects, removing any not controlled by this session if
            #   that switch was specified
            foreach my $winObj (
                sort {$a->number <=> $b->number}
                ($axmud::CLIENT->desktopObj->ivValues('freeWinHash'))
            ) {
                if (! defined $number || ($winObj->session && $winObj->session eq $session)) {

                    push (@winObjList, $winObj);
                }
            }

        # ;cfw <number>
        } else {

            # Check the window object exists, if specified
            if (! $axmud::CLIENT->desktopObj->ivExists('freeWinHash', $number)) {

                return $self->error(
                    $session, $inputString,
                    'The \'free\' window #' . $number . ' doesn\'t exist',
                );

            } else {

                push (@winObjList, $axmud::CLIENT->desktopObj->ivShow('freeWinHash', $number));
            }
        }

        if (! @winObjList) {

            if (! defined $number) {

                return $self->error(
                    $session, $inputString,
                    'No \'free\' windows found',
                );

            } else {

                return $self->error(
                    $session, $inputString,
                    'No \'free\' windows for this session found',
                );
            }
        }

        # Close each window in turn
        foreach my $winObj (@winObjList) {

            $winObj->winDestroy();
        }

        # Operation complete
        if (@winObjList == 1) {

            return $self->complete($session, $standardCmd, 'Closed 1 \'free\' window');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Closed ' . (scalar @winObjList) . ' \'free\' windows',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListFreeWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listfreewindow', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lfw', 'listfree', 'listfreewin', 'listfreewindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists \'free\' windows';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my @winObjList;

        # Check for improper arguments
        if (
            (defined $switch && $switch ne '-s')
            || defined $check
        ) {
            return $self->improper($session, $inputString);
        }

        # Get a list of 'free' window objects, removing any not controlled by this session if that
        #   switch was specified
        foreach my $winObj (
            sort {$a->number <=> $b->number} ($axmud::CLIENT->desktopObj->ivValues('freeWinHash'))
        ) {
            if (! $switch || ($winObj->session && $winObj->session eq $session)) {

                push (@winObjList, $winObj);
            }
        }

        if (! @winObjList) {

            if (! $switch) {

                return $self->error(
                    $session, $inputString,
                    'No \'free\' windows found',
                );

            } else {

                return $self->error(
                    $session, $inputString,
                    'No \'free\' windows for this session found',
                );
            }
        }

        # Display header
        $session->writeText('List of \'free\' windows (+ enabled, * visible)');
        $session->writeText('   Num  Type     Name             Sesn Wksp');

        # Display list
        foreach my $winObj (@winObjList) {

            my ($column, $sessionString, $workspaceString);

            if ($winObj->enabledFlag) {
                $column = '+';
            } else {
                $column = ' ';
            }

            if ($winObj->visibleFlag) {
                $column .= '* ';
            } else {
                $column .= '  ';
            }

            if ($winObj->session) {
                $sessionString = $winObj->session->number;
            } else {
                $sessionString = 'n/a';
            }

            if ($winObj->workspaceObj) {
                $workspaceString = $winObj->workspaceObj->number;
            } else {
                $workspaceString = 'n/a';
            }

            $session->writeText(
                $column . sprintf(
                    '%-4.4s %-8.8s %-16.16s %-4.4s %-4.4s',
                    $winObj->number,
                    $winObj->winType,
                    $winObj->winName,
                    $sessionString,
                    $workspaceString,
                ),
            );
        }

        # Display footer
        if (@winObjList == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 \'free\' window found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . @winObjList . ' \'free\' windows found)',
            );
        }
    }
}

# Textviews

{ package Games::Axmud::Cmd::ScrollLock;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('scrolllock', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['scr', 'lock', 'scrolllock'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles scroll lock mode in the \'main\' window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my ($stripObj, $paneObj);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Scroll lock mode is toggled in whichever of the session's 'main' window pane objects is
        #   the one selected by the entry strip object's switcher button
        $stripObj = $session->mainWin->ivShow('firstStripHash', 'Games::Axmud::Strip::Entry');
        if ($stripObj && $stripObj->paneObjList) {

            $paneObj = $stripObj->ivFirst('paneObjList');
        }

        if (! $paneObj || ! $paneObj->toggleScrollLock()) {

            return $self->error(
                $session, $inputString,
                'General error applying scroll lock mode',
            );

        } else {

            return $self->complete($session, $standardCmd, 'Scroll-lock mode toggled');
        }
    }
}

{ package Games::Axmud::Cmd::SplitScreen;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('splitscreen', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sps', 'screen', 'splitscreen'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles split screen mode in the \'main\' window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my ($stripObj, $paneObj);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Split screen mode is toggled in whichever of the session's 'main' window pane objects is
        #   the one selected by the entry strip object's switcher button
        $stripObj = $session->mainWin->ivShow('firstStripHash', 'Games::Axmud::Strip::Entry');
        if ($stripObj && $stripObj->paneObjList) {

            $paneObj = $stripObj->ivFirst('paneObjList');
        }

        if (! $paneObj || ! $paneObj->toggleSplitScreen()) {

            return $self->error(
                $session, $inputString,
                'General error applying split screen mode',
            );

        } else {

            return $self->complete($session, $standardCmd, 'Split screen mode toggled');
        }
    }
}

{ package Games::Axmud::Cmd::ClearTextView;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('cleartextview', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ctv', 'cls', 'cleartextview'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Removes all text from textview(s)';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $number,
            $check,
        ) = @_;

        # Local variables
        my (
            $thisFlag,
            @list,
        );

        # Check for improper arguments
        if (
            (
                defined $switch && $switch ne '-m' && $switch ne '-s' && $switch ne '-w'
                && $switch ne '-t'
            ) || (
                defined $switch && ($switch eq '-m' || $switch eq '-s') && defined $number
            ) || (
                defined $switch && ($switch eq '-w' || $switch eq '-t') && ! defined $number
            ) || defined $check
        ) {
            return $self->improper($session, $inputString);
        }

        # Compile a list of textview objects which should be cleared
        if (! $switch) {

            push (@list, $session->defaultTabObj->textViewObj);

        } elsif ($switch eq '-m') {

            foreach my $textViewObj ($axmud::CLIENT->desktopObj->ivValues('textViewHash')) {

                if ($textViewObj->winObj && $textViewObj->winObj eq $session->mainWin) {

                    push (@list, $textViewObj);
                }
            }

        } elsif ($switch eq '-s') {

            foreach my $textViewObj ($axmud::CLIENT->desktopObj->ivValues('textViewHash')) {

                if (
                    (
                        $textViewObj->winObj
                        && $textViewObj->winObj->visibleSession
                        && $textViewObj->winObj->visibleSession eq $session
                    ) || (
                        $textViewObj->winObj
                        && $textViewObj->winObj->session
                        && $textViewObj->winObj->session eq $session
                    )
                ) {
                    push (@list, $textViewObj);
                }
            }

        } elsif ($switch eq '-w') {

            foreach my $textViewObj ($axmud::CLIENT->desktopObj->ivValues('textViewHash')) {

                if ($textViewObj->winObj && $textViewObj->winObj->number eq $number) {

                    push (@list, $textViewObj);
                }
            }

        } elsif ($switch eq '-t') {

            if ($axmud::CLIENT->desktopObj->ivExists('textViewHash', $number)) {

                push (@list, $axmud::CLIENT->desktopObj->ivShow('textViewHash', $number));
            }
        }

        if (! @list) {

            return $self->error(
                $session, $inputString,
                'No matching textviews found',
            );

        } else {

            foreach my $textViewObj (@list) {

                $textViewObj->clearBuffer();
                if ($textViewObj eq $session->defaultTabObj->textViewObj) {

                    $thisFlag = TRUE;
                }
            }

            if (! $thisFlag) {

                if (scalar (@list) == 1) {

                    return $self->complete($session, $standardCmd, '1 textview emptied');

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        scalar (@list) . ' textviews emptied',
                    );
                }

            } else {

                # (Don't show a $self->complete message, so that the session's default textview
                #   remains cleared; just return 1)
                return 1;
            }
        }
    }
}

{ package Games::Axmud::Cmd::SetTextView;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('settextview', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['stv', 'settv', 'settextview'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the default size of textviews';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $size,
            $check,
        ) = @_;

        # Local variables
        my $count;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;stv
        if (! defined $size) {

            $axmud::CLIENT->set_customTextBufferSize($axmud::CLIENT->constTextBufferSize);

        # ;stv <size>
        } else {

            if (
                ! $axmud::CLIENT->intCheck(
                    $size,
                    $axmud::CLIENT->constMinBufferSize,
                    $axmud::CLIENT->constMaxBufferSize,
                )
            ) {
                return $self->error(
                    $session, $inputString,
                    'Invalid textview size \'' . $size . '\' (must be a number in the range '
                    . $axmud::CLIENT->constMinBufferSize . '-' . $axmud::CLIENT->constMaxBufferSize
                    . ')'
                );

            } else {

                $axmud::CLIENT->set_customTextBufferSize($size);
            }
        }

        # Update all existing textview objects (the size is applied only to textview which are
        #   using a non-zero size, with zero meaning unlimited size)
        $count = 0;
        foreach my $textViewObj ($axmud::CLIENT->desktopObj->ivValues('textViewHash')) {

            if ($textViewObj->maxLines) {

                $textViewObj->set_maxLines($axmud::CLIENT->customTextBufferSize);
                $count++;
            }
        }

        # Operation complete
        return $self->complete(
            $session, $standardCmd,
            'Textview size set to ' . $axmud::CLIENT->customTextBufferSize . ' (textview objects'
            . ' updated: ' . $count . ')',
        );
    }
}

{ package Games::Axmud::Cmd::ListTextView;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listtextview', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ltv', 'listtv', 'listtextview'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists textview objects';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Display header
        $session->writeText('List of textview objects');
        $session->writeText('   Num  Win  Pane ScrLk SplSc  Colour scheme');

        # Display list
        @list = sort {$a->number <=> $b->number}
                    ($axmud::CLIENT->desktopObj->ivValues('textViewHash'));

        foreach my $textViewObj (@list) {

            my ($scrollString, $colourString);

            if ($textViewObj->scrollLockFlag) {
                $scrollString = 'on';
            } else {
                $scrollString = 'off';
            }

            if ($textViewObj->colourScheme) {
                $colourString = $textViewObj->colourScheme;
            } else {
                $colourString = 'n/a';
            }

            $session->writeText(
                '   ' . sprintf(
                    '%-4.4s %-4.4s %-4.4s %-5.5s %-5.5s ',
                    $textViewObj->number,
                    $textViewObj->winObj->number,
                    $textViewObj->paneObj->number,
                    $scrollString,
                    $textViewObj->splitScreenMode,
                ) . $colourString,
            );
        }

        # Display footer
        if (scalar (@list) == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 textview object found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar (@list) . ' textview objects found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::FindText;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('findtext', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['find', 'findtext'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Finds matching text in a textview';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $tvObjNum, $tvObjFlag, $prevFlag, $nextFlag, $caseFlag, $splitFlag, $regex,
            $tvObj, $line, $offset, $length,
        );

        # Extract switches
        ($switch, $tvObjNum, @args) = $self->extract('-t', 1, @args);
        if ($switch) {

            $tvObjFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-p', 0, @args);
        if ($switch) {

            $prevFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-n', 0, @args);
        if ($switch) {

            $nextFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-c', 0, @args);
        if ($switch) {

            $caseFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-s', 0, @args);
        if ($switch) {

            $splitFlag = TRUE;
        }

        # @args should now containg a single argument
        $regex = shift @args;
        if (! defined $regex || @args) {

            return $self->improper($session, $inputString);
        }

        # Some switches can't be combined
        if ($prevFlag && $nextFlag) {

            return $self->error(
                $session, $inputString,
                'The switches \'-p\' and \'-n\' can\'t be combined',
            );
        }

        # Get the textview object whose buffer should be searched. If none was specified, use the
        #   session's default tab
        if ($tvObjFlag) {

            $tvObj = $axmud::CLIENT->desktopObj->ivShow('textViewHash', $tvObjNum);
            if (! $tvObj) {

                return $self->error(
                    $session, $inputString,
                    'There is no textview object numbered \'' . $tvObjNum . '\'',
                );
            }

        } else {

            $tvObj = $session->defaultTabObj->textViewObj;
        }

        # Perform the search
        ($line, $offset, $length) = $tvObj->searchBuffer($regex, $nextFlag, TRUE, $caseFlag);
        if (! defined $line) {

            return $self->complete(
                $session, $standardCmd,
                'No matching text found in textview #' . $tvObj->number,
            );

        } elsif ($tvObj ne $session->defaultTabObj->textViewObj) {

            return $self->complete(
                $session, $standardCmd,
                'Match found at line ' . $line . ', offset ' . $offset . ', length: ' . $length,
            );

        } else {

            # Don't show a confirmation if a match is found in the session's default textview (as it
            #   makes the textview scroll)
            return 1;
        }
    }
}

{ package Games::Axmud::Cmd::FindReset;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('findreset', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['frt', 'findreset'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets the last find operation';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $tvObjNum,
            $check,
        ) = @_;

        # Local variables
        my $tvObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get the textview object whose search mark should be reset. If none was specified, use the
        #   session's default tab
        if (defined $tvObjNum) {

            $tvObj = $axmud::CLIENT->desktopObj->ivShow('textViewHash', $tvObjNum);
            if (! $tvObj) {

                return $self->error(
                    $session, $inputString,
                    'There is no textview object numbered \'' . $tvObjNum . '\'',
                );
            }

        } else {

            $tvObj = $session->defaultTabObj->textViewObj;
        }

        # Reset the textview's search mark
        $tvObj->setSearchMark();
        # Un-select any selected text
        $tvObj->unselectText();

        if ($tvObj ne $session->defaultTabObj->textViewObj) {

            return $self->complete(
                $session, $standardCmd,
                'The search position in textview #' . $tvObj->number . ' has been reset',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'The search position in the session\'s default textview has been reset',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ConvertText;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('converttext', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cvt', 'cvtext', 'converttext'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles conversion of invisible text';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (! $axmud::CLIENT->convertInvisibleFlag) {

            $axmud::CLIENT->set_convertInvisibleFlag(TRUE);

            return $self->complete(
                $session, $standardCmd,
                'Conversion of invisible text turned on',
            );

        } else {

            $axmud::CLIENT->set_convertInvisibleFlag(FALSE);

            return $self->complete(
                $session, $standardCmd,
                'Conversion of invisible text turned off',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetColour;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setcolour', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sco', 'setcol', 'setcolor', 'setcolour'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets ' . $axmud::SCRIPT . ' standard colour tags';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $tag, $new,
            $check,
        ) = @_;

        # Local variables
        my ($tagType, $underlayFlag, $newType, $newUnderlayFlag);

        # Check for improper arguments
        if (! defined $tag || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check $tag is a standard colour tag
        ($tagType, $underlayFlag) = $axmud::CLIENT->checkColourTags($tag);
        if (! $tagType) {

            return $self->error(
                $session, $inputString,
                'Unrecognised colour tag \'' . $tag . '\'',
            );

        } elsif ($tagType ne 'standard' || $underlayFlag) {

            return $self->error(
                $session, $inputString,
                '\'' . $tag . '\' is not a standard colour tag (e.g. \'red\', \'BLUE\')',
            );
        }

        # Check $new is a recognised colour tag, if specified
        if (defined $new) {

            ($newType, $newUnderlayFlag) = $axmud::CLIENT->checkColourTags($new);
            if (! $newType) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised colour tag \'' . $new . '\'',
                );
            }
        }

        # ;sco <tag> <colour>
        if (defined $new) {

            # If <colour> is an underlay tag, convert it into a text tag
            if ($newUnderlayFlag) {

                $new = $axmud::CLIENT->swapColours($new);
                ($newType, $newUnderlayFlag) = $axmud::CLIENT->checkColourTags($new);
            }

        # ;sco <tag>
        } else {

            if (! $axmud::CLIENT->checkBoldTags($tag)) {
                $new = $axmud::CLIENT->ivShow('constColourTagHash', $tag);
            } else {
                $new = $axmud::CLIENT->ivShow('constBoldColourTagHash', $tag);
            }
        }

        # Convert <colour> to an RGB tag (if it's not already an RGB tag) and update the IV
        $new = $axmud::CLIENT->returnRGBColour($new);

        if (! $axmud::CLIENT->checkBoldTags($tag)) {
            $axmud::CLIENT->set_standardColourTag($tag, $new, FALSE);
        } else {
            $axmud::CLIENT->set_standardColourTag($tag, $new, TRUE);
        }

        # Update textview objects
        foreach my $textViewObj ($axmud::CLIENT->desktopObj->ivValues('textViewHash')) {

            my $tabObj;

            # Update the textview object's Gtk3::TextTag
            $textViewObj->updateStandardTag($tag);

            # Update any textview object in monochrome mode
            if (
                $textViewObj->monochromeFlag
                && ($textViewObj->textColour eq $tag || $textViewObj->backgroundColour eq $tag)
            ) {
                $tabObj = $textViewObj->paneObj->findTextView($textViewObj);
                if ($tabObj) {

                    $textViewObj->paneObj->applyMonochrome(
                        $tabObj,
                        $textViewObj->backgroundColour,
                        $textViewObj->textColour,
                    );
                }
            }
        }

        # Update any colour scheme using the standard colour tag (doesn't affect any textview
        #   objects in monochrome mode)
        foreach my $schemeObj ($axmud::CLIENT->ivValues('colourSchemeHash')) {

            if (
                $schemeObj->textColour eq $tag
                || $schemeObj->underlayColour eq $axmud::CLIENT->swapColours($tag)
                || $schemeObj->backgroundColour eq $tag
            ) {
                $session->pseudoCmd('updatecolourscheme ' . $schemeObj->name, 'hide_complete');
            }
        }

        return $self->complete(
            $session, $standardCmd,
            'Standard colour tag \'' . $tag . '\' set to ' . $new,
        );
    }
}

{ package Games::Axmud::Cmd::ListColour;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listcolour', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lco', 'listcol', 'listcolor', 'listcolour'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists ' . $axmud::SCRIPT . ' standard colour tags';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $textViewObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the session's default textview object (for convenience)
        $textViewObj = $session->defaultTabObj->textViewObj;

        # Display header
        $session->writeText('List of ' . $axmud::SCRIPT . ' standard (text) colour tags');
        $session->writeText('   Tag      RGB      Example  Default  Example');

        # Display list
        foreach my $tag ($axmud::CLIENT->constColourTagList) {

            $self->showLine(
                $textViewObj,
                $tag,
                $axmud::CLIENT->ivShow('colourTagHash', $tag),
                $axmud::CLIENT->ivShow('constColourTagHash', $tag),
            );
        }

        foreach my $tag ($axmud::CLIENT->constBoldColourTagList) {

            $self->showLine(
                $textViewObj,
                $tag,
                $axmud::CLIENT->ivShow('boldColourTagHash', $tag),
                $axmud::CLIENT->ivShow('constBoldColourTagHash', $tag),
            );
        }

        # Display header
        return $self->complete(
            $session, $standardCmd,
            'End of list (8 standard colour tags found)',
        );
    }

    # Methods

    sub showLine {

        # Called by $self->do
        # Shows a single line in the list
        #
        # Expected arguments
        #   $textViewObj    - The textview object for the session's default tab
        #   $tag            - An Axmud standard colour tag, e.g. 'red'
        #   $rgbTag         - The equivalent RGB colour tag, e.g. '#8B0000'
        #   $rgbDefault     - The default RGB colour for this tag
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $textViewObj, $tag, $rgbTag, $rgbDefault, $check) = @_;

        # Check for improper arguments
        if (
            ! defined $textViewObj || ! defined $tag || ! defined $rgbTag
            || ! defined $rgbDefault || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->showLine', @_);
        }

        $textViewObj->insertText(sprintf('   %-8.8s ', $tag), 'echo');

        $textViewObj->insertText(
            sprintf('%-8.8s ', $rgbTag),
            'echo',
        );

        $textViewObj->insertText('Hello    ', $rgbTag, 'echo');

        $textViewObj->insertText(
            sprintf('%-8.8s ', $rgbDefault),
            'echo',
        );

        $textViewObj->insertText('Hello    ', $rgbDefault, 'after');

        return 1;
    }
}

{ package Games::Axmud::Cmd::SetSystemColour;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setsystemcolour', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ssy', 'setsyscol', 'setsystemcolor', 'setsystemcolour'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets colours of ' . $axmud::SCRIPT . ' system messages';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $colour,
            $check,
        ) = @_;

        # Local variables
        my ($type, $underlayFlag);

        # Check for improper arguments
        if (
            ! defined $switch
            || (
                $switch ne '-c' && $switch ne '-m' && $switch ne '-e' && $switch ne '-w'
                && $switch ne '-d' && $switch ne '-i'
            ) || defined $check
        ) {
            return $self->improper($session, $inputString);
        }

        # If $colour is specified, it must be a standard colour tag
        if (defined $colour) {

            ($type, $underlayFlag) = $axmud::CLIENT->checkColourTags($colour);
            if (! $type) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised colour tag \'' . $colour . '\'',
                );

            } elsif ($type ne 'standard') {

                return $self->error(
                    $session, $inputString,
                    'For system messages, you must specify ' . $axmud::NAME_ARTICLE
                    . ' standard colour tag',
                );
            }
        }

        # ;ssy -c <colour>
        # ;ssy -c
        if ($switch eq '-c') {

            if ($colour) {
                $axmud::CLIENT->set_customInsertCmdColour($colour);
            } else {
                $axmud::CLIENT->set_customInsertCmdColour($axmud::CLIENT->constInsertCmdColour);
            }

            return $self->complete(
                $session, $standardCmd,
                'System world command colour set to \'' . $axmud::CLIENT->customInsertCmdColour
                . '\'',
            );

        # ;ssy -m <colour>
        # ;ssy -m
        } elsif ($switch eq '-m') {

            if ($colour) {

                $axmud::CLIENT->set_customShowSystemTextColour($colour);

            } else {

                $axmud::CLIENT->set_customShowSystemTextColour(
                    $axmud::CLIENT->constShowSystemTextColour,
                );
            }

            return $self->complete(
                $session, $standardCmd,
                'System message colour set to \'' . $axmud::CLIENT->customShowSystemTextColour
                . '\'',
            );

        # ;ssy -e <colour>
        # ;ssy -e
        } elsif ($switch eq '-e') {

            if ($colour) {
                $axmud::CLIENT->set_customShowErrorColour($colour);
            } else {
                $axmud::CLIENT->set_customShowErrorColour($axmud::CLIENT->constShowErrorColour);
            }

            return $self->complete(
                $session, $standardCmd,
                'System error message colour set to \'' . $axmud::CLIENT->customShowErrorColour
                . '\'',
            );

        # ;ssy -w <colour>
        # ;ssy -w
        } elsif ($switch eq '-w') {

            if ($colour) {
                $axmud::CLIENT->set_customShowWarningColour($colour);
            } else {
                $axmud::CLIENT->set_customShowWarningColour($axmud::CLIENT->constShowWarningColour);
            }

            return $self->complete(
                $session, $standardCmd,
                'System warning message colour set to \'' . $axmud::CLIENT->customShowWarningColour
                . '\'',
            );

        # ;ssy -d <colour>
        # ;ssy -d
        } elsif ($switch eq '-d') {

            if ($colour) {
                $axmud::CLIENT->set_customShowDebugColour($colour);
            } else {
                $axmud::CLIENT->set_customShowDebugColour($axmud::CLIENT->constShowDebugColour);
            }

            return $self->complete(
                $session, $standardCmd,
                'System debug colour set to \'' . $axmud::CLIENT->customShowDebugColour
                . '\'',
            );

        # ;ssy -i <colour>
        # ;ssy -i
        } elsif ($switch eq '-i') {

            if ($colour) {

                $axmud::CLIENT->set_customShowImproperColour($colour);

            } else {

                $axmud::CLIENT->set_customShowImproperColour(
                    $axmud::CLIENT->constShowImproperColour,
                );
            }

            return $self->complete(
                $session, $standardCmd,
                'System improper arguments message colour set to \''
                . $axmud::CLIENT->customShowImproperColour . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListSystemColour;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listsystemcolour', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lsy', 'listsyscol', 'listsystemcolor', 'listsystemcolour'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists colours of ' . $axmud::SCRIPT . ' system messages';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $textViewObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the session's default textview object (for convenience)
        $textViewObj = $session->defaultTabObj->textViewObj;

        # Display header
        $session->writeText('List of ' . $axmud::SCRIPT . ' standard (text) colour tags');
        $session->writeText(
            '   Type             Tag      RGB      Example  Default  RGB      Example',
        );

        # Display list
        $self->showLine(
            $textViewObj,
            'World commands',
            $axmud::CLIENT->customInsertCmdColour,
            $axmud::CLIENT->constInsertCmdColour,
        );

        $self->showLine(
            $textViewObj,
            'System messages',
            $axmud::CLIENT->customShowSystemTextColour,
            $axmud::CLIENT->constShowSystemTextColour,
        );

        $self->showLine(
            $textViewObj,
            'Error messages',
            $axmud::CLIENT->customShowErrorColour,
            $axmud::CLIENT->constShowErrorColour,
        );

        $self->showLine(
            $textViewObj,
            'Warning messages',
            $axmud::CLIENT->customShowWarningColour,
            $axmud::CLIENT->constShowWarningColour,
        );

        $self->showLine(
            $textViewObj,
            'Debug messages',
            $axmud::CLIENT->customShowDebugColour,
            $axmud::CLIENT->constShowDebugColour,
        );

        $self->showLine(
            $textViewObj,
            'Improper args',
            $axmud::CLIENT->customShowImproperColour,
            $axmud::CLIENT->constShowImproperColour,
        );

        # Display header
        return $self->complete(
            $session, $standardCmd,
            'End of list (6 system colours found)',
        );
    }

    # Methods

    sub showLine {

        # Called by $self->do
        # Shows a single line in the list
        #
        # Expected arguments
        #   $textViewObj    - The textview object for the session's default tab
        #   $string         - String describing the system colour, e.g. 'Error messages'
        #   $tag            - The system colour (can be any Axmud colour tag)
        #   $defaultTag     - The corresponding default system colour (ditto)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $textViewObj, $string, $tag, $defaultTag, $check) = @_;

        # Local variables
        my ($rgbTag, $rgbDefault);

        # Check for improper arguments
        if (
            ! defined $textViewObj || ! defined $string || ! defined $tag || ! defined $defaultTag
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->showLine', @_);
        }

        $rgbTag = $axmud::CLIENT->returnRGBColour($tag);
        $rgbDefault = $axmud::CLIENT->returnRGBColour($defaultTag);

        $textViewObj->insertText(
            sprintf(
                '   %-16.16s %-8.8s %-8.8s ',
                $string,
                $tag,
                $rgbTag,
            ),
            'echo',
        );

        $textViewObj->insertText('Hello    ', $rgbTag, 'echo');

        $textViewObj->insertText(
            sprintf('%-8.8s %-8.8s ', $defaultTag, $rgbDefault),
            'echo',
        );

        $textViewObj->insertText('Hello    ', $rgbDefault, 'after');

        return 1;
    }
}

{ package Games::Axmud::Cmd::SetXTerm;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setxterm', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sxt', 'setxterm'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the colour cube used for xterm-256 colours';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (! defined $switch) {

            return $self->complete(
                $session, $standardCmd,
                'Current xterm-256 colour cube: ' . $axmud::CLIENT->currentColourCube,
            );

        } elsif ($switch ne '-x' && $switch ne '-n') {

            return $self->error(
                $session, $inputString,
                'Invalid switch (try \'-x\' for the xterm cube, or \'-n\' for the netscape cube',
            );

        } else {

            if ($switch eq '-x') {

                if ($axmud::CLIENT->currentColourCube eq 'xterm') {

                    return $self->error(
                        $session, $inputString,
                        'The xterm-256 colour cube in use is already \'xterm\'',
                    );

                } else {

                    $axmud::CLIENT->set_currentColourCube('xterm');
                }

            } elsif ($switch eq '-n') {

                if ($axmud::CLIENT->currentColourCube eq 'netscape') {

                    return $self->error(
                        $session, $inputString,
                        'The xterm-256 colour cube in use is already \'netscape\'',
                    );

                } else {

                    $axmud::CLIENT->set_currentColourCube('netscape');
                }
            }

            # Update xterm colour tags in all textviews
            foreach my $textViewObj ($axmud::CLIENT->desktopObj->ivValues('textViewHash')) {

                $textViewObj->updateXTermTags();
            }

            # Update colour schemes in all 'internal' windows
            foreach my $winObj ($axmud::CLIENT->desktopObj->listGridWins('internal')) {

                $winObj->updateColourScheme();
            }

            return $self->complete(
                $session, $standardCmd,
                'The xterm-256 colour cube has been set to \'' . $axmud::CLIENT->currentColourCube
                . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::TogglePalette;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('togglepalette', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tgp', 'togglepalette'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles use of the OSC colour palette';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if ($axmud::CLIENT->oscPaletteFlag) {

            $axmud::CLIENT->set_oscPaletteFlag(FALSE);

            return $self->complete(
                $session, $standardCmd,
                'Use of OSC colour palette disabled',
            );

        } else {

            $axmud::CLIENT->set_oscPaletteFlag(TRUE);

            return $self->complete(
                $session, $standardCmd,
                'Use of OSC colour palette enabled',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddColourScheme;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addcolourscheme', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['acs', 'addscheme', 'addcolorscheme', 'addcolourscheme'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a colour scheme';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $text, $string, $underlay, $background, $font, $fontSize, $wrapMode,
            $schemeObj, $type, $underlayFlag,
        );

        # Extract switches
        ($switch, $text, @args) = $self->extract('-t', 1, @args);
        if ($switch && ! defined $text) {

            $string = 'text colour';
        }

        ($switch, $underlay, @args) = $self->extract('-u', 1, @args);
        if ($switch && ! defined $underlay && ! $string) {

            $string = 'underlay colour';
        }

        ($switch, $background, @args) = $self->extract('-b', 1, @args);
        if ($switch && ! defined $background && ! $string) {

            $string = 'background colour';
        }

        ($switch, $font, @args) = $self->extract('-f', 1, @args);
        if ($switch && ! defined $font && ! $string) {

            $string = 'font';
        }

        ($switch, $fontSize, @args) = $self->extract('-s', 1, @args);
        if ($switch && ! defined $fontSize && ! $string) {

            $string = 'font size';
        }

        # (Take the first wrap mode switch specified, and ignore any subsequent ones)
        ($switch, @args) = $self->extract('-n', 0, @args);
        if ($switch && ! $wrapMode) {

            $wrapMode = 'no_wrap';
        }

        ($switch, @args) = $self->extract('-c', 0, @args);
        if ($switch && ! $wrapMode) {

            $wrapMode = 'wrap_char';
        }

        ($switch, @args) = $self->extract('-w', 0, @args);
        if ($switch && ! $wrapMode) {

            $wrapMode = 'wrap_word';
        }

        ($switch, @args) = $self->extract('-e', 0, @args);
        if ($switch && ! $wrapMode) {

            $wrapMode = 'wrap_word_char';
        }

        # @args should now be empty
        if (! defined $name || @args) {

            return $self->improper($session, $inputString);
        }

        # Check the colour scheme doesn't already exist
        if ($axmud::CLIENT->ivExists('colourSchemeHash', $name)) {

            return $self->error(
                $session, $inputString,
                'The colour scheme \'' . $name . '\' already exists',
            );

        # Check the name is valid
        } elsif (! $axmud::CLIENT->nameCheck($name, 16)) {

            return $self->error($session, $inputString, 'Illegal name \'' . $name . '\'');

        # Check for invalid switch patterns
        } elsif ($string) {

            return $self->error(
                $session, $inputString,
                'Add colour scheme with which ' . $string . '?',
            );
        }

        # Create the colour scheme object
        $schemeObj = Games::Axmud::Obj::ColourScheme->new($name);
        if (! $schemeObj) {

            return $self->error(
                $session, $inputString,
                'Unable to create colour scheme \'' . $name . '\'',
            );
        }

        # Apply switches, if any were specified
        if (defined $text) {

            ($type, $underlayFlag) = $axmud::CLIENT->checkColourTags($text);
            if (! $type) {

                return $self->error(
                    $session, $inputString,
                    'Invalid colour tag \'' . $text . '\'',
                );

            } elsif ($underlayFlag) {

                return $self->error(
                    $session, $inputString,
                    'Underlay tags like \'' . $text . '\' can\'t be used as the text colour',
                );

            } else {

                $schemeObj->ivPoke('textColour', $text);
            }
        }

        if (defined $underlay) {

            ($type, $underlayFlag) = $axmud::CLIENT->checkColourTags($underlay);
            if (! $type) {

                return $self->error(
                    $session, $inputString,
                    'Invalid colour tag \'' . $underlay . '\'',
                );

            } elsif (! $underlayFlag) {

                return $self->error(
                    $session, $inputString,
                    'Colour tag \'' . $underlay . '\' is not an underlay colour',
                );

            } else {

                $schemeObj->ivPoke('underlayColour', $underlay);
            }
        }

        if (defined $background) {

            ($type, $underlayFlag) = $axmud::CLIENT->checkColourTags($background);
            if (! $type) {

                return $self->error(
                    $session, $inputString,
                    'Invalid colour tag \'' . $background . '\'',
                );

            } elsif ($underlayFlag) {

                return $self->error(
                    $session, $inputString,
                    'Underlay tags like \'' . $background . '\' can\'t be used as the background'
                    . ' colour',
                );

            } else {

                $schemeObj->ivPoke('backgroundColour', $background);
            }
        }

        if (defined $font) {

            $schemeObj->ivPoke('font', $font);
        }

        if (defined $fontSize) {

            if (! $axmud::CLIENT->intCheck($fontSize, 0)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid font size \'' . $fontSize . '\'',
                );

            } else {

                $schemeObj->ivPoke('fontSize', $fontSize);
            }
        }

        if (defined $wrapMode) {

            $schemeObj->ivPoke('wrapMode', $wrapMode);
        }

        # Update IVs
        $axmud::CLIENT->add_colourScheme($schemeObj);

        return $self->complete($session, $standardCmd, 'Added colour scheme \'' . $name . '\'');
    }
}

{ package Games::Axmud::Cmd::EditColourScheme;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editcolourscheme', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ecs', 'editscheme', 'editcolorscheme', 'editcolourscheme'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a colour scheme';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the colour scheme exists
        $obj = $axmud::CLIENT->ivShow('colourSchemeHash', $name);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'The colour scheme \'' . $name . '\' doesn\'t exist',
            );
        }

        # Open an 'edit' window for the colour scheme
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::ColourScheme',
                $session->mainWin,
                $session,
                'Edit colour scheme \'' . $name . '\'',
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit colour scheme \'' . $name . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for colour scheme \'' . $name . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ModifyColourScheme;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('modifycolourscheme', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'mcs', 'modscheme', 'modifycolorscheme', 'modifycolourscheme',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies a colour scheme';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $text, $textFlag, $flagCount, $underlay, $underlayFlag, $background,
            $backgroundFlag, $font, $fontFlag, $fontSize, $fontSizeFlag, $monoFlag, $oldTag,
            $newTag, $addFlag, $deleteFlag, $schemeObj, $type, $tagUnderlayFlag, $msg, $wrapMode,
        );

        # Extract switches
        $flagCount = 0;

        ($switch, $text, @args) = $self->extract('-t', 1, @args);
        if ($switch) {

            $textFlag = TRUE;
            $flagCount++;
        }

        ($switch, $underlay, @args) = $self->extract('-u', 1, @args);
        if ($switch) {

            $underlayFlag = TRUE;
            $flagCount++;
        }

        ($switch, $background, @args) = $self->extract('-b', 1, @args);
        if ($switch) {

            $backgroundFlag = TRUE;
            $flagCount++;
        }

        ($switch, $font, @args) = $self->extract('-f', 1, @args);
        if ($switch) {

            $fontFlag = TRUE;
            $flagCount++;
        }

        ($switch, $fontSize, @args) = $self->extract('-s', 1, @args);
        if ($switch) {

            $fontSizeFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-m', 0, @args);
        if ($switch) {

            $monoFlag = TRUE;
            $flagCount++;
        }

        ($switch, $oldTag, $newTag, @args) = $self->extract('-a', 2, @args);
        if ($switch) {

            $addFlag = TRUE;
            $flagCount++;
        }

        ($switch, $oldTag, @args) = $self->extract('-d', 1, @args);
        if ($switch) {

            $deleteFlag = TRUE;
            $flagCount++;
        }

        # (Take the first wrap mode switch specified, and ignore any subsequent ones)
        ($switch, @args) = $self->extract('-n', 0, @args);
        if ($switch && ! $wrapMode) {

            $wrapMode = 'no_wrap';
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-c', 0, @args);
        if ($switch && ! $wrapMode) {

            $wrapMode = 'wrap_char';
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-w', 0, @args);
        if ($switch && ! $wrapMode) {

            $wrapMode = 'wrap_word';
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-e', 0, @args);
        if ($switch && ! $wrapMode) {

            $wrapMode = 'wrap_word_char';
            $flagCount++;
        }

        # @args should now be empty
        if (! defined $name || ! $flagCount || @args) {

            return $self->improper($session, $inputString);
        }

        # Check the colour scheme exists
        $schemeObj = $axmud::CLIENT->ivShow('colourSchemeHash', $name);
        if (! $schemeObj) {

            return $self->error(
                $session, $inputString,
                'The colour scheme \'' . $name . '\' doesn\'t exist',
            );
        }

        # Check the validity of any colour tags and font sizes specified, before making any changes
        if ($textFlag && defined $text) {

            ($type, $tagUnderlayFlag) = $axmud::CLIENT->checkColourTags($text);
            if (! $type) {

                return $self->error(
                    $session, $inputString,
                    'Invalid colour tag \'' . $text . '\'',
                );

            } elsif ($tagUnderlayFlag) {

                return $self->error(
                    $session, $inputString,
                    'Underlay tags like \'' . $text . '\' can\'t be used as the text colour',
                );
            }
        }

        if ($underlayFlag && defined $underlay) {

            ($type, $tagUnderlayFlag) = $axmud::CLIENT->checkColourTags($underlay);
            if (! $type) {

                return $self->error(
                    $session, $inputString,
                    'Invalid colour tag \'' . $underlay . '\'',
                );

            } elsif (! $tagUnderlayFlag) {

                return $self->error(
                    $session, $inputString,
                    'Colour tag \'' . $underlay . '\' is not an underlay colour',
                );
            }
        }

        if ($backgroundFlag && defined $background) {

            ($type, $tagUnderlayFlag) = $axmud::CLIENT->checkColourTags($background);
            if (! $type) {

                return $self->error(
                    $session, $inputString,
                    'Invalid colour tag \'' . $background . '\'',
                );

            } elsif ($tagUnderlayFlag) {

                return $self->error(
                    $session, $inputString,
                    'Underlay tags like \'' . $background . '\' can\'t be used as the'
                    . ' background colour',
                );
            }
        }

        if ($fontSizeFlag && defined $fontSize && ! $axmud::CLIENT->intCheck($fontSize, 0)) {

            return $self->error(
                $session, $inputString,
                'Invalid font size \'' . $fontSize . '\'',
            );
        }

        if (($addFlag || $deleteFlag) && defined $oldTag) {

            ($type) = $axmud::CLIENT->checkColourTags($oldTag);
            if (! $type) {

                return $self->error(
                    $session, $inputString,
                    'Invalid colour tag \'' . $oldTag . '\'',
                );

            } elsif ($deleteFlag && ! $schemeObj->ivExists('overrideHash', $oldTag)) {

                return $self->error(
                    $session, $inputString,
                    'The colour tag \'' . $oldTag . '\' isnt\'t one of those specified as an'
                    . ' override in the colour scheme',
                );
            }
        }

        if ($addFlag && defined $newTag) {

            ($type) = $axmud::CLIENT->checkColourTags($newTag);
            if (! $type) {

                return $self->error(
                    $session, $inputString,
                    'Invalid colour tag \'' . $newTag . '\'',
                );
            }
        }

        # Apply switches
        if ($textFlag) {

            if (defined $text) {
                $schemeObj->ivPoke('textColour', $text);
            } else {
                $schemeObj->ivPoke('textColour', $axmud::CLIENT->constTextColour);
            }
        }

        if ($underlayFlag) {

            if (defined $underlay) {
                $schemeObj->ivPoke('underlayColour', $underlay);
            } else {
                $schemeObj->ivPoke('underlayColour', $axmud::CLIENT->constUnderlayColour);
            }
        }

        if ($backgroundFlag) {

            if (defined $background) {
                $schemeObj->ivPoke('backgroundColour', $background);
            } else {
                $schemeObj->ivPoke('backgroundColour', $axmud::CLIENT->constBackgroundColour);
            }
        }

        if ($fontFlag) {

            if (defined $font) {
                $schemeObj->ivPoke('font', $font);
            } else {
                $schemeObj->ivPoke('font', $axmud::CLIENT->constFont);
            }
        }

        if ($fontSizeFlag) {

            if (defined $fontSize) {
                $schemeObj->ivPoke('fontSize', $fontSize);
            } else {
                $schemeObj->ivPoke('fontSize', $axmud::CLIENT->constFontSize);
            }
        }

        if ($monoFlag) {

            if (! $schemeObj->overrideAllFlag) {

                $schemeObj->ivPoke('overrideAllFlag', TRUE);
                $msg = ', setting \'ignore all colour tags\' mode ON';

            } else {

                $schemeObj->ivPoke('overrideAllFlag', FALSE);
                $msg = ', setting \'ignore all colour tags\' mode OFF';
            }

        } else {

            $msg = '';
        }

        if ($addFlag) {

            $schemeObj->ivAdd('overrideHash', $oldTag, $newTag);
        }

        if ($deleteFlag) {

            $schemeObj->ivDelete('overrideHash', $oldTag);
        }

        if ($wrapMode) {

            $schemeObj->ivPoke('wrapMode', $wrapMode);
        }

        # Resolve any problems now, before they have a chance to create a Perl error
        $schemeObj->repair();

        # Operation complete
        return $self->complete(
            $session, $standardCmd,
            'Modified colour scheme \'' . $name . '\'' . $msg,
        );
    }

    ##################
    # Methods
}

{ package Games::Axmud::Cmd::UpdateColourScheme;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('updatecolourscheme', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'ucs', 'updatescheme', 'updatecolorscheme', 'updatecolourscheme',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Updates \'internal\' windows using a colour scheme';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the colour scheme exists
        $obj = $axmud::CLIENT->ivShow('colourSchemeHash', $name);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'The colour scheme \'' . $name . '\' doesn\'t exist',
            );
        }

        # Check the colour scheme's colour tags are valid, before trying to apply them to 'interal'
        #   windows (e.g. don't use an underlay colour tag like 'ul_black' for text)
        $obj->repair();

        # Update textviews in any 'internal' window using this colour scheme
        foreach my $winObj ($axmud::CLIENT->desktopObj->listGridWins('internal')) {

            $winObj->updateColourScheme($name);
        }

        # Operation complete
        return $self->complete(
            $session, $standardCmd,
            'All \'internal\' windows using the colour scheme \'' . $name . '\' updated',
        );
    }

    ##################
    # Methods
}

{ package Games::Axmud::Cmd::ApplyColourScheme;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('applycolourscheme', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'apl', 'applyscheme', 'applycolorscheme', 'applycolourscheme',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Applies a colour scheme to \'internal\' windows';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $mainFlag, $protocolFlag, $customFlag, $sessionFlag, $winNum, $schemeObj,
            @winList,
            %checkHash,
        );

        # Extract switches
        ($switch, @args) = $self->extract('-m', 0, @args);
        if ($switch) {

            $mainFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-p', 0, @args);
        if ($switch) {

            $protocolFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-c', 0, @args);
        if ($switch) {

            $customFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-s', 0, @args);
        if ($switch) {

            $sessionFlag = TRUE;
        }

        ($switch, $winNum, @args) = $self->extract('-w', 1, @args);
        if ($switch) {

            # Check the 'grid' window exists
            my $winObj = $axmud::CLIENT->ivShow('gridcheckHash', $winNum);
            if (! $winObj) {

                return $self->error(
                    $session, $inputString,
                    '\'Grid\' window #' . $winNum . ' doesn\'t exist',
                );

            } elsif (
                $winObj->winType ne 'main'
                && $winObj->winType ne 'protocol'
                && $winObj->winType ne 'fixed'
            ) {
                return $self->error(
                    $session, $inputString,
                    '\'Grid\' window #' . $winNum . ' isn\'t an \'internal\' window',
                );

            # Don't add duplicates to this list of window objects to which the colour scheme
            #   should be applied
            } elsif (! exists $checkHash{$winNum}) {

                push (@winList, $winObj);
            }
        }

        # @args should now be empty
        if (! defined $name || @args) {

            return $self->improper($session, $inputString);
        }

        # Check the colour scheme exists
        $schemeObj = $axmud::CLIENT->ivShow('colourSchemeHash', $name);
        if (! $schemeObj) {

            return $self->error(
                $session, $inputString,
                'The colour scheme \'' . $name . '\' doesn\'t exist',
            );
        }

        # Get a list of 'internal' windows to which the colour scheme should be applied
        foreach my $winObj ($axmud::CLIENT->desktopObj->listGridWins('internal')) {

            if (
                ! exists $checkHash{$winObj->number}
                && (! $mainFlag || $winObj->winType eq 'main')
                && (! $protocolFlag || $winObj->winType eq 'protocol')
                && (! $customFlag || $winObj->winType eq 'custom')
                && (
                    ! $sessionFlag
                    || (
                        $winObj->winType eq 'main'
                        && (
                            $axmud::CLIENT->shareMainWinFlag
                            || (
                                $winObj->visibleSession
                                && $winObj->visibleSession eq $session
                            )
                        )
                    ) || ($winObj->winType ne 'main' && $winObj->session eq $session)
                )
            ) {
                push (@winList, $winObj);
            }
        }

        # Apply the colour scheme
        foreach my $winObj (@winList) {

            $winObj->applyColourScheme($name);
        }

        if (scalar @winList == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Colour scheme \'' . $name . '\' applied across 1 \'internal\' window',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Colour scheme \'' . $name . '\' applied across ' . (scalar @winList)
                . ' \'internal\' windows',
            );
        }
    }

    ##################
    # Methods
}

{ package Games::Axmud::Cmd::DeleteColourScheme;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletecolourscheme', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'dcs', 'delscheme', 'deletecolorscheme', 'deletecolourscheme',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a colour scheme';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the colour scheme exists
        if (! $axmud::CLIENT->ivExists('colourSchemeHash', $name)) {

            return $self->error(
                $session, $inputString,
                'The colour scheme \'' . $name . '\' doesn\'t exist',
            );

        # Check it's not a standard colour scheme
        } elsif (
            $axmud::CLIENT->ivExists('constGridWinTypeHash', $name)
            || $axmud::CLIENT->ivExists('constFreeWinTypeHash', $name)
        ) {

            return $self->error(
                $session, $inputString,
                'The standard colour scheme \'' . $name . '\' can\'t be deleted',
            );
        }

        # Update IVs
        $axmud::CLIENT->ivDelete('colourSchemeHash', $name);

        return $self->complete(
            $session, $standardCmd,
            'Colour scheme \'' . $name . '\' deleted',
        );
    }
}

{ package Games::Axmud::Cmd::ListColourScheme;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listcolourscheme', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lcs', 'listscheme', 'listcolorscheme', 'listcolourscheme'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists colour schemes';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If a name was specified, check it exists
        if (defined $name && ! $axmud::CLIENT->ivExists('colourSchemeHash', $name)) {

            return $self->error(
                $session, $inputString,
                'The colour scheme \'' . $name . '\' doesn\'t exist',
            );
        }

        # Display header
        $session->writeText(
            'List of colour schemes (* - standard, # - ignores all other colour tags)',
        );

        # Display list
        if (defined $name) {

            push (@list, $axmud::CLIENT->ivShow('colourSchemeHash', $name));

        } else {

            push (@list, sort {lc($a->name) cmp lc($b->name)}
                    ($axmud::CLIENT->ivValues('colourSchemeHash')));
        }

        foreach my $schemeObj (@list) {

            my $column;

            if (
                $axmud::CLIENT->ivExists('constGridWinTypeHash', $schemeObj->name)
                || $axmud::CLIENT->ivExists('constFreeWinTypeHash', $schemeObj->name)
            ) {
                $column = ' *';
            } else {
                $column = '  ';
            }

            if ($schemeObj->overrideAllFlag) {
                $column .= '# ';
            } else {
                $column .= '  ';
            }

            $session->writeText($column . $schemeObj->name);
            $session->writeText(
                '      Text: ' . $schemeObj->textColour . ', underlay: '
                . $schemeObj->underlayColour . ', background: ' . $schemeObj->backgroundColour,
            );
            $session->writeText(
                '      Font: ' . $schemeObj->font . ' ' . $schemeObj->fontSize,
            );

            foreach my $key (sort {lc($a) cmp lc($b)} ($schemeObj->ivKeys('overrideHash'))) {

                my $value = $schemeObj->ivShow('overrideHash', $key);
                if (! defined $value) {

                    $value = '(use normal colour)';
                }

                $session->writeText('      Override: ' . $key . ' > ' . $value);
            }

            $session->writeText('      Wrap: ' . $schemeObj->wrapMode);
        }

        # Display footer
        if (scalar @list == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 colour scheme found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar (@list) . ' colour schemes found)',
            );
        }
    }
}

# Recordings, missions, quests and routes

{ package Games::Axmud::Cmd::Record;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('record', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rcd', 'record'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Starts or stops a recording';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $stripObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the 'main' window's entry strip object (if any), so we can update its icon
        $stripObj = $session->mainWin->getStrip('entry');

        # If there is no recording in progress...
        if (! $session->recordingFlag) {

            # Empty the previous recording (if any)
            $session->set_recordingList();
            # Start adding new commands to the beginning of the (empty) list
            $session->set_recordingPosn();

            # Start a new recording. $session->worldCmd intercepts all commands sent to the world
            #   and stores them in $session->recordingList
            $session->set_recordingFlag(TRUE);
            $session->set_recordingPausedFlag(FALSE);

            # Update the comamnd entry box's icon
            if ($stripObj) {

                $stripObj->set_recordIcon($session);
            }

            return $self->complete($session, $standardCmd, 'New recording started');

        } else {

            # Update
            $session->set_recordingFlag(FALSE);
            $session->set_recordingPausedFlag(FALSE);

            # Update the comamnd entry box's icon
            if ($stripObj) {

                $stripObj->set_recordIcon($session);
            }

            if ($session->recordingPausedFlag) {

                return $self->complete($session, $standardCmd, 'Paused recording stopped');

            } else {

                return $self->complete($session, $standardCmd, 'Recording stopped');
            }
        }
    }
}

{ package Games::Axmud::Cmd::PauseRecording;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('pauserecording', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['prc', 'pauserc', 'pauserecording'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Pauses or resumes the current recording';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $stripObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the 'main' window's entry strip object (if any), so we can update its icon
        $stripObj = $session->mainWin->getStrip('entry');

        # If there is no recording in progress...
        if (! $session->recordingFlag) {

            return $self->error(
                $session, $inputString,
                'Cannot pause/resume the recording - no recording is in progress',
            );

        } elsif ($session->recordingPausedFlag) {

            $session->set_recordingPausedFlag(FALSE);

            # Update the comamnd entry box's icon
            if ($stripObj) {

                $stripObj->set_recordIcon($session);
            }

            return $self->complete(
                $session, $standardCmd,
                'The paused recording has been resumed',
            );

        } else {

            $session->set_recordingPausedFlag(TRUE);

            # Update the comamnd entry box's icon
            if ($stripObj) {

                $stripObj->set_recordIcon($session);
            }

            return $self->complete(
                $session, $standardCmd,
                'The current recording has been paused',
            );
        }
    }
}

{ package Games::Axmud::Cmd::WorldCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('worldcommand', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['wc', 'worldcmd', 'worldcommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a world command to the current recording';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my $cmd;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # If there is no recording in progress...
        if (! $session->recordingFlag) {

            return $self->error(
                $session, $inputString,
                'Cannot add a world command to the recording - no recording is in progress',
            );

        } else {

            # The initial '>' character identifies the string as a world command
            $cmd = '> ' . join(' ', @args);
            # Add the string to the recording
            $self->addRecordingString($session, $cmd);

            return $self->complete(
                $session, $standardCmd,
                'World command \'' . $cmd . '\' added to the current recording',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ClientCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('clientcommand', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cc', 'clientcmd', 'clientcommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a client command to the current recording';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my $cmd;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # If there is no recording in progress...
        if (! $session->recordingFlag) {

            return $self->error(
                $session, $inputString,
                'Cannot add ' . $axmud::NAME_ARTICLE . ' command to the recording - no recording is'
                . ' in progress',
            );

        } else {

            # The initial ';' character identifies the string as a client command
            $cmd = '; ' . join(' ', @args);
            # Add the string to the recording
            $self->addRecordingString($session, $cmd);
        }
    }
}

{ package Games::Axmud::Cmd::SpeedWalkCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('speedwalkcommand', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['spc', 'speedcmd', 'speedcommand', 'speedwalkcommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a speedwalk command to the current recording';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my $cmd;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # If there is no recording in progress...
        if (! $session->recordingFlag) {

            return $self->error(
                $session, $inputString,
                'Cannot add a speedwalk command to the recording - no recording is in progress',
            );

        } else {

            # The initial '.' character identifies the string as a world command
            $cmd = '. ' . join(' ', @args);
            # Add the string to the recording
            $self->addRecordingString($session, $cmd);

            return $self->complete(
                $session, $standardCmd,
                'Speedwalk command \'' . $cmd . '\' added to the current recording',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Comment;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('comment', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cmt', 'comment'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a comment to the current recording';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my $comment;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # If there is no recording in progress...
        if (! $session->recordingFlag) {

            return $self->error(
                $session, $inputString,
                'Cannot add a comment to the recording - no recording is in progress',
            );

        } else {

            # The initial '#' character identifies the string as a comment
            $comment = '# ' . join(' ', @args);
            # Add the string to the recording
            $self->addRecordingString($session, $comment);

            return $self->complete(
                $session, $standardCmd,
                'Comment \'' . $comment . '\' added to the current recording',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Break;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('break', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['brk', 'break'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a break to the current recording';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $arg,
            $check,
        ) = @_;

        # Local variables
        my ($string, $msg);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If there is no recording in progress...
        if (! $session->recordingFlag) {

            return $self->error(
                $session, $inputString,
                'Cannot add a break to the recording - no recording is in progress',
            );
        }

        # ;break
        if (! defined $switch) {

            # An ordinary break
            $string = '@';
            $msg = 'Break added to the current recording';

        # ;break -t <pattern>
        } elsif ($switch eq '-t') {

            # A trigger break
            if (! $arg) {

                return $self->error(
                    $session, $inputString,
                    'Add a trigger break using which pattern? (Try \';break -t <pattern>\')',
                );

            } elsif ($axmud::CLIENT->regexCheck($arg)) {

                return $self->error(
                    $session, $inputString,
                    'The pattern \'' . $arg . '\' isn\'t a valid regular expression',
                );

            } else {

                # The initial character identifies $string as a trigger break
                $string = 't ' . $arg;
                $msg = 'Trigger break added to the current recording';
            }

        # ;break -p <number>
        } elsif ($switch eq '-p') {

            # A pause break
            if (! $arg) {

                return $self->error(
                    $session, $inputString,
                    'Add a pause break using which number? (Try \';break -p <number>\')',
                );

            } elsif (! $axmud::CLIENT->floatCheck($arg, 0) || $arg == 0) {

                return $self->error(
                    $session, $inputString,
                    'Invalid number \'' . $arg . '\' (must be a number greater than 0)',
                );

            } else {

                # The initial character identifies $string as a pause break
                $string = 'p ' . $arg;
                $msg = 'Pause break added to the current recording';
            }

        # ;break -l
        } elsif ($switch eq '-l') {

            # A Locator break
            if ($arg) {

                return $self->error(
                    $session, $inputString,
                    'The switch -l can\'t be used with an argument',
                );

            } else {

                # The initial character identifies $string as a Locator break
                $string = 'l';
                $msg = 'Locator break added to the current recording';
            }

        } else {

            # Invalid switch
            return $self->improper($session, $inputString);
        }

        # Add the string to the recording
        $self->addRecordingString($session, $string);

        return $self->complete($session, $standardCmd, $msg);
    }
}

{ package Games::Axmud::Cmd::InsertRecording;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('insertrecording', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['irc', 'insert', 'insertrc', 'insertrecording'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the insertion point for the current recording';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If there is no recording in progress, or no lines have been recorded yet...
        if (! $session->recordingFlag) {

            return $self->error(
                $session, $inputString,
                'Cannot add an insertion point to the recording - no recording is in progress',
            );

        } elsif (! $session->recordingList) {

            return $self->error(
                $session, $inputString,
                'Cannot add an insertion point to the recording - a recording is in progress, but'
                . ' no lines have been recorded yet',
            );

        # ;irc <number>
        } elsif (defined $number) {

            # If the insertion point's line number is bigger than the size of the recording, it is
            #   still set - commands will still be added to the end of the list (everything works
            #   auto-magically)
            if (! $axmud::CLIENT->intCheck($number, 1)) {

                return $self->error(
                    $session, $inputString,
                    'Please specify a line number, 1 or more (try \';listrecording\' for current'
                    . ' line numbers)',
                );

            } else {

                # Create the insertion point
                $session->ivPoke('recordingPosn', $number - 1);

                return $self->complete(
                    $session, $standardCmd,
                    'Current recording\'s insertion point set to line #' . $number,
                );
            }

        # ;irc
        } else {

            if (! defined $session->recordingPosn) {

                return $self->error(
                    $session, $inputString,
                    'Don\'t need to cancel the current recording\'s insertion point - no'
                    . ' insertion point has been set',
                );

            } else {

                # Cancel the insertion point
                $session->ivUndef('recordingPosn');

                return $self->complete(
                    $session, $standardCmd,
                    'Recording\'s insertion point cancelled (new lines will be added to the end of'
                    . ' the recording)',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::DeleteRecording;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleterecording', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['drc', 'deleterc', 'deleterecording'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes line(s) from the current recording';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $start, $stop,
            $check,
        ) = @_;

        # Local variables
        my ($text, $number);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If there is no recording in progress...
        if (! $session->recordingFlag) {

            return $self->error(
                $session, $inputString,
                'Cannot delete lines - no recording is in progress',
            );

        } elsif (! $session->recordingList) {

            return $self->error(
                $session, $inputString,
                'Cannot delete line(s) - the current recording is empty',
            );
        }

        # ;drc
        if (! defined $start) {

            $number = $session->ivNumber('recordingList');
            $text = $session->ivPop('recordingList');

            return $self->complete(
                $session, $standardCmd,
                'Deleted recording line #' . $number . ' \'' . $text . '\'',
            );

        # ;drc <line>
        # ;drc <start> <stop> (where <start> and <stop> are the same number)
        } elsif (! defined $stop || $start == $stop) {

            if (! $axmud::CLIENT->intCheck($start, 1)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid line number \'' . $start . '\' (try \';listrecording\' for current'
                    . ' recording\'s line numbers)',
                );

            } elsif ($session->ivNumber('recordingList') < $start) {

                return $self->error(
                    $session, $inputString,
                    'Current recording line #' . $start . ' doesn\'t exist (try \';listrecording\''
                    . ' for current recording\'s line numbers)',
                );

            } else {

                # Delete the line from the recording
                ($text) = $session->ivSplice('recordingList', ($start - 1), 1);

                return $self->complete(
                    $session, $standardCmd,
                    'Deleted recording line #' . $start . ' \'' . $text . '\'',
                );
            }

        # ;drc <start> <stop>
        } else {

            if (! $axmud::CLIENT->intCheck($start, 1)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid line number \'' . $start . '\' (try \';listrecording\' for current'
                    . ' recording\'s line numbers)',
                );

            } elsif (! $axmud::CLIENT->intCheck($stop, 1)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid line number \'' . $stop . '\' (try \';listrecording\' for current'
                    . ' recording\'s line numbers)',
                );

            } elsif ($session->ivNumber('recordingList') < $start) {

                return $self->error(
                    $session, $inputString,
                    'Current recording line #' . $start . ' doesn\'t exist (try \';listrecording\''
                    . ' for current recording\'s line numbers)',
                );

            } else {

                # If line number $stop hasn't been recorded yet, just delete all lines from line
                #   number $start until the end of the recording
                if ($session->ivNumber('recordingList') < $stop) {

                    $stop = $session->ivNumber('recordingList') ;
                }

                # Delete the lines
                $session->ivSplice('recordingList', ($start - 1), ($stop - $start + 1));

                return $self->complete(
                    $session, $standardCmd,
                    'Deleted recording lines #' . $start . ' to #' . $stop,
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ListRecording;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listrecording', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lrc', 'listrc', 'listrecord', 'listrecording'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists contents of the current recording';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my ($count, $status);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (! $session->recordingList) {

            if ($session->recordingFlag) {

                return $self->error(
                    $session, $inputString,
                    'A recording is in progress, but no lines have been added to it yet',
                );

            } else {

                return $self->error(
                    $session, $inputString,
                    'No recording has been made',
                );
            }
        }

        # Display header
        if ($session->recordingPausedFlag) {
            $status = 'paused';
        } elsif ($session->recordingFlag) {
            $status = 'in progress';
        } else {
            $status = 'stopped';
        }

        $session->writeText(
            'Current recording (' . $status . ') (> command, ! ' . $axmud::SCRIPT . ' command,'
            . ' # comment, @/T/P/L break, * insert point)',
        );

        # Display list
        $count = 0;
        foreach my $line ($session->recordingList) {

            my ($initChar, $cmd, $column, $data);

            $count++;
            $initChar = substr($line, 0, 1);
            $cmd = substr($line, 1);

            if ($initChar eq '>') {

                # World command
                $column = ' >';
                $data = $cmd;

            } elsif ($initChar eq ';') {

                # Client command
                $column = ' ;';
                $data = $cmd;

            } elsif ($initChar eq '#') {

                # Comment
                $column = ' #';
                $data = '\'' . $cmd . '\'';

            } elsif ($initChar eq '@') {

                # (Ordinary) break
                $column = ' @';
                $data = '<Break>';

            } elsif ($initChar eq 't') {

                # Trigger break
                $column = ' T';
                if (length $cmd > 48) {
                    $data = '<Trigger break: ' . substr ($cmd, 0, 48) . '...>';
                } else {
                    $data = '<Trigger break: ' .  $cmd . '>';
                }

            } elsif ($initChar eq 'p') {

                # Pause break
                $column = ' P';
                $data = '<Pause break - ' .  $cmd . '>';

            } elsif ($initChar eq 'l') {

                # Locator break
                $column = ' L';
                $data = '<Locator break>';

            } else {

                # Unrecognised string
                $column = '  ';
                $data = '<Warning - unrecognised command at line ' . $count . '>';
            }

            if (
                defined $session->recordingPosn
                && $session->recordingPosn == ($count - 1)
            ) {
                # Command insertion point is at this line
                $column .= '* ';

            } else {

                $column .= '  ';
            }

            $session->writeText($column . sprintf('%-8.8s ', $count) . $data);
        }

        # If the insertion point is outside the list...
        if (
            defined $session->recordingPosn
            && ($session->recordingPosn + 1) > $session->ivNumber('recordingList')
        ) {
            $session->writeText(
                '  * -        <Insertion point exists at future line '
                . ($session->recordingPosn + 1) . '>',
            );
        }

        if ($count == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of recording listing (1 line found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of recording listing (' . $count . ' lines found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::CopyRecording;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('copyrecording', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['crc', 'copyrc', 'copyrecord', 'copyrecording'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows recording\'s commands as a single line';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (@list, @list2, @list3);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (! $session->recordingList) {

            if ($session->recordingFlag) {

                return $self->error(
                    $session, $inputString,
                    'A recording is in progress, but no lines have been added to it yet',
                );

            } else {

                return $self->error(
                    $session, $inputString,
                    'No recording has been made',
                );
            }
        }

        # Extract from the recording only world commands
        foreach my $string ($session->recordingList) {

            my $cmd;

            if (substr ($string, 0, 1) eq '>') {

                # This is a command, not a comment or a break
                $cmd = substr($string, 1);
                # (The initial '>' character can be followed by optional whitespace, which we don't
                #   need)
                $cmd = $axmud::CLIENT->trimWhitespace($cmd);
                if ($cmd) {

                    push (@list, $cmd);
                }
            }
        }


        # Display header
        $session->writeText(
            'Current recording commands (all client commands, comments and breaks omitted)',
        );

        # Display list

        # Compile a string of commands, separated by the usual command separator, and ignoring
        #   comments and breaks
        $session->writeText(' ');
        $session->writeText('Actual recording:');
        $session->writeText(join ($axmud::CLIENT->cmdSep, @list));

        # Show unabbreviated and abbreviated versions
        foreach my $item (@list) {

            my $abbrevDir = $session->currentDict->abbrevDir($item);

            push (@list2, $session->currentDict->unabbrevDir($item));

            if (defined $abbrevDir) {
                push (@list3, $session->currentDict->abbrevDir($item));
            } else {
                push (@list3, $item);
            }
        }

        $session->writeText(' ');
        $session->writeText('Unabbreviated version:');
        $session->writeText(join ($axmud::CLIENT->cmdSep, @list2));

        $session->writeText(' ');
        $session->writeText('Abbreviated version:');
        $session->writeText(join ($axmud::CLIENT->cmdSep, @list3));
        $session->writeText(' ');

        # Display footer
        if (@list == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of recording listing (1 world command found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of recording listing (' . @list . ' world commands found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::StartMission;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('startmission', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['smn', 'startmn', 'startmission'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Starts a new mission';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name, $switch,
            $check,
        ) = @_;

        # Local variables
        my ($missionObj, $cloneObj, $result);

        # Check for improper arguments
        if (! defined $name || (defined $switch && $switch ne '-i') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there's not already a mission in progress
        if ($session->currentMission) {

            return $self->error(
                $session, $inputString,
                'There is already a mission (\'' . $session->currentMission->name
                . '\') in progress',
            );

        } elsif (! $session->currentWorld->ivExists('missionHash', $name)) {

            return $self->error(
                $session, $inputString,
                'The \'' . $name . '\' mission doesn\'t exist',
            );

        } else {

            $missionObj = $session->currentWorld->ivShow('missionHash', $name);
        }

        # The mission object stored in the current world might be executed simultaneously by more
        #   than one session, so we create a clone of it, and store the clone in
        #   $session->currentMission
        $cloneObj = $missionObj->clone($session);
        if (! $cloneObj) {

            return $self->error(
                $session, $inputString,
                'General error starting the \'' . $name . '\' mission',
            );

        } else {

            $session->set_currentMission($cloneObj);
        }

        # Start the mission
        if (! $cloneObj->startMission()) {

            return $self->error(
                $session, $inputString,
                'General error starting the \'' . $name . '\' mission',
            );
        }

        # Process the first group of commands (just as if ;mission had been typed). If the -i switch
        #   was specified, send the argument TRUE to specify that only a single command must be sent
        #   on this occasion
        if ($switch) {
            $result = $cloneObj->continueMission($session, TRUE);
        } else {
            $result = $cloneObj->continueMission($session);
        }

        if (! $result) {

            # Terminate the mission
            return $self->error(
                $session, $inputString,
                'The \'' . $name . '\' mission has been terminated because of an error',
            );

        } else {

            # The mission is underway, or has been completed. Either way, a message has already
            #   been displayed
            return 1;
        }
    }
}

{ package Games::Axmud::Cmd::Mission;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('mission', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mn', 'mission'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Continues the current mission';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $missionObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there's a mission in progress
        if (! $session->currentMission) {

            return $self->error(
                $session, $inputString,
                'There is no mission in progress (try \';startmission <name\')',
            );

        } else {

            $missionObj = $session->currentMission;
        }

        # If we're on a trigger/pause/Locator break, or just an ordinary break, the mission's break
        #   variables must be reset
        if (defined $missionObj->breakType) {

            $missionObj->resetBreak();
        }

        # Continue the mission
        if (! $missionObj->continueMission($session)) {

            # Terminate the mission
            return $self->error(
                $session, $inputString,
                'The \'' . $missionObj->name . '\' mission has been terminated because of an error',
            );

        } else {

            # The mission is underway, or complete (message already displayed)
            return 1;
        }
    }
}

{ package Games::Axmud::Cmd::NudgeMission;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('nudgemission', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['nmn', 'nmission', 'nudgemission'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Advances the current mission by one step';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $missionObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there's a mission in progress
        if (! $session->currentMission) {

            return $self->error(
                $session, $inputString,
                'There is no mission in progress (try \';startmission <name\')',
            );

        } else {

            $missionObj = $session->currentMission;
        }

        # If we're on a trigger/pause/Locator break, or just an ordinary break, the mission's break
        #   variables must be reset
        if (defined $missionObj->breakType) {

            $missionObj->resetBreak();
        }

        # Continue the mission. The TRUE arguments tells the mission object to process one command
        #   only
        if (! $missionObj->continueMission($session, TRUE)) {

            # Terminate the mission
            return $self->error(
                $session, $inputString,
                'The \'' . $missionObj->name . '\' mission has been terminated because of an error',
            );

        } else {

            # The mission is underway, or complete (message already displayed)
            return 1;
        }
    }
}

{ package Games::Axmud::Cmd::HaltMission;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('haltmission', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['hmn', 'haltmn', 'haltmission'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Halts (resets) the current mission';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $name;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there's a mission in progress
        if (! $session->currentMission) {

            return $self->error(
                $session, $inputString,
                'There is no mission in progress (try \';startmission <name\')',
            );

        } else {

            # Halt the mission
            $name = $session->currentMission->name;
            $session->reset_currentMission();

            return $self->complete(
                $session, $standardCmd,
                'The \'' . $name . '\' mission has been halted',
            );
        }
    }
}

{ package Games::Axmud::Cmd::RepeatComment;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('repeatcomment', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rcm', 'repcm', 'repeatcm', 'repeatcomment'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Repeats the current mission\'s most recent comment';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $missionObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there's a mission in progress
        if (! $session->currentMission) {

            return $self->error(
                $session, $inputString,
                'There is no mission in progress (try \';startmission <name\')',
            );

        } else {

            $missionObj = $session->currentMission;
        }

        # Check there's a comment to display
        if (! $missionObj->prevComment) {

            return $self->error(
                $session, $inputString,
                'The \'' . $missionObj->name . '\' mission hasn\'t displayed a comment yet',
            );

        } else {

            # Repeat the most recent comment
            $missionObj->displayComment($session, $missionObj->prevComment);

            return $self->complete(
                $session, $standardCmd,
                'The \'' . $missionObj->name . '\' mission\'s most recent comment has been'
                . ' re-displayed',
            );
        }
    }
}

{ package Games::Axmud::Cmd::RepeatMission;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('repeatmission', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rmn', 'repmn', 'repeatmn', 'repeatmission'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Repeats commands from the current mission';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my (
            $missionObj, $count,
            @prevList, @repeatList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there's a mission in progress
        if (! $session->currentMission) {

            return $self->error(
                $session, $inputString,
                'There is no mission in progress (try \';startmission <name\')',
            );

        } else {

            $missionObj = $session->currentMission;
        }

        # Check the mission has processed at least one command
        if (! $missionObj->prevCmdList) {

            return $self->error(
                $session, $inputString,
                'The \'' . $missionObj->name . '\' mission hasn\'t processed any commands yet',
            );
        }

        # Check that <number> is valid, if specified
        if (defined $number && ! $axmud::CLIENT->intCheck($number, 1)) {

            return $self->error(
                $session, $inputString,
                'Invalid number \'' . $number . '\'',
            );

        } elsif (! defined $number) {

            # Default number of commands to repeat is one
            $number = 1;
        }

        # Import a copy of the mission's list of previous commands
        @prevList = $missionObj->prevCmdList;

        # Extract the last <number> arguments, and put them in the right order
        for (my $count = 1; $count <= $number; $count++) {

            push (@repeatList, pop @prevList);
        }

        @repeatList = reverse @repeatList;

        # Repeat the command(s)
        $count = 0;
        foreach my $string (@repeatList) {

            my ($initChar, $cmd);

            # $string starts with an initial char, followed by optional whitespace, followed by a
            #   command to process. Separate $string into its components
            $initChar = substr($string, 0, 1);
            $cmd = substr($string, 1);
            $cmd = $axmud::CLIENT->trimWhitespace($cmd);

            # Repeat world/client commands, but ignore other kinds of command
            if ($initChar eq '>') {

                $session->worldCmd($cmd);
                $count++;

            } elsif ($initChar eq ';') {

                $session->clientCmd($cmd);
                $count++;
            }
        }

        if ($count == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Repeated most recent command from the \'' . $missionObj->name . '\' mission',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Repeated ' . $count. ' most recent from the \'' . $missionObj->name . '\' mission',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddMission;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addmission', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['amn', 'addmn', 'addmission'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a new mission using the current recording';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name, $descrip,
            $check,
        ) = @_;

        # Local variables
        my $missionObj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the mission doesn't already exist
        if ($session->currentWorld->ivExists('missionHash', $name)) {

            return $self->error(
                $session, $inputString,
                'The current world profile already has a mission called \'' . $name . '\'',
            );

        # Check the name is allowed (max 16 characters, alphanumeric chars & underline, 1st char
        #   can't be number)
        } elsif (! $axmud::CLIENT->nameCheck($name, 16)) {

            return $self->error(
                $session, $inputString,
                'Invalid mission name \'' . $name . '\' (max 16 chars, alphanumeric and'
                . ' underlines, 1st char not numeric',
            );

        # Check the description is not too long, if specified
        } elsif (defined $descrip && length $descrip > 64) {

            return $self->error(
                $session, $inputString,
                'Invalid mission description (max 64 chars)',
            );
        }

        # Create the new mission
        $missionObj = Games::Axmud::Obj::Mission->new($session, $name, $descrip);
        if (! $missionObj) {

            return $self->error(
                $session, $inputString,
                'General error creating the \'' . $name . '\' mission',
            );

        } else {

            # Add the mission to the world profile
            $session->currentWorld->ivAdd('missionHash', $name, $missionObj);

            # Copy the contents of the recording to the new mission
            $missionObj->ivPoke('missionList', $session->recordingList);

            if ($missionObj->missionList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Mission \'' . $name . '\' added using 1 command, comment or break',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Mission \'' . $name . '\' added using ' . scalar $missionObj->missionList
                    . ' commands, comments and breaks',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::CloneMission;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('clonemission', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cmn', 'clonemn', 'clonemission'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Clones an existing mission';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $original, $copy,
            $check,
        ) = @_;

        # Local variables
        my ($originalObj, $copyObj);

        # Check for improper arguments
        if (! defined $original || ! defined $copy || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that a mission called $original exists
        if (! $session->currentWorld->ivExists('missionHash', $original)) {

            return $self->error(
                $session, $inputString,
                'The current world profile doesn\'t have a mission called \'' . $original . '\'',
            );

        } else {

            $originalObj = $session->currentWorld->ivShow('missionHash', $original);
        }

        # Check that a mission named $copy doesn't already exist
        if ($session->currentWorld->ivExists('missionHash', $copy)) {

            return $self->error(
                $session, $inputString,
                'The current world profile already has a mission called \'' . $copy . '\'',
            );

        # Check that $copy's name is allowed (max 16 characters, alphanumeric chars & underline,
        #   1st char can't be number)
        } elsif (! $axmud::CLIENT->nameCheck($copy, 16)) {

            return $self->error(
                $session, $inputString,
                'Invalid mission name \'' . $copy . '\' (max 16 chars, alphanumeric and'
                . ' underlines, 1st char not numeric',
            );
        }

        # Clone the existing mission
        $copyObj = $originalObj->clone($session, $copy);
        if (! $copyObj) {

            return $self->error(
                $session, $inputString,
                'General error creating the \'' . $copy . '\' mission',
            );

        } else {

            # Add the copy to the world profile
            $session->currentWorld->ivAdd('missionHash', $copy, $copyObj);

            return $self->complete(
                $session, $standardCmd,
                'Created cloned mission \'' . $copy . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditMission;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editmission', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['emn', 'editmn', 'editmission'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a mission';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the mission exists
        if (! $session->currentWorld->ivExists('missionHash', $name)) {

            return $self->error(
                $session, $inputString,
                'Could not edit the \'' . $name . '\' mission - the current world profile'
                . ' doesn\'t have any such mission',
            );

        } else {

            $obj = $session->currentWorld->ivShow('missionHash', $name);
        }

        # Open an \'edit\' window for the mission
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Mission',
                $session->mainWin,
                $session,
                'Edit mission \'' . $obj->name . '\'',
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the \'' . $name . '\' mission',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the \'' . $name . '\' mission',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteMission;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletemission', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dmn', 'delmn', 'deletemn', 'deletemission'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a mission';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the mission exists
        if (! $session->currentWorld->ivExists('missionHash', $name)) {

            return $self->error(
                $session, $inputString,
                'The current world profile doesn\'t have a mission called \'' . $name . '\'',
            );
        }

        # Check the mission isn't in progress
        if ($session->currentMission && $session->currentMission->name eq $name) {

            return $self->error(
                $session, $inputString,
                'The \'' . $name . '\' mission can\'t be deleted while it is in progress (try'
                . '\';haltmission\' first)',
            );
        }

        # Delete the mission
        $session->currentWorld->ivDelete('missionHash', $name);

        return $self->complete(
            $session, $standardCmd,
            'Mission \'' . $name . '\' deleted from the current world profile',
        );
    }
}

{ package Games::Axmud::Cmd::ListMission;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listmission', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lmn', 'listmn', 'listmission'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists all missions';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there are some missions to list
        if (! $session->currentWorld->missionHash) {

            return $self->error(
                $session, $inputString,
                'The current world profile doesn\'t contain any missions',
            );
        }

        # Display header
        $session->writeText('List of missions (* mission in progress)');
        $session->writeText('   Name             Description');

        # Display list
        @list = sort {lc($a->name) cmp lc($b->name)}
                    ($session->currentWorld->ivValues('missionHash'));

        foreach my $missionObj (@list) {

            my $column;

            if ($session->currentMission && $session->currentMission eq $missionObj) {
                $column = ' * ';
            } else {
                $column = '   ';
            }

            if ($missionObj->descrip) {

                $session->writeText(
                    $column . sprintf('%-16.16s %-64.64s', $missionObj->name, $missionObj->descrip),
                );

            } else {

                $session->writeText(
                    $column . sprintf('%-16.16s <no description>', $missionObj->name),
                );
            }
        }

        # Display footer
        if (@list == 1) {
            $self->complete($session, $standardCmd, 'End of list (1 mission found)');
        } else {
            $self->complete($session, $standardCmd, 'End of list (' . @list . ' missions found)');
        }
    }
}

{ package Games::Axmud::Cmd::PresentMission;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('presentmission', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['pmn', 'presentmn', 'presentmission'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows the contents of a mission';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my ($missionObj, $currentFlag, $count);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that the specified mission (if any) exists
        if ($name) {

            if (! $session->currentWorld->ivExists('missionHash', $name)) {

                return $self->error(
                    $session, $inputString,
                    'The current world profile doesn\'t have a mission called \'' . $name . '\'',
                );

            } else {

                $missionObj = $session->currentWorld->ivShow('missionHash', $name);
            }

        # If no mission was specified, use the current one (if there is one)
        } elsif (! $name) {

            if ($session->currentMission) {

                $missionObj = $session->currentMission;
                $name = $missionObj->name;

            } else {

                return $self->error(
                    $session, $inputString,
                    'There is no current mission (try \';dumpmission <name>\')',
                );
            }
        }

        if ($session->currentMission && $session->currentMission eq $missionObj) {

            $currentFlag = TRUE;
        }

        # Display header
        if ($currentFlag) {

            $session->writeText(
                'Contents of current mission \'' . $name . '\' (> command, ! ' . $axmud::SCRIPT
                . ' command, # comment, @/T/P/L break, * next line)',
            );

        } else {

            $session->writeText(
                'Contents of mission \'' . $name . '\' (> command, !' . $axmud::SCRIPT
                . ' command, # comment, @/T/P/L break)',
            );
        }

        $session->writeText('   Descrip: \'' . $missionObj->descrip . '\'');

        # Display list
        $count = 0;
        foreach my $line ($missionObj->stringList) {

            my ($initChar, $cmd, $column, $data);

            $count++;
            $initChar = substr($line, 0, 1);
            $cmd = substr($line, 1);

            if ($initChar eq '>') {

                # World command
                $column = ' >';
                $data = $cmd;

            } elsif ($initChar eq ';') {

                # Client command
                $column = ' ;';
                $data = $cmd;

            } elsif ($initChar eq '#') {

                # Comment
                $column = ' #';
                $data = '\'' . $cmd . '\'';

            } elsif ($initChar eq '@') {

                # (Ordinary) break
                $column = ' @';
                $data = '<Break>';

            } elsif ($initChar eq 't') {

                # Trigger break
                $column = ' T';
                if (length $cmd > 48) {
                    $data = '<Trigger break: ' . substr ($cmd, 0, 48) . '...>';
                } else {
                    $data = '<Trigger break: ' .  $cmd . '>';
                }

            } elsif ($initChar eq 'p') {

                # Pause break
                $column = ' P';
                $data = '<Pause break - ' .  $cmd . '>';

            } elsif ($initChar eq 'l') {

                # Locator break
                $column = ' L';
                $data = '<Locator break>';

            } else {

                # Unrecognised string
                $column = '  ';
                $data = '<Warning - unrecognised command at line ' . $count . '>';
            }

            if ($currentFlag && $missionObj->nextString == ($count - 1)) {

                # This is the next string to be processed in a current mission
                $column .= '* ';

            } else {

                $column .= '  ';
            }

            $session->writeText($column . sprintf('%-4.4s ', $count) . $data);
        }

        if ($count == 1) {

            return $self->complete($session, $standardCmd, 'End of mission (1 line found)');

        } else {

            return $self->complete(
                $session, $standardCmd, 'End of mission (' . $count . ' lines found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddQuest;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addquest', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['aqs', 'addqs', 'addquest'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a new quest';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $mission, $qp, $xp, $cash, $exitFlag, $questObj,
            @solutionList, @commentList,
        );

        # Check for improper arguments
        if (! defined $name) {

            return $self->improper($session, $inputString);
        }

        # Check the quest doesn't already exist
        if ($session->currentWorld->ivExists('questHash', $name)) {

            return $self->error(
                $session, $inputString,
                'The current world profile already has a quest called \'' . $name . '\'',
            );

        # Check the name is allowed (max 16 characters, alphanumeric chars & underline, 1st char
        #   can't be number)
        } elsif (! $axmud::CLIENT->nameCheck($name, 16)) {

            return $self->error(
                $session, $inputString,
                'Invalid quest name \'' . $name . '\' (max 16 chars, alphanumeric and'
                . ' underlines, 1st char not numeric',
            );
        }

        # Extract switches -m, -p, -x, -c (which must appear 0 or 1 times)
        ($switch, $mission, @args) = $self->extract('-m', 1, @args);
        ($switch, $qp, @args) = $self->extract('-p', 1, @args);
        ($switch, $xp, @args) = $self->extract('-x', 1, @args);
        ($switch, $cash, @args) = $self->extract('-c', 1, @args);

        # Extract switches -s and -t on a continuous loop, until no more are found
        if (@args) {

            $exitFlag = TRUE;   # Exit the loop when no more -s/-t switches present

            do {

                my $string;

                ($switch, $string, @args) = $self->extract('-s', 1, @args);
                if ($switch) {

                    push (@solutionList, $string);
                    $exitFlag = FALSE;
                }

                ($switch, $string, @args) = $self->extract('-t', 1, @args);
                if ($switch) {

                    push (@commentList, $string);
                    $exitFlag = FALSE;
                }

            } until ($exitFlag);
        }

        # @args must now be empty
        if (@args) {

            return $self->improper($session, $inputString);

        # The number of quest points, xp and cash must be numbers, if specified (0 and negative
        #   numbers allowed)
        } elsif (defined $qp && ! $axmud::CLIENT->floatCheck($qp)) {

            return $self->error(
                $session, $inputString,
                'Invalid value \'' . $qp . '\' for quest points earned (must be a number)',
            );

        } elsif (defined $xp && ! $axmud::CLIENT->floatCheck($xp)) {

            return $self->error(
                $session, $inputString,
                'Invalid value \'' . $xp . '\' for XP earned (must be a number)',
            );

        } elsif (defined $cash && ! $axmud::CLIENT->floatCheck($cash)) {

            return $self->error(
                $session, $inputString,
                'Invalid value \'' . $cash . '\' for cash earned (must be a number)',
            );
        }

        # Create the new quest object
        $questObj = Games::Axmud::Obj::Quest->new($session, $name);
        if (! $questObj) {

            return $self->error(
                $session, $inputString,
                'General error creating the \'' . $name . '\' quest',
            );
        }

        # Add the quest to the world profile
        $session->currentWorld->ivAdd('questHash', $name, $questObj);

        # Set up the quest, using any specified data
        if (defined $mission) {

            $questObj->ivPoke('missionName', $mission);
        }

        if (defined $qp) {

            $questObj->ivPoke('questPoints', $qp);
        }

        if (defined $xp) {

            $questObj->ivPoke('questXP', $xp);
        }

        if (defined $cash) {

            $questObj->ivPoke('questCash', $cash);
        }

        if (@solutionList) {

            $questObj->ivPoke('solutionList', @solutionList);
        }

        if (@commentList) {

            $questObj->ivPoke('commentList', @commentList);
        }

        # The world profile keeps a running total of quest statistics. Now we've added a new quest,
        #   those statistics must be updated
        $session->currentWorld->updateQuestStats($session);

        if (defined $qp) {

            return $self->complete(
                $session, $standardCmd,
                'Quest \'' . $name . '\' worth ' . $qp . ' quest points created',
            );

        } else {

            return $self->complete($session, $standardCmd, 'Quest \'' . $name . '\' created');
        }
    }
}

{ package Games::Axmud::Cmd::CloneQuest;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('clonequest', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cqs', 'cloneqs', 'clonequest'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Clones an existing quest';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $original, $copy,
            $check,
        ) = @_;

        # Local variables
        my ($originalObj, $copyObj);

        # Check for improper arguments
        if (! defined $original || ! defined $copy || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that a quest called $original exists
        if (! $session->currentWorld->ivExists('questHash', $original)) {

            return $self->error(
                $session, $inputString,
                'The current world profile doesn\'t have a quest called \'' . $original . '\'',
            );

        } else {

            $originalObj = $session->currentWorld->ivShow('questHash', $original);
        }

        # Check that a quest named $copy doesn't already exist
        if ($session->currentWorld->ivExists('questHash', $copy)) {

            return $self->error(
                $session, $inputString,
                'The current world profile already has a quest called \'' . $copy . '\'',
            );

        # Check that $copy's name is allowed (max 16 characters, alphanumeric chars & underline,
        #   1st char can't be number)
        } elsif (! $axmud::CLIENT->nameCheck($copy, 16)) {

            return $self->error(
                $session, $inputString,
                'Invalid quest name \'' . $copy . '\' (max 16 chars, alphanumeric and'
                . ' underlines, 1st char not numeric',
            );
        }

        # Create the new quest
        $copyObj = $originalObj->clone($session, $copy);
        if (! $copyObj) {

            return $self->error(
                $session, $inputString,
                'General error creating the \'' . $copy . '\' quest',
            );

        } else {

            # Add the copy to the world profile
            $session->currentWorld->ivAdd('questHash', $copy, $copyObj);

            # The world profile and character profiles keep a running total of quest statistics. Now
            #   we've added a new quest, those statistics must be updated
            $session->currentWorld->updateQuestStats($session);

            return $self->complete(
                $session, $standardCmd,
                'Created cloned quest \'' . $copy . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditQuest;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editquest', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['eqs', 'editqs', 'editquest'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a quest';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the quest exists
        if (! $session->currentWorld->ivExists('questHash', $name)) {

            return $self->error(
                $session, $inputString,
                'Could not edit the \'' . $name . '\' quest - the current world profile doesn\'t'
                . ' have any such quest',
            );

        } else {

            $obj = $session->currentWorld->ivShow('questHash', $name);
        }

        # Open an 'edit' window for the quest
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Quest',
                $session->mainWin,
                $session,
                'Edit quest \'' . $obj->name . '\'',
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the \'' . $name . '\' quest',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the \'' . $name . '\' quest',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ModifyQuest;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('modifyquest', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mqs', 'modqs', 'modifyquest'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies an existing quest';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            @args,
        ) = @_;

        # Local variables
        my (
            $questObj, $switch, $mission, $missionFlag, $qp, $qpFlag, $xp, $xpFlag, $cash,
            $cashFlag, $exitFlag, $solutionFlag, $newSolutionFlag, $commentFlag, $newCommentFlag,
            @solutionList, @commentList,
        );

        # Check for improper arguments
        if (! defined $name || ! @args) {

            return $self->improper($session, $inputString);
        }

        # Check the quest exists
        if (! $session->currentWorld->ivExists('questHash', $name)) {

            return $self->error(
                $session, $inputString,
                'The current world profile doesn\'t have a quest called \'' . $name . '\'',
            );

        } else {

            $questObj = $session->currentWorld->ivShow('questHash', $name);
        }

        # Extract switches -m, -p, -x, -c (which must appear 0 or 1 times)
        ($switch, $mission, @args) = $self->extract('-m', 1, @args);
        if (defined $switch) {

            $missionFlag = TRUE;
        }

        ($switch, $qp, @args) = $self->extract('-p', 1, @args);
        if (defined $qp) {

            $qpFlag = TRUE;
        }

        ($switch, $xp, @args) = $self->extract('-x', 1, @args);
        if (defined $xp) {

            $xpFlag = TRUE;
        }

        ($switch, $cash, @args) = $self->extract('-c', 1, @args);
        if (defined $cash) {

            $cashFlag = TRUE;
        }

        # Extract switches -s and -t on a continuous loop, until no more are found
        if (@args) {

            $exitFlag = TRUE;   # Exit the loop when no more -s/-t switches present

            do {

                my $string;

                ($switch, $string, @args) = $self->extract('-s', 1, @args);
                if ($switch) {

                    $exitFlag = FALSE;
                    if (defined $string) {

                        push (@solutionList, $string);
                        $solutionFlag = TRUE;

                    } else {

                        @solutionList = ();
                        $newSolutionFlag = TRUE;
                    }
                }

                ($switch, $string, @args) = $self->extract('-t', 1, @args);
                if ($switch) {

                    $exitFlag = FALSE;

                    if (defined $string) {

                        push (@commentList, $string);
                        $commentFlag = TRUE;

                    } else {

                        @commentList = ();
                        $newCommentFlag = TRUE;
                    }
                }

            } until ($exitFlag);
        }

        # @args must now be empty
        if (@args) {

            return $self->improper($session, $inputString);

        # The number of quest points, xp and cash must be numbers, if specified (0 and negative
        #   numbers allowed)
        } elsif (defined $qp && ! $axmud::CLIENT->floatCheck($qp)) {

            return $self->error(
                $session, $inputString,
                'Invalid value \'' . $qp . '\' for quest points earned (must be a number)',
            );

        } elsif (defined $xp && ! $axmud::CLIENT->floatCheck($xp)) {

            return $self->error(
                $session, $inputString,
                'Invalid value \'' . $xp . '\' for XP earned (must be a number)',
            );

        } elsif (defined $cash && ! $axmud::CLIENT->floatCheck($cash)) {

            return $self->error(
                $session, $inputString,
                'Invalid value \'' . $cash . '\' for cash earned (must be a number)',
            );
        }

        # Modify the quest, using any specified data
        if (defined $mission) {

            if ($missionFlag) {

                $questObj->ivPoke('missionName', $mission);

            } else {

                # -m was used without an argument; reset the value to the default
                $questObj->ivUndef('missionName');
            }
        }

        if (defined $qp) {

            if ($qpFlag) {

                $questObj->ivPoke('questPoints', $qp);

            } else {

                # -m was used without an argument; reset the value to the default
                $questObj->ivUndef('questPoints');
            }
        }

        if (defined $xp) {

            if ($xpFlag) {

                $questObj->ivPoke('questXP', $xp);

            } else {

                # -m was used without an argument; reset the value to the default
                $questObj->ivUndef('questXP');
            }
        }

        if (defined $cash) {

            if ($cashFlag) {

                $questObj->ivPoke('questCash', $cash);

            } else {

                # -m was used without an argument; reset the value to the default
                $questObj->ivUndef('questCash');
            }
        }

        if ($newSolutionFlag) {

            # Overwrite the current list
            $questObj->ivPoke('solutionList', @solutionList);

        } elsif ($solutionFlag) {

            # Add to the current list
            $questObj->ivPush('solutionList', @solutionList);
        }

        if ($newCommentFlag) {

            # Overwrite the current list
            $questObj->ivPoke('commentList', @commentList);

        } elsif ($commentFlag) {

            # Add to the current list
            $questObj->ivPush('commentList', @commentList);
        }

        # The world profile and character profiles keep a running total of quest statistics. Now
        #   we've modified an existing quest, those statistics must be updated
        $session->currentWorld->updateQuestStats($session);

        return $self->complete($session, $standardCmd, 'Quest \'' . $name . '\' modified');
    }
}

{ package Games::Axmud::Cmd::FinishQuest;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('finishquest', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['fqs', 'finqs', 'finishqs', 'finishquest'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Marks a quest as completed';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args
        ) = @_;

        # Local variables
        my ($switch, $deleteFlag, $name, $charObj, $questObj);

        # Extract switches
        ($switch, @args) = $self->extract('-d', 0, @args);
        if (defined $switch) {

            $deleteFlag = TRUE;
        }

        # @args should now contain exactly one argument
        $name = shift @args;
        if (! defined $name || @args) {

            return $self->improper($session, $inputString);
        }

        # Complete quests are linked to a character, so we need a current character profile
        if (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t mark the \'' . $name . '\' quest as complete because there is no current'
                . ' character profile',
            );

        } else {

            # Import the current character profile (for convenience)
            $charObj = $session->currentChar;
        }

        # Check the quest exists (the GA::Obj::Quest object is stored in the world profile)
        if (! $session->currentWorld->ivExists('questHash', $name)) {

            return $self->error(
                $session, $inputString,
                'The current world profile doesn\'t have a quest called \'' . $name . '\'',
            );

        } else {

            $questObj = $session->currentWorld->ivShow('questHash', $name);
        }

        # ;fqs <name>
        if (! $deleteFlag) {

            if ($charObj->ivExists('questHash', $name)) {

                return $self->error(
                    $session, $inputString,
                    'The \'' . $name . '\' quest has already been marked as completed by \''
                    . $charObj->name . '\'',
                );

            } else {

                # Mark the quest as completed by this character by adding to the character profile's
                #   quest hash
                $charObj->ivAdd('questHash', $name, undef);
                # Update the character profile's quest statistics
                $charObj->updateQuestStats($session);

                return $self->complete(
                    $session, $standardCmd,
                    'The \'' . $name . '\' quest marked as completed by \'' . $charObj->name . '\'',
                );
            }

        # ;fqs -d <name>
        # ;fqs <name> -d
        } else {

            if (! $charObj->ivExists('questHash', $name)) {

                return $self->error(
                    $session, $inputString,
                    'The \'' . $name . '\' quest has not been marked as completed by \''
                    . $charObj->name . '\'',
                );

            } else {

                # Mark the quest as not completed by this character by removing it from the
                #   character profile's quest hash
                $charObj->ivDelete('questHash', $name);
                # Update the character profile's quest statistics
                $charObj->updateQuestStats($session);

                return $self->complete(
                    $session, $standardCmd,
                    'The \'' . $name . '\' quest marked as not completed by \'' . $charObj->name
                    . '\'',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::DeleteQuest;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletequest', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dqs', 'delqs', 'deleteqs', 'deletequest'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a quest';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the quest exists
        if (! $session->currentWorld->ivExists('questHash', $name)) {

            return $self->error(
                $session, $inputString,
                'The current world profile doesn\'t have a quest called \'' . $name . '\'',
            );
        }

        # Delete the quest from the world profile
        $session->currentWorld->ivDelete('questHash', $name);
        # Delete the quest from any character profiles, where it is marked as 'solved'
        foreach my $profObj ($session->ivValues('profHash')) {

            if ($profObj->category eq 'char' && $profObj->ivExists('questHash', $name)) {

                $profObj->ivDelete('questHash', $name);
            }
        }

        # The world profile and character profiles keep a running total of quest statistics.
        #   Now we've deleted an existing quest, those statistics must be updated
        $session->currentWorld->updateQuestStats($session);

        return $self->complete($session, $standardCmd, 'Quest \'' . $name . '\' deleted');
    }
}

{ package Games::Axmud::Cmd::ListQuest;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listquest', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lqs', 'listqs', 'listquest'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists all quests';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my (
            $switch, $name, $worldObj, $charObj, $questObj,
            @list,
            %hash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there are some quests to list
        if (! $session->currentWorld->questHash) {

            return $self->error(
                $session, $inputString,
                'The current world profile doesn\'t contain any quests',
            );
        }

        # The world profile and character profiles keep a running total of quest statistics. Before
        #   displaying them, make sure they are up-to-date (they should be, unless the user's code
        #   has been making rogue ->ivPoke calls)
        $session->currentWorld->updateQuestStats($session);

        # Process the argument
        if (defined $arg) {

            if ($arg eq '-q' || $arg eq '-x' || $arg eq '-c') {

                # It's a switch
                $switch = $arg;

            } else {

                # It's a quest name
                $name = $arg;
                if (! $session->currentWorld->ivExists('questHash', $name)) {

                    return $self->error(
                        $session, $inputString,
                        'The \'' . $name . '\' quest doesn\'t exist',
                    );

                } else {

                    $questObj = $session->currentWorld->ivShow('questHash', $name);
                }
            }
        }

        # Import the current world and current character (for convenience)
        $worldObj = $session->currentWorld;
        $charObj = $session->currentChar;

        # ;lqs
        # ;lqs <switch>
        if (! defined $name) {

            # Display header
            $session->writeText('List of quests (* completed by current character)');

            if ($charObj) {


                $session->writeText(
                    '   Completed quests: ' . $charObj->questCount . '(' . $worldObj->questCount
                    . '), earned: points ' . $charObj->questPointCount . '('
                    . $worldObj->questPointCount
                    . ') XP ' . $charObj->questXPCount . '(' . $worldObj->questXPCount
                    . ') cash ' . $charObj->questCashCount . '(' . $worldObj->questCashCount . ')',
                );

            } else {

                $session->writeText(
                    'Quests: ' . $worldObj->questCount
                    . ', worth: points ' . $worldObj->questPointCount
                    . ' XP ' . $worldObj->questXPCount
                    . ' cash ' . $worldObj->questCashCount,
                );
            }

            $session->writeText('   Name             Mission          QP       XP       Cash');

            # Display list. Import the hash of quests
            %hash = $session->currentWorld->questHash;

            # Sort the list
            if (defined $switch) {

                if ($switch eq '-q') {

                    @list = sort {

                        if ($hash{$a}->questPoints == $hash{$b}->questPoints) {

                            # QP equal, so sort alphabetically
                            return (lc($a) cmp lc($b));

                        } else {

                            # Sort by QP
                            return $hash{$a}->questPoints <=> $hash{$b}->questPoints;
                        }

                    } (keys %hash);

                } elsif ($switch eq '-x') {

                    @list = sort {

                        if ($hash{$a}->questXP == $hash{$b}->questXP) {
                            return (lc($a) cmp lc($b));
                        } else {
                            return $hash{$a}->questXP <=> $hash{$b}->questXP;
                        }

                    } (keys %hash);

                } elsif ($switch eq '-c') {

                    @list = sort {

                        if ($hash{$a}->questCash == $hash{$b}->questCash) {
                            return (lc($a) cmp lc($b));
                        } else {
                            return $hash{$a}->questCash <=> $hash{$b}->questCash;
                        }

                    } (keys %hash);
                }

            } else {

                # No switch specified, so sort alphabetically
                @list = sort {lc($a) cmp lc($b)} (keys %hash);
            }

            foreach my $quest (@list) {

                my ($string, $questObj, $mission, $qp, $xp, $cash);

                $questObj = $hash{$quest};

                if (defined $charObj && $charObj->ivExists('questHash', $quest)) {
                    $string = ' * ';        # Solved quest
                } else {
                    $string = '   ';
                }

                if (defined $questObj->missionName) {
                    $mission = $questObj->missionName;
                } else {
                    $mission = '<none>';
                }

                if (defined $questObj->questPoints) {
                    $qp = $questObj->questPoints;
                } else {
                    $qp = 'n/a';
                }

                if (defined $questObj->questXP) {
                    $xp = $questObj->questXP;
                } else {
                    $xp = 'n/a';
                }

                if (defined $questObj->questCash) {
                    $cash = $questObj->questCash;
                } else {
                    $cash = 'n/a';
                }

                $session->writeText(
                    $string . sprintf(
                        '%-16.16s %-16.16s %-8.8s %-8.8s %-8.8s',
                        $quest,
                        $mission,
                        $qp,
                        $xp,
                        $cash,
                    )
                );
            }

            # Display footer
            if (@list == 1) {

                return $self->complete($session, $standardCmd, 'End of list (1 quest found)');

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (' . scalar @list . ' quests found)',
                );
            }

        # ;lqs <name>
        } else {

            if ($charObj && $charObj->ivExists('questHash', $name)) {

                $session->writeText(
                    '\'' . $name . '\' quest (completed by ' . $charObj->name . ')',
                );

            } else {

                $session->writeText('\'' . $name . '\' quest');
            }

            if (defined $questObj->missionName) {
                $session->writeText('   Mission : ' . $questObj->missionName);
            } else {
                $session->writeText('   Mission : <none>');
            }

            $session->writeText('   Points  : ' . $questObj->questPoints);
            $session->writeText('   XP      : ' . $questObj->questXP);
            $session->writeText('   Cash    : ' . $questObj->questCash);

            if ($questObj->solutionList) {

                $session->writeText('   Solution:');
                foreach my $line ($questObj->solutionList) {

                    $session->writeText('      ' . $line);
                }

            } else {

                $session->writeText('   Solution: <empty>');
            }

            if ($questObj->commentList) {

                $session->writeText('   Comments:');
                foreach my $line ($questObj->commentList) {

                    $session->writeText('      ' . $line);
                }

            } else {

                $session->writeText('   Comments: <empty>');
            }

            if ($questObj->privateHash) {

                $session->writeText('   Info:');

                @list = sort {lc($a) cmp lc($b)} ($questObj->ivKeys('privateHash'));
                foreach my $key (@list) {

                    $session->writeText(
                        sprintf(
                            '      %-16.16s %-48.48s', $key, $questObj->ivShow('privateHash', $key),
                        )
                    );
                }
            }

            return $self->complete($session, $standardCmd, 'End of quest display');
        }
    }
}

{ package Games::Axmud::Cmd::AddRoute;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addroute', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['art', 'addroute'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a new pre-defined route';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $useRecordingFlag, $count, $roadFlag, $quickFlag, $circuitFlag, $worldFlag,
            $guildFlag, $raceFlag, $charFlag, $customCategory, $customFlag, $specifiedProf,
            $specifiedFlag, $hopFlag, $start, $stop, $name, $route, $cage, $routeKey, $newObjFlag,
            $routeObj,
            @cmdList,
        );

        # Extract the switch which stands in place of <route>
        ($switch, @args) = $self->extract('-a', 0, @args);
        if (defined $switch) {

            $useRecordingFlag = TRUE;
        }

        # Extract group 1 switches
        $count = 0;
        ($switch, @args) = $self->extract('-o', 0, @args);
        if (defined $switch) {

            $roadFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-q', 0, @args);
        if (defined $switch) {

            $quickFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-t', 0, @args);
        if (defined $switch) {

            $circuitFlag = TRUE;
            $count++;
        }

        if ($count == 0) {

            # Default is a road route
            $roadFlag = TRUE;

        } elsif ($count > 1) {

            # Multiple group 1 switches not allowed
            return $self->error(
                $session, $inputString,
                'The switches -o, -q and -t can\'t be combined',
            );
        }

        # Extract group 2 switches
        $count = 0;

        ($switch, @args) = $self->extract('-w', 0, @args);
        if (defined $switch) {

            $worldFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-g', 0, @args);
        if (defined $switch) {

            $guildFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-r', 0, @args);
        if (defined $switch) {

            $raceFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-c', 0, @args);
        if (defined $switch) {

            $charFlag = TRUE;
            $count++;
        }

        ($switch, $customCategory, @args) = $self->extract('-x', 1, @args);
        if (defined $switch) {

            if (! defined $customCategory) {

                return $self->error(
                    $session, $inputString,
                    'Associate the route with which custom profile category?',
                );

            } else {

                $customFlag = TRUE;
                $count++;
            }
        }

        ($switch, $specifiedProf, @args) = $self->extract('-d', 1, @args);
        if (defined $switch) {

            if (! defined $specifiedProf) {

                return $self->error(
                    $session, $inputString,
                    'Associate the route with which profile?',
                );

            } else {

                $specifiedFlag = TRUE;
                $count++;
            }
        }

        if ($count == 0) {

            # Default profile is the current world
            $worldFlag = TRUE;

        } elsif ($count > 1) {

            # Multiple group 2 switches not allowed
            return $self->error(
                $session, $inputString,
                'The switches -w, -r, -g, -c, -x and -d can\'t be combined',
            );
        }

        # Extract group 3 switches
        ($switch, @args) = $self->extract('-h', 0, @args);
        if (defined $switch) {
            $hopFlag = FALSE;
        } else {
            $hopFlag = TRUE;
        }

        # Extract the remaining arguments
        if ($circuitFlag) {

            $start = shift @args;
            $name = shift @args;

        } else {

            $start = shift @args;
            $stop = shift @args;
        }

        # Now, if -d wasn't used, there should be exactly one argument left (namely <route>). If -d
        #   was used, there should be no arguments left
        if ($useRecordingFlag) {

            # No arguments left
            if (! defined $start || (! defined $name && ! defined $stop)) {

                return $self->improper($session, $inputString);

            } elsif (@args) {

                return $self->error(
                    $session, $inputString,
                    'Too many arguments - did you specify both -a and <route>?',
                );

            } elsif (! $session->recordingList) {

                return $self->error(
                    $session, $inputString,
                    'Can\'t save the current recording as a route because the recording is empty',
                );
            }

        } else {

            # One argument left
            if (! @args || ! defined $start || (! defined $name && ! defined $stop)) {

                return $self->improper($session, $inputString);

            } elsif (@args > 1) {

                return $self->error(
                    $session, $inputString,
                    'Too many arguments - try enclosing the route <inside diamond brackets>',
                );

            } else {

                $route = shift @args;
            }
        }

        # If <start>, <stop> or <name> are invalid, display a complaint
        if ((length ($start) > 16) || $start =~ m/@@@/) {

            return $self->error(
                $session, $inputString,
                'Invalid room tag \'' . $start . '\' (max 16 chars, must not contain \'@@@\')',
            );

        } elsif (defined $stop && ((length ($stop) > 16) || $stop =~ m/@@@/)) {

            return $self->error(
                $session, $inputString,
                'Invalid room tag \'' . $stop . '\' (max 16 chars, must not contain \'@@@\')',
            );

        } elsif (defined $name && ((length ($name) > 16) || $name =~ m/@@@/)) {

            return $self->error(
                $session, $inputString,
                'Invalid circuit name \'' . $name . '\' (max 16 chars, must not contain \'@@@\')',
            );
        }

        # If a current profile was specified, check that it exists
        if (
            ($guildFlag && ! defined $session->currentGuild)
            || ($raceFlag && ! defined $session->currentRace)
            || ($charFlag && ! defined $session->currentChar)
            || ($customFlag && ! $session->ivExists('currentProfHash', $customCategory))
        ) {
            return $self->error(
                $session, $inputString,
                'The specified current profile doesn\'t exist',
            );

        } elsif ($specifiedFlag && ! $session->ivExists('profHash', $specifiedProf)) {

            return $self->error(
                $session, $inputString,
                'The profile \'' . $specifiedProf . '\' doesn\'t exist',
            );
        }

        # If the switch '-a' was specified instead of <route>, we need to construct a route from the
        #   contents of the current recording
        if ($useRecordingFlag) {

            # Extract from the recording only commands
            foreach my $cmd ($session->recordingList) {

                if (substr ($cmd, 0, 1) eq '>') {

                    # This is a world command, not any other kind of instruction, nor a comment or a
                    #   break (only world commands can be used in route objects)
                    push (@cmdList, substr ($cmd, 1));
                }
            }

            # Need to check that the recording contained at least one world command
            if (! @cmdList) {

                return $self->error(
                    $session, $inputString,
                    'Can\'t add the route because the current recording does not contain any world'
                    . ' commands',
                );

            } else {

                # Compile a string like 'n;w;nw;n;up;open door'
                $route = join ($axmud::CLIENT->cmdSep, @cmdList);
            }

        # If <route> is a speedwalk command, check it's a valid speedwalk command
        } elsif (
            index($route, $axmud::CLIENT->constSpeedSigil) == 0
            && ! $session->parseSpeedWalk($route)
        ) {
            return $self->error(
                $session, $inputString,
                'Invalid speedwalk command \'' . $route . '\'',
            );
        }

        # Find the right cage
        if ($worldFlag) {
            $cage = $session->findCurrentCage('route', 'world');
        } elsif ($guildFlag) {
            $cage = $session->findCurrentCage('route', 'guild');
        } elsif ($raceFlag) {
            $cage = $session->findCurrentCage('route', 'race');
        } elsif ($charFlag) {
            $cage = $session->findCurrentCage('route', 'char');
        } elsif ($customFlag) {
            $cage = $session->findCurrentCage('route', $customCategory);
        } elsif ($specifiedFlag) {
            $cage = $session->findCage('route', $specifiedProf);
        }

        # Check the cage actually exists
        if (! defined $cage) {

            return $self->error(
                $session, $inputString,
                'Couldn\'t find a route cage for the specified profile',
            );
        }

        # If the route already exists (for this cage - don't consult other cages), use the existing
        #   GA::Obj::Route; otherwise create a new route object
        if ($roadFlag) {
            $routeKey = 'r_' . $start . '@@@' . $stop;
        } elsif ($quickFlag) {
            $routeKey = 'q_' . $start . '@@@' . $stop;
        } elsif ($circuitFlag) {
            $routeKey = 'c_' . $start . '@@@' . $name;
        }

        $routeObj = $cage->ivShow('routeHash', $routeKey);
        if (! $routeObj) {

            # Need to create a new route object
            $newObjFlag = TRUE;

            if ($roadFlag) {

                $routeObj = Games::Axmud::Obj::Route->new(
                    $session,
                    'road',
                    $start,
                    $stop,
                    $route,
                    $hopFlag,
                );

            } elsif ($quickFlag) {

                $routeObj = Games::Axmud::Obj::Route->new(
                    $session,
                    'quick',
                    $start,
                    $stop,
                    $route,
                    $hopFlag,
                );

            } elsif ($circuitFlag) {

                $routeObj = Games::Axmud::Obj::Route->new(
                    $session,
                    'circuit',
                    $start,
                    $name,
                    $route,
                    $hopFlag,
                );
            }

            if (! $routeObj) {

                return $self->error(
                    $session, $inputString,
                    'General error creating the route object',
                );

            } else {

                # Update the route cage with the new route object
                $cage->ivAdd('routeHash', $routeKey, $routeObj);
            }

        } else {

            # Modify an existing route object
            $routeObj->ivPoke('route', $route);
            $routeObj->ivPoke('hopFlag', $hopFlag);

            # Set the number of steps (individual commands on the route)
            $routeObj->resetStepCount($session);
        }

        if ($roadFlag) {

            if (! $newObjFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Replaced the road route between \'' . $start . '\' and \'' . $stop
                    . '\' with \'' . $route . '\' (profile: \'' . $cage->profName . '\')',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Added a road route between \'' . $start . '\' and \'' . $stop . '\' using \''
                    . $route . '\' (profile: \'' . $cage->profName . '\')',
                );
            }

        } elsif ($quickFlag) {

            if (! $newObjFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Replaced the quick route between \'' . $start . '\' and \'' . $stop
                    . '\' with \'' . $route . '\' (profile: \'' . $cage->profName . '\')',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Added a quick route between \'' . $start . '\' and \'' . $stop . '\' using \''
                    . $route . '\' (profile: \'' . $cage->profName . '\')',
                );
            }

        } elsif ($circuitFlag) {

            if (! $newObjFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Replaced the circuit route \'' . $name . '\' starting from \'' . $start
                    . '\' with \'' . $route . '\' (profile: \'' . $cage->profName . '\')',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Added the circuit route \'' . $name . '\' starting from \'' . $start
                    . '\' using \'' . $route . '\' (profile: \'' . $cage->profName . '\')',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::EditRoute;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editroute', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ert', 'editroute'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a pre-defined route';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $count, $roadFlag, $quickFlag, $circuitFlag, $worldFlag, $guildFlag, $raceFlag,
            $charFlag, $customCategory, $customFlag, $specifiedProf, $specifiedFlag, $start, $stop,
            $name, $cage, $routeKey, $routeObj, $string,
        );

        # Extract group 1 switches
        $count = 0;
        ($switch, @args) = $self->extract('-o', 0, @args);
        if (defined $switch) {

            $roadFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-q', 0, @args);
        if (defined $switch) {

            $quickFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-t', 0, @args);
        if (defined $switch) {

            $circuitFlag = TRUE;
            $count++;
        }

        if ($count == 0) {

            # Default is a road route
            $roadFlag = TRUE;

        } elsif ($count > 1) {

            # Multiple group 1 switches not allowed
            return $self->error(
                $session, $inputString,
                'The switches -o, -q and -t can\'t be combined',
            );
        }

        # Extract group 2 switches
        $count = 0;

        ($switch, @args) = $self->extract('-w', 0, @args);
        if (defined $switch) {

            $worldFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-g', 0, @args);
        if (defined $switch) {

            $guildFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-r', 0, @args);
        if (defined $switch) {

            $raceFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-c', 0, @args);
        if (defined $switch) {

            $charFlag = TRUE;
            $count++;
        }

        ($switch, $customCategory, @args) = $self->extract('-x', 1, @args);
        if (defined $switch) {

            if (! defined $customCategory) {

                return $self->error(
                    $session, $inputString,
                    'Edit the route associated with which custom profile category?',
                );

            } else {

                $customFlag = TRUE;
                $count++;
            }
        }

        ($switch, $specifiedProf, @args) = $self->extract('-d', 1, @args);
        if (defined $switch) {

            if (! defined $specifiedProf) {

                return $self->error(
                    $session, $inputString,
                    'Edit the route associated with which profile?',
                );

            } else {

                $specifiedFlag = TRUE;
                $count++;
            }
        }

        if ($count == 0) {

            # Default profile is the current world
            $worldFlag = TRUE;

        } elsif ($count > 1) {

            # Multiple group 2 switches not allowed
            return $self->error(
                $session, $inputString,
                'The switches -w, -r, -g, -c, -x and -d can\'t be combined',
            );
        }

        # Extract the remaining arguments
        if ($circuitFlag) {

            $start = shift @args;
            $name = shift @args;

        } else {

            $start = shift @args;
            $stop = shift @args;
        }

        # There should now be no arguments left
        if (@args || ! defined $start || (! defined $name && ! defined $stop)) {

            return $self->improper($session, $inputString);
        }

        # If a current profile was specified, check that it exists
        if (
            ($guildFlag && ! defined $session->currentGuild)
            || ($raceFlag && ! defined $session->currentRace)
            || ($charFlag && ! defined $session->currentChar)
            || ($customFlag && ! $session->ivExists('currentProfHash', $customCategory))
        ) {
            return $self->error(
                $session, $inputString,
                'The specified current profile doesn\'t exist',
            );

        } elsif ($specifiedFlag && ! $session->ivExists('profHash', $specifiedProf)) {

            return $self->error(
                $session, $inputString,
                'The profile \'' . $specifiedProf . '\' doesn\'t exist',
            );
        }

        # Find the right cage
        if ($worldFlag) {
            $cage = $session->findCurrentCage('route', 'world');
        } elsif ($guildFlag) {
            $cage = $session->findCurrentCage('route', 'guild');
        } elsif ($raceFlag) {
            $cage = $session->findCurrentCage('route', 'race');
        } elsif ($charFlag) {
            $cage = $session->findCurrentCage('route', 'char');
        } elsif ($customFlag) {
            $cage = $session->findCurrentCage('route', $customCategory);
        } elsif ($specifiedFlag) {
            $cage = $session->findCage('route', $specifiedProf);
        }

        # Check the cage actually exists
        if (! defined $cage) {

            return $self->error(
                $session, $inputString,
                'Couldn\'t find a route cage for the specified profile',
            );
        }

        # Check the specified GA::Obj::Route exists
        if ($roadFlag) {
            $routeKey = 'r_' . $start . '@@@' . $stop;
        } elsif ($quickFlag) {
            $routeKey = 'q_' . $start . '@@@' . $stop;
        } elsif ($circuitFlag) {
            $routeKey = 'c_' . $start . '@@@' . $name;
        }

        $routeObj = $cage->ivShow('routeHash', $routeKey);
        if (! $routeObj) {

            if ($roadFlag) {

                return $self->error(
                    $session, $inputString,
                    'Couldn\'t find a road route starting at \'' . $start . '\' and stopping at \''
                    . $stop . '\' for the specified profile',
                );

            } elsif ($quickFlag) {

                return $self->error(
                    $session, $inputString,
                    'Couldn\'t find a quick route starting at \'' . $start . '\' and stopping at \''
                    . $stop . '\' for the specified profile',
                );

            } elsif ($circuitFlag) {

                return $self->error(
                    $session, $inputString,
                    'Couldn\'t find any route starting at \'' . $start . '\' called \'' . $name
                    . '\' for the specified profile',
                );
            }

        } else {

            # Open an 'edit' window for the route
            $string = 'Edit ' . $routeObj->routeType . ' route';
            if ($routeObj->routeType eq 'circuit') {
                $string .= ' \'' . $routeObj->circuitName . '\'';
            } else {
                $string .= ' (' . $routeObj->startRoom . ' > ' . $routeObj->stopRoom . ')';
            }

            if (
                ! $session->mainWin->createFreeWin(
                    'Games::Axmud::EditWin::Route',
                    $session->mainWin,
                    $session,
                    $string,
                    $routeObj,
                    FALSE,                  # Not temporary
                )
            ) {
                return $self->error(
                    $session, $inputString,
                    'Could not edit the specified ' . $routeObj->routeType . ' route',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Opened an \'edit\' window for the specified ' . $routeObj->routeType
                    . ' route',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::DeleteRoute;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteroute', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['drt', 'delroute', 'deleteroute'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a pre-defined route';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $count, $roadFlag, $quickFlag, $circuitFlag, $worldFlag, $guildFlag, $raceFlag,
            $charFlag, $customCategory, $customFlag, $specifiedProf, $specifiedFlag, $start, $stop,
            $name, $cage, $routeKey,
        );

        # Extract group 1 switches
        $count = 0;
        ($switch, @args) = $self->extract('-o', 0, @args);
        if (defined $switch) {

            $roadFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-q', 0, @args);
        if (defined $switch) {

            $quickFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-t', 0, @args);
        if (defined $switch) {

            $circuitFlag = TRUE;
            $count++;
        }

        if ($count == 0) {

            # Default is a road route
            $roadFlag = TRUE;

        } elsif ($count > 1) {

            # Multiple group 1 switches not allowed
            return $self->error(
                $session, $inputString,
                'The switches -o, -q and -t can\'t be combined',
            );
        }

        # Extract group 2 switches
        $count = 0;

        ($switch, @args) = $self->extract('-w', 0, @args);
        if (defined $switch) {

            $worldFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-g', 0, @args);
        if (defined $switch) {

            $guildFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-r', 0, @args);
        if (defined $switch) {

            $raceFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-c', 0, @args);
        if (defined $switch) {

            $charFlag = TRUE;
            $count++;
        }

        ($switch, $customCategory, @args) = $self->extract('-x', 1, @args);
        if (defined $switch) {

            if (! defined $customCategory) {

                return $self->error(
                    $session, $inputString,
                    'Delete the route associated with which custom profile category?',
                );

            } else {

                $customFlag = TRUE;
                $count++;
            }
        }

        ($switch, $specifiedProf, @args) = $self->extract('-d', 1, @args);
        if (defined $switch) {

            if (! defined $specifiedProf) {

                return $self->error(
                    $session, $inputString,
                    'Delete the route associated with which profile?',
                );

            } else {

                $specifiedFlag = TRUE;
                $count++;
            }
        }

        if ($count == 0) {

            # Default profile is the current world
            $worldFlag = TRUE;

        } elsif ($count > 1) {

            # Multiple group 2 switches not allowed
            return $self->error(
                $session, $inputString,
                'The switches -w, -r, -g, -c, -x and -d can\'t be combined',
            );
        }

        # Extract the remaining arguments
        if ($circuitFlag) {

            $start = shift @args;
            $name = shift @args;

        } else {

            $start = shift @args;
            $stop = shift @args;
        }

        # There should now be no arguments left
        if (@args || ! defined $start || (! defined $name && ! defined $stop)) {

            return $self->improper($session, $inputString);
        }

        # If a current profile was specified, check that it exists
        if (
            ($guildFlag && ! defined $session->currentGuild)
            || ($raceFlag && ! defined $session->currentRace)
            || ($charFlag && ! defined $session->currentChar)
            || ($customFlag && ! $session->ivExists('currentProfHash', $customCategory))
        ) {
            return $self->error(
                $session, $inputString,
                'The specified current profile doesn\'t exist',
            );

        } elsif ($specifiedFlag && ! $session->ivExists('profHash', $specifiedProf)) {

            return $self->error(
                $session, $inputString,
                'The profile \'' . $specifiedProf . '\' doesn\'t exist',
            );
        }

        # Find the right cage
        if ($worldFlag) {
            $cage = $session->findCurrentCage('route', 'world');
        } elsif ($guildFlag) {
            $cage = $session->findCurrentCage('route', 'guild');
        } elsif ($raceFlag) {
            $cage = $session->findCurrentCage('route', 'race');
        } elsif ($charFlag) {
            $cage = $session->findCurrentCage('route', 'char');
        } elsif ($customFlag) {
            $cage = $session->findCurrentCage('route', $customCategory);
        } elsif ($specifiedFlag) {
            $cage = $session->findCage('route', $specifiedProf);
        }

        # Check the cage actually exists
        if (! defined $cage) {

            return $self->error(
                $session, $inputString,
                'Couldn\'t find a route cage for the specified profile',
            );
        }

        # Check the specified GA::Obj::Route exists
        if ($roadFlag) {
            $routeKey = 'r_' . $start . '@@@' . $stop;
        } elsif ($quickFlag) {
            $routeKey = 'q_' . $start . '@@@' . $stop;
        } elsif ($circuitFlag) {
            $routeKey = 'c_' . $start . '@@@' . $name;
        }

        if (! $cage->ivExists('routeHash', $routeKey)) {

            if ($roadFlag) {

                return $self->error(
                    $session, $inputString,
                    'Couldn\'t find a road route starting at \'' . $start . '\' and stopping at \''
                    . $stop . '\' for the specified profile',
                );

            } elsif ($quickFlag) {

                return $self->error(
                    $session, $inputString,
                    'Couldn\'t find a quick route starting at \'' . $start . '\' and stopping at \''
                    . $stop . '\' for the specified profile',
                );

            } elsif ($circuitFlag) {

                return $self->error(
                    $session, $inputString,
                    'Couldn\'t find any route starting at \'' . $start . '\' called \'' . $name
                    . '\' for the specified profile',
                );
            }

        } else {

            # Delete the route
            $cage->ivDelete('routeHash', $routeKey);

            if ($roadFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Deleted the road route between \'' . $start . '\' and \'' . $stop
                    . '\' (profile: \'' . $cage->profName . '\')',
                );

            } elsif ($quickFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Deleted the quick route between \'' . $start . '\' and \'' . $stop
                    . '\' (profile: \'' . $cage->profName . '\')',
                );

            } elsif ($circuitFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Deleted the circuit route from \'' . $start . '\' called \'' . $stop
                    . '\' (profile: \'' . $cage->profName . '\')',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ListRoute;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listroute', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lrt', 'listroute'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists pre-defined routes';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $count, $worldFlag, $guildFlag, $raceFlag, $charFlag, $customCategory,
            $customFlag, $specifiedProf, $specifiedFlag, $allFlag, $start, $cage, $header,
            @routeObjList, @newList, @startRoomList,
            %startRoomHash,
        );

        # Extract (group 2) switches
        $count = 0;

        ($switch, @args) = $self->extract('-w', 0, @args);
        if (defined $switch) {

            $worldFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-g', 0, @args);
        if (defined $switch) {

            $guildFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-r', 0, @args);
        if (defined $switch) {

            $raceFlag = TRUE;
            $count++;
        }

        ($switch, @args) = $self->extract('-c', 0, @args);
        if (defined $switch) {

            $charFlag = TRUE;
            $count++;
        }

        ($switch, $customCategory, @args) = $self->extract('-x', 1, @args);
        if (defined $switch) {

            if (! defined $customCategory) {

                return $self->error(
                    $session, $inputString,
                    'List routes associated with which custom profile category?',
                );

            } else {

                $customFlag = TRUE;
                $count++;
            }
        }

        ($switch, $specifiedProf, @args) = $self->extract('-d', 1, @args);
        if (defined $switch) {

            if (! defined $specifiedProf) {

                return $self->error(
                    $session, $inputString,
                    'List routes associated with with which profile?',
                );

            } else {

                $specifiedFlag = TRUE;
                $count++;
            }
        }

        if ($count == 0) {

            # Show routes from all current profiles
            $allFlag = TRUE;

        } elsif ($count > 1) {

            # Multiple group 2 switches not allowed
            return $self->error(
                $session, $inputString,
                'The switches -w, -r, -g, -c, -x and -d can\'t be combined',
            );
        }

        # There should now be 0 or 1 arguments left
        if (@args > 1) {

            return $self->improper($session, $inputString);

        } elsif (@args) {

            $start = $args[0];
        }

        # If a current profile was specified, check that it exists
        if (
            ($guildFlag && ! defined $session->currentGuild)
            || ($raceFlag && ! defined $session->currentRace)
            || ($charFlag && ! defined $session->currentChar)
            || ($customFlag && ! $session->ivExists('currentProfHash', $customCategory))
        ) {
            return $self->error(
                $session, $inputString,
                'The specified current profile doesn\'t exist',
            );

        } elsif ($specifiedFlag && ! $session->ivExists('profHash', $specifiedProf)) {

            return $self->error(
                $session, $inputString,
                'The profile \'' . $specifiedProf . '\' doesn\'t exist',
            );
        }

        # Find the right cage. If a profile was specified, find its route cage
        if ($worldFlag) {
            $cage = $session->findCurrentCage('route', 'world');
        } elsif ($guildFlag) {
            $cage = $session->findCurrentCage('route', 'guild');
        } elsif ($raceFlag) {
            $cage = $session->findCurrentCage('route', 'race');
        } elsif ($charFlag) {
            $cage = $session->findCurrentCage('route', 'char');
        } elsif ($customFlag) {
            $cage = $session->findCurrentCage('route', $customCategory);
        } elsif ($specifiedFlag) {
            $cage = $session->findCage('route', $specifiedProf);
        }

        # Check the cage actually exists
        if (! $allFlag && ! defined $cage) {

            return $self->error(
                $session, $inputString,
                'Couldn\'t find a route cage for the specified profile',
            );
        }

        # If all profiles were specified, go through all the route cages associated with current
        #   profiles - starting with the lowest priority profile, the world profile whose route cage
        #   is $cage - and get an ordered list of routes
        # If $start was not specified, all routes are returned; if $start was specified, only routes
        #   beginning at that room are returned
        if ($allFlag) {

            @routeObjList = $session->worldModelObj->listAvailableRoutes($session, $start);

        # For a specified profile, get all routes stored in the profile's route cage
        } else {

            @routeObjList = $cage->ivValues('routeHash');

            # If $start was specified, eliminate those routes starting at the wrong
            #   location
            if ($start) {

                foreach my $obj (@routeObjList) {

                    if ($obj->startRoom eq $start) {

                        push (@newList, $obj);
                    }
                }

                @routeObjList = @newList;
            }
        }

        if (! @routeObjList) {

            if ($start) {

                return $self->error(
                    $session, $inputString,
                    'No routes from \'' . $start . '\' found',
                );

            } else {

                return $self->error($session, $inputString, 'No routes found');
            }
        }

        # Compile a hash of start rooms, in the form
        #   $startRoomHash{start_room_tag} = undef
        if ($start) {

            # Hash holds one entry only
            $startRoomHash{$start} = undef;

        } else {

            # Hash holds one entry for every possible start room
            foreach my $obj (@routeObjList) {

                $startRoomHash{$obj->startRoom} = undef;
            }
        }

        # Get a sorted list of start rooms
        @startRoomList = sort {lc($a) cmp lc($b)} (keys %startRoomHash);

        # Now, process @routeObjList so we can display an ordered list of routes
        # Each key in %startRoomHash is given a corresponding value set to a list reference. The
        #   list contains all routes starting at the start room matching the key
        foreach my $routeObj (@routeObjList) {

            push (@{$startRoomHash{$routeObj->startRoom}}, $routeObj);
        }

        # Display header
        $header = 'List of pre-defined routes';
        if ($allFlag) {

            if ($start) {
                $header .= ' (all profiles, start: ' . $start . ')';
            } else {
                $header .= ' (all profiles)';
            }

        } else {

            if ($start) {
                $header .= ' (profile: ' . $cage->profName . ', start: ' . $start . ')';
            } else {
                $header .= ' (profile: ' . $cage->profName . ')';
            }
        }

        $session->writeText($header . ' (* - hoppable)');

        # Display list
        $count = 0;

        # Display all routes grouped by their start room
        OUTER: foreach my $startRoom (@startRoomList) {

            my (@list, @roadList, @quickList, @circuitList, @sortedList);

            # Get a list of route objects starting at $startRoom
            @list = @{$startRoomHash{$startRoom}};
            # Split this list into road, quick and circuit routes
            foreach my $routeObj (@list) {

                if ($routeObj->routeType eq 'road') {
                    push (@roadList, $routeObj);
                } elsif ($routeObj->routeType eq 'quick') {
                    push (@quickList, $routeObj);
                } else {
                    push (@circuitList, $routeObj);
                }
            }

            # Display each list in turn
            $session->writeText('   Room \'' . $startRoom . '\'');

            if (@roadList) {

                @sortedList = sort {lc($a->stopRoom) cmp lc($b->stopRoom)} (@roadList);
                $session->writeText('      Road routes');

                foreach my $routeObj (@sortedList) {

                    $self->displayColumns($session, $routeObj);
                }

                $count++;
            }

            if (@quickList) {

                @sortedList = sort {lc($a->stopRoom) cmp lc($b->stopRoom)} (@quickList);
                $session->writeText('      Quick routes');

                foreach my $routeObj (@sortedList) {

                    $self->displayColumns($session, $routeObj);
                }

                $count++;
            }

            if (@circuitList) {

                @sortedList = sort {lc($a->circuitName) cmp lc($b->circuitName)} (@circuitList);
                $session->writeText('      Circuit routes');

                foreach my $routeObj (@sortedList) {

                    $self->displayColumns($session, $routeObj);
                }

                $count++;
            }
        }

        # Display footer
        if ($count == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 route displayed)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . $count . ' routes displayed)',
            );
        }
    }

    sub displayColumns {

        # Called by $self->do
        # Displays information about a single GA::Obj::Route, splitting the route (which could well
        #   be longer than the reasonable width of the 'main' window) into columns
        #
        # Expected arguments
        #   $session    - The calling function's GA::Session
        #   $routeObj   - A GA::Obj::Route to display
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $session, $routeObj, $check) = @_;

        # Local variables
        my ($count, $route, $piece, $string);

        # Check for improper arguments
        if (! defined $session || ! defined $routeObj || defined $check)  {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->displayColumns', @_);
        }

        $route = $routeObj->route;
        if ($routeObj->hopFlag) {
            $string = '*';
        } else {
            $string = ' ';
        }

        $count = 0;
        do {

            $count++;

            # Remove the first 64 characters remaining in $route
            if (length $route > 64) {

                $piece = substr($route, 0, 64);
                $route = substr($route, 64);

            } else {

                $piece = $route;
                $route = '';
            }

            if ($count == 1) {

                if ($routeObj->routeType eq 'circuit') {

                    $session->writeText(
                        sprintf(
                            '    %-1.1s >%-16.16s %-64.64s',
                            $string,
                            $routeObj->circuitName,
                            $piece,
                        )
                    );

                } else {

                    $session->writeText(
                        sprintf(
                            '    %-1.1s >%-16.16s %-64.64s',
                            $string,
                            $routeObj->stopRoom,
                            $piece,
                        )
                    );
                }

            } else {

                $session->writeText(sprintf('                        %-64.64s', $piece));
            }

        } until (! $route);

        return 1;
    }
}

{ package Games::Axmud::Cmd::Go;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('go', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['go'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Moves the character to a new room';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $numberFlag, $room, $roomTag, $roomNum, $roomObj, $label, $result, $dictObj,
            $roomListRef, $exitListRef,
            @roomList, @exitList, @cmdList, @modList,
        );

        # Extract the optional switch
        ($switch, @args) = $self->extract('-n', 0, @args);
        if (defined $switch) {

            $numberFlag = TRUE;
        }

        # Extract the remaining argument
        $room = shift @args;

        # Check for improper arguments
        if (! defined $room || @args) {

            return $self->improper($session, $inputString);
        }

        # Is $room a tag that the world model recognises?
        if (! $numberFlag) {

            if ($session->worldModelObj->ivExists('roomTagHash', lc($room))) {

                # It's a room tag
                $roomTag = $room;
                $roomNum = $session->worldModelObj->ivShow('roomTagHash', lc($room));
                $roomObj = $session->worldModelObj->ivShow('modelHash', $roomNum);

            } elsif (! $axmud::CLIENT->intCheck($room)) {

                # $room contains non-numeric characters, so it can't be a room number

                return $self->error(
                    $session, $inputString,
                    'The world model doesn\'t have a room tagged \'' . $room . '\'',
                );
            }
        }

        # Is $room a room number?
        if (! $roomTag) {

            if (! $session->worldModelObj->ivExists('modelHash', $room)) {

                # Unrecognised room number
                return $self->error(
                    $session, $inputString,
                    'The world model doesn\'t have a room #' . $room,
                );

            } else {

                # Check it's a room, and not something else
                $roomObj = $session->worldModelObj->ivShow('modelHash', $room);
                if ($roomObj->category ne 'room') {

                    if ($roomObj->category eq 'armour') {

                        return $self->error(
                            $session, $inputString,
                            'The world model object #' . $room . ' is a \'armour\', not a room'
                            . '\', not a room',
                        );

                    } else {

                        return $self->error(
                            $session, $inputString,
                            'The world model object #' . $room . ' is a \'' . $roomObj->category
                            . '\', not a room',
                        );
                    }

                } else {

                    $roomNum = $roomObj->number;
                    $roomTag = $roomObj->roomTag;   # May be 'undef'
                }
            }
        }

        # In any further messages, use the room tag if we can, or the room number otherwise
        if ($roomTag) {
            $label = '\'' . $roomTag . '\'';
        } else {
            $label = '#' . $roomNum;
        }

        # If the current location isn't known, we can't move the character
        if (! $session->mapObj->currentRoom) {

            return $self->error(
                $session, $inputString,
                'Can\'t go to ' . $label . ' because the character\'s current location isn\'t'
                . ' known (try \';setroom\' first)',
            );
        }

        # Check that we're not already in the room...
        if ($session->mapObj->currentRoom eq $roomObj) {

            return $self->error(
                $session, $inputString,
                'The character\'s current location is already set to ' . $label,
            );
        }

        # If the Automapper window is open, use its code to move to the room (so that the route is
        #   highlighted, and so on)
        if ($session->mapWin) {

            # Select the destination room
            $session->mapWin->setSelectedObj(
                [$roomObj, 'room'],
                FALSE,          # Cancel any currently-selected objects
            );

            # Move to the destination room ('send_char' - move character)
            if (! $session->mapWin->processPathCallback('send_char')) {

                return $self->error(
                    $session, $inputString,
                    'Could not move the character to ' . $label,
                );

            } else {

                return $self->complete($session, $standardCmd, 'Going to ' . $label);
            }

        } else {

            # Otherwise, ask the world model directly to provide the route (code adapted from
            #   GA::Win::Map->processPathCallback)

            # Use the universal version of the A* algorithm to find a path between the current and
            #   selected rooms (if they're in the same region, the call is automatically redirected
            #   to ->findPath)
            ($roomListRef, $exitListRef) = $session->worldModelObj->findUniversalPath(
                $session,
                $session->mapObj->currentRoom,
                $roomObj,
                $session->worldModelObj->avoidHazardsFlag,
            );

            if (! defined $roomListRef || ! @$roomListRef) {

                return $self->error(
                    $session, $inputString,
                    'Could not find a path between the current room and ' . $label,
                );
            }

            # (Don't bother to apply post-processing to smooth jagged edges - the user can't see
            #   them, anyway)

            # Convert the list references returned by the called functions into lists
            @roomList = @$roomListRef;
            @exitList = @$exitListRef;

            # Compile a list of commands to get from one end of the route to the other. If assisted
            #   moves are turned on, use them; otherwise, use each exit's nominal direction
            @cmdList = $session->worldModelObj->convertExitList($session, @exitList);
            if (! @cmdList) {

                # @cmdList shouldn't ever be empty, but just to be safe...
                return $self->error(
                    $session, $inputString,
                    'General pathfinding error, could not move to ' . $label,
                );
            }

            # Move the character, abbreviating any primary/secondary directions, if possible
            # Compile a single string of world commands (e.g. 'north;east;north'), which allows
            #   GA::Session->worldCmd to avoid drawing the automapper's ghost room dozens or
            #   hundreds of times (and matches the behaviour of ';drive', etc, which also make a
            #   single call to GA::Session->worldCmd)
            $dictObj = $session->currentDict;
            foreach my $cmd (@cmdList) {

                push (@modList, $dictObj->abbrevDir($cmd));
            }

            $session->worldCmd(join($axmud::CLIENT->cmdSep, @modList));

            if (@cmdList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Going to ' . $label . ' (in 1 step)',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Going to ' . $label . ' (in ' . scalar @cmdList . ' steps)',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::Drive;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('drive', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['drv', 'drive'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Moves the character using pre-defined routes';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $start, $stop,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $start || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;drive <stop>
        if (! defined $stop) {

            # If the user didn't specify <start>, we have to get it from the Locator task. Check
            #   the Locator is running, knows its current location, and that the current location
            #   has a room tag set
            if (! $session->locatorTask) {

                return $self->error(
                    $session, $inputString,
                   'The Locator task isn\'t running (try \';drive <start> <stop>\' instead',
                );

            } elsif (! $session->locatorTask->roomObj) {

                return $self->error(
                    $session, $inputString,
                   'The Locator task doesn\'t know about the character\'s location (try'
                   . ' \';drive <start> <stop>\' instead',
                );

            } elsif (! $session->locatorTask->roomObj->roomTag) {

                return $self->error(
                    $session, $inputString,
                    'Can\'t use pre-defined routes: the Locator task\'s current room doesn\'t have'
                    . ' a room tag set',
                );

            } else {

                $stop = $start;
                $start = $session->locatorTask->roomObj->roomTag;

                if ($start eq $stop) {

                    return $self->error(
                        $session, $inputString,
                        'The character is already at \'' . $stop . '\'',
                    );
                }
            }
        }

        # ;drive <start> <stop>
        # ;drive <stop>
        return $self->useRoute($session, $inputString, $standardCmd, $start, $stop, 'both');
    }
}

{ package Games::Axmud::Cmd::Road;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('road', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['road'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Moves the character using pre-defined road routes';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $start, $stop,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $start || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;road <stop>
        if (! defined $stop) {

            # If the user didn't specify <start>, we have to get it from the Locator task. Check
            #   the Locator is running, knows its current location, and that the current location
            #   has a room tag set
            if (! $session->locatorTask) {

                return $self->error(
                    $session, $inputString,
                   'The Locator task isn\'t running (try \';road <start> <stop>\' instead',
                );

            } elsif (! $session->locatorTask->roomObj) {

                return $self->error(
                    $session, $inputString,
                   'The Locator task doesn\'t know about the character\'s location (try'
                   . ' \';road <start> <stop>\' instead',
                );

            } elsif (! $session->locatorTask->roomObj->roomTag) {

                return $self->error(
                    $session, $inputString,
                    'Can\'t use pre-defined routes: the Locator task\'s current room doesn\'t have'
                    . ' a room tag set',
                );

            } else {

                $stop = $start;
                $start = $session->locatorTask->roomObj->roomTag;

                if ($start eq $stop) {

                    return $self->error(
                        $session, $inputString,
                        'The character is already at \'' . $stop . '\'',
                    );
                }
            }
        }

        # ;road <start> <stop>
        # ;road <stop>
        return $self->useRoute($session, $inputString, $standardCmd, $start, $stop, 'road');
    }
}

{ package Games::Axmud::Cmd::Quick;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('quick', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['quick'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Moves the character using pre-defined quick routes';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $start, $stop,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $start || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;quick <stop>
        if (! defined $stop) {

            # If the user didn't specify <start>, we have to get it from the Locator task. Check
            #   the Locator is running, knows its current location, and that the current location
            #   has a room tag set
            if (! $session->locatorTask) {

                return $self->error(
                    $session, $inputString,
                   'The Locator task isn\'t running (try \';quick <start> <stop>\' instead',
                );

            } elsif (! $session->locatorTask->roomObj) {

                return $self->error(
                    $session, $inputString,
                   'The Locator task doesn\'t know about the character\'s location (try'
                   . ' \';quick <start> <stop>\' instead',
                );

            } elsif (! $session->locatorTask->roomObj->roomTag) {

                return $self->error(
                    $session, $inputString,
                    'Can\'t use pre-defined routes: the Locator task\'s current room doesn\'t have'
                    . ' a room tag set',
                );

            } else {

                $stop = $start;
                $start = $session->locatorTask->roomObj->roomTag;

                if ($start eq $stop) {

                    return $self->error(
                        $session, $inputString,
                        'The character is already at \'' . $stop . '\'',
                    );
                }
            }
        }

        # ;quick <start> <stop>
        # ;quick <stop>
        return $self->useRoute($session, $inputString, $standardCmd, $start, $stop, 'quick');
    }
}

{ package Games::Axmud::Cmd::Circuit;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('circuit', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cct', 'circuit'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Moves the character along a circuit route';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $start, $name,
            $check,
        ) = @_;

        # Local variables
        my $routeObj;

        # Check for improper arguments
        if (! defined $start || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;circuit <name>
        if (! defined $name) {

            # If the user didn't specify <start>, we have to get it from the Locator task. Check
            #   the Locator is running, knows its current location, and that the current location
            #   has a room tag set
            if (! $session->locatorTask) {

                return $self->error(
                    $session, $inputString,
                   'The Locator task isn\'t running (try \';circuit <start> <name>\' instead',
                );

            } elsif (! $session->locatorTask->roomObj) {

                return $self->error(
                    $session, $inputString,
                   'The Locator task doesn\'t know about the character\'s location (try'
                   . ' \';circuit <start> <stop>\' instead',
                );

            } elsif (! $session->locatorTask->roomObj->roomTag) {

                return $self->error(
                    $session, $inputString,
                    'Can\'t use pre-defined routes: the Locator task\'s current room doesn\'t have'
                    . ' a room tag set',
                );

            } else {

                $name = $start;
                $start = $session->locatorTask->roomObj->roomTag;
            }
        }

        # ;circuit <start> <name>
        # ;circuit <name>

        # Look for a 'circuit' route object starting at the tagged room <start>, using a circuit
        #   called <name>. Start searching in the highest-priority route cage
        OUTER: foreach my $category ($session->profPriorityList) {

            my $cage = $session->findCurrentCage('route', $category);
            if ($cage) {

                # Route objects are stored in the route cage's ->routeHash IV. Keys are in the form:
                #   c_start-room-tag@@@stop-room-tag (for circuit routes)

                # Find the route from this cage, or from inferior cages (if necessary)
                $routeObj = $cage->ivShow(
                    'routeHash',
                    'c_' . $start . '@@@' . $name,
                    $session,                           # Consult inferior cages
                );

                last OUTER;
            }
        }

        if (! $routeObj) {

            return $self->error(
                $session, $inputString,
                'No circuit route from \'' . $start . '\' called \'' . $name . '\' found',
            );
        }

        # Check that the route object actually contains a circuit
        if (! $routeObj->route) {

            return $self->error(
                $session, $inputString,
                   'General error processing the circuit route from \'' . $start . '\' called \''
                   . $name . '\'',
            );
        }

        # If the Locator task is running, tell it about the target room's tag
        if ($session->locatorTask) {

            $session->locatorTask->set_arrivalTag($start);
        }

        # Take the route
        if (index($routeObj->route, $axmud::CLIENT->constSpeedSigil) == 0) {
            $session->speedWalkCmd($routeObj->route);
        } else {
            $session->worldCmd($routeObj->route);
        }

        if ($routeObj->stepCount == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Taking circuit route from \'' . $start . '\' called \'' . $name . '\' (in 1 step)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Taking circuit route from \'' . $start . '\' called \'' . $name . '\' (in '
                . $routeObj->stepCount . ' steps)',
            );
        }
    }
}

# Advance task

{ package Games::Axmud::Cmd::ResetGuildSkills;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resetguildskills', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rgs', 'resetgs', 'resetguildskills'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets the character\'s guild skills';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that there is a current guild and character
        if (! $session->currentGuild) {

            return $self->error(
                $session, $inputString,
                'Can\'t list skills - there is no current guild profile',
            );

        } elsif (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t list skills - there is no current character profile',
            );
        }

        # Reset skills
        if (! $session->currentChar->resetSkills($session)) {

            return $self->error(
                $session, $inputString,
                'Current character\'s skills couldn\'t be reset',
            );

        } else {

            return $self->complete($session, $standardCmd, 'Current character\'s skills reset');
        }
    }
}

{ package Games::Axmud::Cmd::ListGuildSkills;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listguildskills', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lgs', 'listgs', 'listguildskills'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists guild skills for the current character';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $guildObj, $charObj, $count,
            @guildList,
            %charHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that there is a current guild and character
        if (! $session->currentGuild) {

            return $self->error(
                $session, $inputString,
                'Can\'t list skills - there is no current guild profile',
            );

        } elsif (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t list skills - there is no current character profile',
            );

        } else {

            # Import the profiles
            $guildObj = $session->currentGuild;
            $charObj = $session->currentChar;
        }

        # Check there are some skills to list
        if (! $guildObj->skillList) {

            return $self->error(
                $session, $inputString,
                'No skills defined for the current guild (\'' . $guildObj->name . '\')',
            );

        } elsif (! $charObj->skillLevelHash) {

            # Let's assume the user knows which character is the current one...
            return $self->error(
                $session, $inputString,
                'No skills defined for the current character',
            );
        }

        # Display header
        $session->writeText(
            'List of skills for \'' . $charObj->name . '\' (guild \'' . $guildObj->name . '\')',
        );

        # Display list

        # Import list of skills from this guild - should already be in a sensible order
        @guildList = $guildObj->skillList;
        # Import hash of skills from the character, so we can check them off (and show a warning if,
        #   for some reason, any skill is absent in the guild's list)
        %charHash = $charObj->skillLevelHash;

        $count = 0;
        foreach my $skill (@guildList) {

            $count++;

            $session->writeText('   ' . $skill);

            if (! exists $charHash{$skill}) {

                $session->writeText('      (missing from character\'s skill list)');

            } else {

                $session->writeText(
                    '      Level: ' . $charObj->ivShow('skillLevelHash', $skill)
                    . ' (Max: ' . $guildObj->ivShow('skillMaxLevelHash', $skill)
                    . ') Times advanced: ' . $charObj->ivShow('skillAdvanceCountHash', $skill),
                );

                $session->writeText(
                    '      XP spent: ' . $charObj->ivShow('skillTotalXPHash', $skill)
                    . ' (next ' . $charObj->ivShow('skillNextXPHash', $skill)
                    . ') Cash spent: ' . $charObj->ivShow('skillTotalCashHash', $skill)
                    . ' (next ' . $charObj->ivShow('skillNextCashHash', $skill) . ')',
                );

                # Remove this skill from the hash, so we can check for inconsistencies
                delete $charHash{$skill};
            }
        }

        if (%charHash) {

            foreach my $skill (keys %charHash) {

                $count++;
                $session->writeText(
                    sprintf(
                        '   %-48.48s Level: %-5.5s (Missing from guild)',
                        $skill,
                        $charObj->ivShow('skillLevelHash', $skill),
                    )
                );

                $session->writeText(
                    sprintf(
                        '      Times advanced: %-5.5s, XP spent: %-5.5s (next %5.5s), cash spent:'
                        . ' %-8.8s (next %-8.8s)',
                        $charObj->ivShow('skillAdvanceCountHash', $skill),
                        $charObj->ivShow('skillTotalXPHash', $skill),
                        $charObj->ivShow('skillNextXPHash', $skill),
                        $charObj->ivShow('skillTotalCashHash', $skill),
                        $charObj->ivShow('skillNextCashHash', $skill),
                    )
                );
            }
        }

        # Display footer
        if ($count == 1) {

            return $self->complete($session, $standardCmd, 'List displayed (1 skill found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'List displayed (' . $count . ' skills found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Advance;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('advance', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['adv', 'advance'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Advance the current character\'s guild skills';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $otherSwitch, $skill, $number, $singleArg, $taskObj,
            @skillList, @copyList,
        );

        # Check that there is a current guild and character
        if (! $session->currentGuild) {

            return $self->error(
                $session, $inputString,
                'Can\'t advance skills - there is no current guild profile',
            );

        } elsif (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t advance skills - there is no current character profile',
            );
        }

        # Extract '-s <skill> <number>' and/or '-s <skill> -a' on a continuous loop, until there are
        #   no -s switches left
        # Extract the switch which stands in place of <number>
        do {

            ($switch, $skill, $number, @args) = $self->extract('-s', 2, @args);
            if (defined $switch) {

                # $number may have been -a, but $self->extract will see that as another switch
                #   pattern. Try to extract -a
                ($otherSwitch, @args) = $self->extract('-a', 0, @args);
                if (defined $otherSwitch) {
                    push (@skillList, $skill, '-a');
                } else {
                    push (@skillList, $number);
                }
            }

        } until (! $switch);

        if (@skillList) {

            # There should be 0 arguments left
            if (@args) {

                return $self->error(
                    $session, $inputString,
                    'The switch -s can\'t be combined with standalone <number> or -a arguments',
                );
            }

        } else {

            # There should be 0 or 1 arguments left
            if (@args > 1) {

                return $self->improper($session, $inputString);

            } else {

                $singleArg = shift @args;
            }
        }

        # ;adv
        # ;adv <number>
        # ;adv -a
        if (! @skillList) {

            if (! defined $singleArg) {

                # Advance pre-defined skills once
                @copyList = (undef, 1);

            } elsif ($singleArg eq '-a') {

                # Advance pre-defined skills as many times as possible
                @copyList = (undef, -1);

            } elsif ($axmud::CLIENT->intCheck($singleArg)) {

                if ($singleArg < 1) {

                    return $self->error(
                        $session, $inputString,
                        'Can\'t advance skills - <number> must be an integer greater than 0',
                    );

                } else {

                    # Advance pre-defined skills a specific number of times
                    @copyList = (undef, $singleArg);
                }

            } else {

                return $self->improper($session, $inputString);
            }

        # ;adv -s <skill>
        # ;adv -s <skill> <number>
        # ;adv -s <skill> -a
        } else {

            # Process @skillList, 2 arguments at a time
            do {

                $skill = shift @skillList;
                $number = shift @skillList; # 'undef', <number> or -a

                if (! $number) {

                    # ;adv -s <skill>
                    push (@copyList, $skill, 1);

                } elsif ($number eq '-a') {

                    push (@copyList, $skill, -1);

                } elsif ($axmud::CLIENT->intCheck($number)) {

                    # ;adv -s <skill> <number>
                    push (@copyList, $skill, $number);

                } else {

                    return $self->improper($session, $inputString);
                }

            } until (! @skillList);
        }

        # If the Advance task isn't already running, start it
        if (! $session->advanceTask) {

            $taskObj = Games::Axmud::Task::Advance->new($session, 'current');
            if (! $taskObj) {

                return $self->error(
                    $session, $inputString,
                    'General error creating a new Advance task',
                );
            }
        }

        # Add the skills to the task's list of skills to process
        $session->advanceTask->ivPush('skillList', @copyList);

        if (@copyList == 2) {

            return $self->complete(
                $session, $standardCmd,
                '1 instruction sent to the Advance task',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                ((scalar @copyList) / 2) . ' instructions sent to the Advance task',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SkipAdvance;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('skipadvance', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ska', 'skipadv', 'skipadvance'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Skips skills in the character\'s advance list';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $num,
            $check,
        ) = @_;

        # Local variables
        my ($charObj, $skill, $firstSkill, $count);

        # Check for improper arguments
        if (! defined $switch || ($switch ne '-o' && $switch ne '-c') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that <number> is valid (if specified)
        if (defined $num && ! $axmud::CLIENT->intCheck($num, 0)) {

            return $self->error(
                $session, $inputString,
                'Invalid number \'' . $num . '\' - if specified, must be an integer above zero',
            );

        } elsif (! $num) {

            # If not specified, use the <number> 1
            $num = 1;
        }

        # Check that there is a current character
        if (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t list skills - there is no current character profile',
            );

        } else {

            # Import it
            $charObj = $session->currentChar;
        }

        # ;ska -o
        # ;ska -o <number>
        if ($switch eq '-o') {

            if (! $charObj->currentAdvanceOrderList) {

                return $self->error(
                    $session, $inputString,
                    'Can\'t skip skills in the character\'s \'order\' list because it is empty',
                );

            } else {

                OUTER: for ($count = 1; $count <= $num; $count++) {

                    $skill = $charObj->ivShift('currentAdvanceOrderList');
                    if ($count == 1) {

                        $firstSkill = $skill;
                    }

                    if (! $charObj->currentAdvanceOrderList) {

                        # List is now empty
                        last OUTER;
                    }
                }
            }

        # ;ska -c
        # ;ska -c <number>
        } else {

            if (! $charObj->advanceCycleList) {

                # The cycle list is completely empty (the current list should also be empty, so
                #   don't bother checking it)
                return $self->error(
                    $session, $inputString,
                    'Can\'t skip skills in the character\'s \'cycle\' list because it is empty',
                );

            } elsif (! $charObj->currentAdvanceCycleList) {

                # Refresh the current list
                $charObj->ivPush('currentAdvanceCycleList', $charObj->advanceCycleList);
            }

            for ($count = 1; $count <= $num; $count++) {

                $skill = $charObj->ivShift('currentAdvanceOrderList');
                if ($count == 1) {

                    $firstSkill = $skill;
                }

                if (! $charObj->currentAdvanceOrderList) {

                    # List is now empty, so refresh it
                    $charObj->ivPush('currentAdvanceCycleList', $charObj->advanceCycleList);
                }
            }
        }

        if ($count == 2) {

            return $self->complete(
                $session, $standardCmd,
                'Skipped the pre-defined skill \'' . $firstSkill . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Skipped ' . ($count - 1) . ' pre-defined skills',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListAdvance;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listadvance', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lav', 'listadv', 'listadvance'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists the skill order/cycle for current character';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my ($guildObj, $charObj, $count, $totalCount);

        # Check for improper arguments
        if ((defined $switch && $switch ne '-g' && $switch ne '-c') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that there is a current guild and character
        if ($switch && $switch eq '-g' && ! $session->currentGuild) {

            return $self->error(
                $session, $inputString,
                'Can\'t list skills - there is no current guild profile',
            );

        } elsif (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t list skills - there is no current character profile',
            );

        } else {

            # Import the profiles
            $guildObj = $session->currentGuild;
            $charObj = $session->currentChar;
        }

        # Check there are some skills to list
        if (
            (! $switch || $switch eq '-c')
            && ! $charObj->advanceOrderList
            && ! $charObj->advanceCycleList
        ) {
            return $self->error(
                $session, $inputString,
                'No skills defined for the current character',
            );

        } elsif (
            $switch
            && $switch eq '-g'
            && ! $guildObj->advanceOrderList
            && ! $guildObj->advanceCycleList
        ) {
            return $self->error(
                $session, $inputString,
                'No skills defined for the current guild (\'' . $guildObj->name . '\')',
            );
        }

        # ;lav
        # ;lav -c
        if (! $switch || $switch eq '-c') {

            # Display header
            $session->writeText('Skill order/cycle for character \'' . $charObj->name . '\'');

            if ($charObj->advanceMethod eq 'order' || $charObj->advanceMethod eq 'cycle') {

                $session->writeText('   Advance method: ' . $charObj->advanceMethod);

            } else {

                $session->writeText(
                    '   Advance method: combo (stage: ' . $charObj->advanceMethodStatus . ')',
                );
            }

            # Display list
            if (
                $charObj->advanceMethod eq 'order'
                || (
                    $charObj->advanceMethod eq 'combo'
                    && $charObj->advanceMethodStatus eq 'order'
                )
            ) {

                $session->writeText('Skill order:');

                $count = 0;
                foreach my $skill ($charObj->currentAdvanceOrderList) {

                    $count++;
                    $totalCount++;
                    $session->writeText(sprintf('   %-4.4s ', $count) . $skill);
                }
            }

            if (
                $charObj->advanceMethod eq 'cycle'
                || (
                    $charObj->advanceMethod eq 'combo'
                    && $charObj->advanceMethodStatus eq 'cycle'
                )
            ) {
                $session->writeText('Skill cycle:');

                $count = 0;
                foreach my $skill ($charObj->currentAdvanceCycleList) {

                    $count++;
                    $totalCount++;
                    $session->writeText(sprintf('   %-4.4s ', $count) . $skill);
                }
            }

        # ;lav -g
        } else {

            # Display header
            $session->writeText('Skill order/cycle for guild \'' . $guildObj->name . '\'');
            $session->writeText('   Advance method: ' . $guildObj->advanceMethod);

            # Display list
            if ($guildObj->advanceMethod eq 'order' || $guildObj->advanceMethod eq 'combo') {

                $session->writeText('Skill order:');

                $count = 0;
                foreach my $skill ($guildObj->advanceOrderList) {

                    $count++;
                    $totalCount++;
                    $session->writeText(sprintf('   %-4.4s ', $count) . $skill);
                }
            }

            if ($guildObj->advanceMethod eq 'cycle' || $guildObj->advanceMethod eq 'cycle') {

                $session->writeText('Skill cycle:');

                $count = 0;
                foreach my $skill ($guildObj->advanceCycleList) {

                    $count++;
                    $totalCount++;
                    $session->writeText(sprintf('   %-4.4s ', $count) . $skill);
                }
            }
        }

        # Display footer
        if ($totalCount == 1) {

            return $self->complete($session, $standardCmd, 'List displayed (1 skill found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'List displayed (' . $totalCount . ' skills found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListAdvanceHistory;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listadvancehistory', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lah', 'listah', 'listadvancehistory'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists the character\'s advance history';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $charObj, $count,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that there is a current character
        if (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t list advance history - there is no current character profile',
            );

        } else {

            # Import the profile
            $charObj = $session->currentChar;
        }

        # Import the list of GA::Obj::SkillHistory objects
        @list = $charObj->skillHistoryList;
        if (! @list) {

            return $self->error(
                $session, $inputString,
                'The current character\'s advance history list is empty',
            );
        }

        # Display header
        if ($session->currentGuild) {

            $session->writeText(
                'Skill advance history for \'' . $charObj->name . '\' (guild \''
                . $session->currentGuild . '\')',
            );

        } else {

            $session->writeText(
                'Skill advance history for \'' . $charObj->name . '\' (no guild set)',
            );
        }

        # Display list
        $count = 0;
        foreach my $obj (@list) {

            $count++;

            $session->writeText(
                sprintf(' %-5.5s %-48.48s (', $count, $obj->skill) . $obj->advanceMethod . ')',
            );

            $session->writeText(
                '          Skill level: ' . $obj->skillLevel . ' (Times advanced: '
                . $obj->skillAdvanceCount . ')',
            );

            $session->writeText(
                '          XP spent: ' . $obj->skillThisXP . ' (next ' . $obj->skillNextXP
                . ') Cash spent: ' . $obj->skillThisCash . ' (next ' . $obj->skillNextCash . ')',
            );
        }

        # Display footer
        if ($count == 1) {

            return $self->complete($session, $standardCmd, 'List displayed (1 advance found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'List displayed (' . $count . ' advances found)',
            );
        }
    }
}

# Attack task

{ package Games::Axmud::Cmd::Kill;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('kill', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['k', 'kill'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Attacks target(s) in the current room';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Attack one instance (represented by the FALSE argument) of each of the targets specified
        #   in @args
        return $self->killLimitedTargets($session, $inputString, $standardCmd, FALSE, @args);
    }
}

{ package Games::Axmud::Cmd::KKill;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('kkill', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['kk', 'kkill'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Attacks all matching target(s) in the current room';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Attack multiple instances (represented by the TRUE argument) of each of the targets
        #   specified in @args
        return $self->killTargets($session, $inputString, $standardCmd, TRUE, @args);
    }
}

{ package Games::Axmud::Cmd::KillAll;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('killall', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ka', 'killall'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Attacks all target(s) in the current room';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Attack all targets (TRUE means attack players, too)
        return $self->killUnlimitedTargets($session, $inputString, $standardCmd, TRUE);
    }
}

{ package Games::Axmud::Cmd::KillMall;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('killmall', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['km', 'killmall'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Attacks all NPC target(s) in the current room';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Attack all non-player targets (FALSE means don't attack players, too)
        return $self->killUnlimitedTargets($session, $inputString, $standardCmd, FALSE);
    }
}

{ package Games::Axmud::Cmd::Interact;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('interact', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['i', 'int', 'interact'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Interacts with target(s) in the current room';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Attack one instance (represented by the FALSE argument) of each of the targets specified
        #   in @args
        return $self->interactLimitedTargets($session, $inputString, $standardCmd, FALSE, @args);
    }
}

{ package Games::Axmud::Cmd::IInteract;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('iinteract', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ii', 'iint', 'iinteract'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Interacts with all matching target(s) in the room';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Attack multiple instances (represented by the TRUE argument) of each of the targets
        #   specified in @args
        return $self->interactLimitedTargets($session, $inputString, $standardCmd, TRUE, @args);
    }
}

{ package Games::Axmud::Cmd::InteractAll;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('interactall', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ia', 'intall', 'interactall'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Interacts with all target(s) in the current room';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Attack all targets (TRUE means attack players, too)
        return $self->killUnlimitedTargets($session, $inputString, $standardCmd, TRUE);
    }
}

{ package Games::Axmud::Cmd::InteractMall;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('interactmall', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['im', 'intmall', 'interactmall'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Interacts with all NPC target(s) in the room';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Attack all non-player targets (FALSE means don't attack players, too)
        return $self->killUnlimitedTargets($session, $inputString, $standardCmd, FALSE);
    }
}

# Channels and Divert tasks

{ package Games::Axmud::Cmd::AddChannelPattern;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addchannelpattern', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['achp', 'addchan', 'addchannel', 'addchannelpattern'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a Channels/Divert task pattern';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $flagCount, $switch, $gagFlag, $noGagFlag, $exceptFlag, $channel, $pattern, $worldObj,
            $msg,
        );

        # Extract switches
        $flagCount = 0;

        ($switch, @args) = $self->extract('-g', 0, @args);
        if (defined $switch) {

            $gagFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-m', 0, @args);
        if (defined $switch) {

            $noGagFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-e', 0, @args);
        if (defined $switch) {

            $exceptFlag = TRUE;
            $flagCount++;
        }

        # There should be 1 or 2 arguments left
        $channel = shift @args;
        $pattern = shift @args;

        if (@args || ($exceptFlag && defined $pattern)) {

            return $self->improper($session, $inputString);
        }

        # Check validity of switches
        if ($flagCount > 1) {

            return $self->error(
                $session, $inputString,
                'The switches -g, -m an -e can\'t be combined',
            );
        }

        # Import the current world profile
        $worldObj = $session->currentWorld;


        # Check the specified pattern is valid
        if ($exceptFlag) {

            $pattern = $channel;
        }

        if ($axmud::CLIENT->regexCheck($pattern)) {

            return $self->error(
                $session, $inputString,
                'The pattern \'' . $pattern . '\' isn\'t a valid regular expression',
            );
        }

        # Add the pattern to the profile
        if (! $flagCount || $gagFlag) {

            $worldObj->ivPush('channelList', $pattern, $channel, TRUE);
            $msg = 'Added pattern \'' . $pattern . '\' to channel \'' . $channel
                        . '\' (gagged in the \'main\' window)',

        } elsif ($noGagFlag) {

            $worldObj->ivPush('channelList', $pattern, $channel, FALSE);
            $msg =  'Added pattern \'' . $pattern . '\' to channel \'' . $channel
                        . '\' (not gagged in the \'main\' window)',

        } else {

            $worldObj->ivPush('noChannelList', $pattern);

            $msg = 'Added channel exception pattern \'' . $pattern . '\'';
        }

        # If the Channels or Divert tasks are currently running, tell them to reset their triggers
        if ($session->channelsTask) {

            if (! $session->channelsTask->resetTriggers()) {

                return $self->complete(
                    $session, $standardCmd,
                   'General error resetting the current Channels task\'s triggers. ' . $msg,
                );
            }

        } elsif ($session->divertTask) {

            if (! $session->divertTask->resetTriggers()) {

                return $self->error(
                    $session, $inputString,
                    'General error resetting the current Divert task\'s triggers. ' . $msg,
                );
            }
        }

        return $self->complete($session, $standardCmd, $msg);
    }
}

{ package Games::Axmud::Cmd::DeleteChannelPattern;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletechannelpattern', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dchp', 'delchan', 'delchannel', 'deletechannelpattern'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes Channels/Divert task patterns';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $number,
            $check,
        ) = @_;

        # Local variables
        my ($worldObj, $total, $index, $pattern, $msg);

        # Check for improper arguments
        if (! defined $switch || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the current world profile
        $worldObj = $session->currentWorld;

        # ;dchp <number>
        if (! defined $number) {

            $number = $switch;

            # GA::Profile::World->channelList is in groups of 3, in the form
            #   (pattern, channel, flag)
            $total = int(scalar ($worldObj->channelList) / 3);

            if (! $axmud::CLIENT->intCheck($number, 1, $total)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid position \'' . $number . '\' (you must specify the pattern\'s'
                    . ' position, the number in the list generated by \';listchannelpattern\')',
                );

            } else {

                # Remove the group
                $index = ($number - 1) * 3;
                $pattern = $worldObj->ivIndex('channelList', $index);
                $worldObj->ivSplice('channelList', $index, 3);

                $msg = 'Removed channel pattern #' . $number . ' \'' . $pattern . '\'';
            }

        # ;dhcp -e <number>
        } else {

            if ($switch ne '-e') {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised switch \'' . $switch . '\'',
                );
            }

            # GA::Profile::World->noChannelList is in groups of 1
            $total = scalar ($worldObj->noChannelList);

            if (! $axmud::CLIENT->intCheck($number, 1, $total)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid position \'' . $number . '\' (you must specify the pattern\'s'
                    . ' position the number in the list generated by \';listchannelpattern -e\')',
                );

            } else {

                # Remove the pattern
                $index = $number - 1;
                $pattern = $worldObj->ivIndex('noChannelList', $index);
                $worldObj->ivSplice('noChannelList', $index, 1);

                $msg = 'Removed channel pattern exception #' . $number . ' \'' . $pattern . '\'';
            }
        }

        # If the Channels or Divert tasks are currently running, tell them to reset their triggers
        if ($session->channelsTask) {

            if (! $session->channelsTask->resetTriggers()) {

                return $self->complete(
                    $session, $standardCmd,
                   'General error resetting the current Channels task\'s triggers. ' . $msg,
                );
            }

        } elsif ($session->divertTask) {

            if (! $session->divertTask->resetTriggers()) {

                return $self->error(
                    $session, $inputString,
                    'General error resetting the current Divert task\'s triggers. ' . $msg,
                );
            }
        }

        return $self->complete($session, $standardCmd, $msg);
    }
}

{ package Games::Axmud::Cmd::ListChannelPattern;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listchannelpattern', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lchp', 'listchan', 'listchannel', 'listchannelpattern'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists Channels/Divert task patterns';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my (
            $worldObj, $count,
            @list, @modList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the current world profile
        $worldObj = $session->currentWorld;

        # ;achp -e
        if (defined $arg && $arg eq '-e') {

            @list = $worldObj->noChannelList;
            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'The list of channel exception patterns is empty',
                );
            }

            # Display header
            $session->writeText('List of channel exception patterns');
            $session->writeText('   #    Pattern');

            # Display list
            $count = 0;
            foreach my $pattern (@list) {

                $count++;
                $session->writeText(sprintf('   %-4.4s', $count) . ' ' . $pattern);
            }

            # Display footer
            if (@list == 1) {
                $session->writeText('End of list (1 pattern found)');
            } else {
                $session->writeText('End of list (' . scalar @list . ' patterns found)');
            }

        # ;achp <channel>
        } elsif (defined $arg) {

            @list = $worldObj->channelList;

            # @list is in groups of 3, in the form (pattern, channel, flag)
            # Filter out any groups whose channel isn't $channel
            $count = 0;
            do {

                my ($pattern, $channel, $flag);

                $count++;

                $pattern = shift @list;
                $channel = shift @list;
                $flag = shift @list;

                if (defined $channel && $channel eq $arg) {

                    push (@modList, $count, $pattern, $channel, $flag);
                }

            } until (! @list);

            if (! @modList) {

                return $self->complete(
                    $session, $standardCmd,
                    'There are no patterns assigned to the channel \'' . $arg . '\'',
                );
            }

            # Display header
            $session->writeText('List of channel patterns (* - gagged in \'main\' window)');
            $session->writeText('   #    Channel          Pattern');

            # Display list
            do {

                my ($num, $pattern, $channel, $flag, $column);

                $num = shift @modList;
                $pattern = shift @modList;
                $channel = shift @modList;
                $flag = shift @modList;

                if ($flag) {
                    $column = ' * ';
                } else {
                    $column = '   ';
                }

                $session->writeText(
                    $column . sprintf('%-4.4s %-16.16s', $num, $channel) . ' ' . $pattern,
                );

            } until (! @modList);

            # Display footer
            if (@list == 1) {
                $session->writeText('End of list (1 pattern found)');
            } else {
                $session->writeText('End of list (' . scalar @list . ' patterns found)');
            }

        # ;achp
        } else {

            @list = $worldObj->channelList;
            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'The current world profile contains no channel patterns',
                );
            }

            # Display header
            $session->writeText('List of channel patterns (* - gagged in \'main\' window)');
            $session->writeText('   #    Channel          Pattern');

            # Display list
            $count = 0;
            do {

                my ($pattern, $channel, $flag, $column);

                $count++;

                $pattern = shift @list;
                $channel = shift @list;
                $flag = shift @list;

                if ($flag) {
                    $column = ' * ';
                } else {
                    $column = '   ';
                }

                $session->writeText(
                    $column . sprintf('%-4.4s %-16.16s', $count, $channel) . ' ' . $pattern,
                );

            } until (! @list);

            # Display footer
            if (@list == 1) {
                $session->writeText('End of list (1 pattern found)');
            } else {
                $session->writeText('End of list (' . scalar @list . ' patterns found)');
            }
        }
    }
}

{ package Games::Axmud::Cmd::EmptyChannelsWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('emptychannelswindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'ecw',
            'emptychan',
            'emptychannelwindow',
            'emptychannelswindow',
        ];

        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Empties the Channels task window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the Channels task is running and has a window open
        if (! $session->channelsTask || ! $session->channelsTask->taskWinFlag) {

            return $self->error(
                $session, $inputString,
                'The Channels task is not currently running',
            );

        } else {

            # Reset the task's window
            $session->channelsTask->resetWin();

            return $self->complete(
                $session, $standardCmd,
                'The Channels task window has been reset',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EmptyDivertWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('emptydivertwindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['edw', 'emptydivert', 'emptydivertwindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Empties the Divert task window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the Divert task is running and has a window open
        if (! $session->divertTask) {

            return $self->error(
                $session, $inputString,
                'The Divert task is not currently running',
            );

        } elsif (! $session->divertTask->taskWinFlag) {

            return $self->error(
                $session, $inputString,
                'The Divert task is running without a task window',
            );

        } else {

            # Reset the task's window
            $session->divertTask->resetWin();

            return $self->complete(
                $session, $standardCmd,
                'The Divert task window has been reset',
            );
        }
    }
}

# Chat task

{ package Games::Axmud::Cmd::GetIP;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('getip', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['gip', 'getip'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Displays your IP address';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $ip;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        $ip = $axmud::CLIENT->ipv4Get();
        if (! defined $ip) {

            return $self->error($session, $inputString, 'Unable to determine your IP address');

        } else {

            return $self->complete($session, $standardCmd, 'Your IP address is: ' . $ip);
        }
    }
}

{ package Games::Axmud::Cmd::ChatListen;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatlisten', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['listen', 'chatlisten'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Listens out for incoming calls';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $port,
            $check,
        ) = @_;

        # Local variables
        my $taskObj;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If $port was specified, make sure it's an acceptable number
        if (defined $port && ! $axmud::CLIENT->intCheck($port, 0, 65535)) {

            return $self->error(
                $session, $inputString,
                'Invalid telnet port \'' . $port . '\' (must be in range 0-65535)',
            );
        }

        # There can be any number of Chat tasks running simultaneously, but only one of them listens
        #   for incoming connections
        # That task, the 'lead' Chat task, is stored as $session->chatTask
        if (! $session->chatTask) {

            # Since there are no Chat tasks running, create one
            $taskObj = Games::Axmud::Task::Chat->new($session, 'current');
            if (! $taskObj) {

                return $self->error(
                    $session, $inputString,
                    'General error creating a new Chat task',
                );

            } else {

                # Tell the task to start accepting calls on the specified port, when its ->init
                #   function is called
                $taskObj->ivPoke('acceptCallsOnInitFlag', TRUE);
                $taskObj->ivPoke('acceptCallsOnInitPort', $port);   # If 'undef', use default port

                if ($port) {

                    return $self->complete(
                        $session, $standardCmd,
                        'Chat task created, listening out for incoming calls on port ' . $port,
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'Chat task created, listening out for incoming calls on the default port',
                    );
                }
            }

        } elsif ($session->chatTask->acceptSocket) {

            return $self->error(
                $session, $inputString,
                'The lead Chat task is already listening for incoming calls (try \';chatignore\''
                . ' first)',
            );

        } else {

            # Tell the lead Chat task to start accepting incoming calls
            if (! $session->chatTask->acceptCalls($port)) {

                if ($port) {

                    return $self->error(
                        $session, $inputString,
                        'The lead Chat task couldn\'t listen out for incoming calls on port '
                        . $port,
                    );

                } else {

                    return $self->error(
                        $session, $inputString,
                        'The lead Chat task couldn\'t listen out for incoming calls on port '
                        . $axmud::CLIENT->constChatPort,
                    );
                }

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'The lead Chat task is now listening out for incoming calls on port '
                    . $axmud::CLIENT->constChatPort,
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ChatIgnore;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatignore', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ignore', 'chatignore'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Stops listening out for incoming calls';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there is a lead Chat task (which does the listening)
        if (! $session->chatTask) {

            return $self->error($session, $inputString, 'There are no Chat tasks running');
        }

        # Tell the lead Chat task to stop listening out for incoming calls
        if (! $session->chatTask->refuseCalls()) {

            return $self->error(
                $session, $inputString,
                'The lead Chat task couldn\'t stop listening out for incoming calls',
            );

        } elsif (! $session->chatTask) {

            return $self->complete(
                $session, $standardCmd,
                'Lead Chat task has been halted (to stop it listening out for incoming calls)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'The lead Chat task is now ignoring incoming calls',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddContact;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addcontact', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['acn', 'addcontact'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds someone to your contacts list';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name, $ip, $port, $protocol,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || ! defined $ip || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Deal with ;addcontact <name> <ip> <protocol>
        if (defined $port) {

            if ($port eq '-m' || $port eq '-z') {

                $protocol = $port;
                $port = undef;
            }
        }

        # Check <port> is valid, if specified
        if (defined $port && ! $axmud::CLIENT->intCheck($port, 0, 65535)) {

            return $self->error(
                $session, $inputString,
                'Invalid telnet port \'' . $port . '\' (must be in range 0-65535)',
            );

        # Check <protocol> is valid
        } elsif (defined $protocol && $protocol ne '-m' && $protocol ne '-z') {

            return $self->error(
                $session, $inputString,
                'Invalid protocol - use \'-m\' for MudMaster or \'-z\' for zChat',
            );
        }

        # If <port> not specified, use the default chat port
        if (! defined $port) {

            $port = $axmud::CLIENT->constChatPort;
        }

        # Check that <name> isn't already taken
        if ($axmud::CLIENT->ivExists('chatContactHash', $name)) {

            return $self->error(
                $session, $inputString,
                'You already have a contact called \'' . $name . '\' in your contacts list',
            );
        }

        # Add the contact to the contacts list
        if (! $protocol || $protocol eq '-z') {

            # zChat
            $obj = Games::Axmud::Obj::ChatContact->new($session, $name, 1, $ip, $port);

        } else {

            # MudMaster
            $obj = Games::Axmud::Obj::ChatContact->new($session, $name, 0, $ip, $port);
        }

        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Couldn\'t add \'' . $name . '\' to your contacts list',
            );

        } else {

            $axmud::CLIENT->add_chatContact($name, $obj);

            return $self->complete(
                $session, $standardCmd,
                'Added \'' . $name . '\' to your contacts list',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditContact;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editcontact', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ecn', 'editcontact'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a chat contact';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the contact exists
        if (! $axmud::CLIENT->ivExists('chatContactHash', $name)) {

            return $self->error(
                $session, $inputString,
                'Could not edit the contact \'' . $name . '\' - object does not exist',
            );

        } else {

            $obj = $axmud::CLIENT->ivShow('chatContactHash', $name);
        }

        # Open an \'edit\' window for the dictionary
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::ChatContact',
                $session->mainWin,
                $session,
                'Edit contact \'' . $obj->name . '\'',
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the contact \'' . $name . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the contact \'' . $name . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteContact;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletecontact', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dcn', 'delcontact', 'deletecontact'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes someone from your contacts list';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that the contact exists
        if (! $axmud::CLIENT->ivExists('chatContactHash', $name)) {

            return $self->error(
                $session, $inputString,
                'There is no-one in your contacts list called \'' . $name . '\'',
            );

        } else {

            $axmud::CLIENT->del_chatContact($name);

            return $self->complete(
                $session, $standardCmd,
                'Removed \'' . $name . '\' from your contacts list',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListContact;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listcontact', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lcn', 'listcontact'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Displays your contacts list';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there is at least one person in the user's contacts list
        if (! $axmud::CLIENT->chatContactHash) {

            return $self->error($session, $inputString, 'Your contacts list is empty');
        }

        # Get a list of contacts sorted by name
        @list = sort {lc($a->name) cmp lc($b->name)} ($axmud::CLIENT->ivValues('chatContactHash'));
        if (! @list) {

            return $self->complete($session, $standardCmd, 'The Chat task contact list is empty');
        }

        # Display header
        $session->writeText('Chat task contacts list (m - use MudMaster protocol, z - use zChat)');
        $session->writeText('   Name             IP Address       Port');

        # Display list
        foreach my $obj (@list) {

            my $column;

            if ($obj->protocol == 0) {
                $column = ' m ';
            } else {
                $column = ' z ';
            }

            $session->writeText(
                $column . sprintf('%-16.16s %-16.16s %-5.5s', $obj->name, $obj->ip, $obj->port),
            );
        }

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 contact found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @list . ' contacts found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ChatCall;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatcall', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['call', 'chatcall'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Calls one of your contacts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my ($contactObj, $result, $taskObj);

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that the contact exists
        if (! $axmud::CLIENT->ivExists('chatContactHash', $name)) {

            return $self->error(
                $session, $inputString,
                'There is no-one in your contacts list called \'' . $name . '\' (try \';mcall\''
                . ' or \';zcall\')',
            );

        } else {

            $contactObj = $axmud::CLIENT->ivShow('chatContactHash', $name);
        }

        # If the lead Chat task exists but doesn't have a chat session open, use it
        if ($session->chatTask && ! $session->chatTask->sessionFlag) {

            # Tell the task that it's calling someone in our contacts list
            $session->chatTask->ivPoke('chatContactObj', $contactObj);

            if ($contactObj->protocol == 0) {

                # MudMaster protocol
                $result = $session->chatTask->mcall($contactObj->ip, $contactObj->port);

            } else {

                # zChat protocol
                $result = $session->chatTask->zcall($contactObj->ip, $contactObj->port);
            }

            if (! $result) {

                $session->chatTask->ivUndef('chatContactObj');

                return $self->error(
                    $session, $inputString,
                    'Error initiating a chat session with \'' . $name . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Chat session with \'' . $name . '\' initiated, calling ' . $contactObj->ip
                    . ' port ' . $contactObj->port,
                );
            }

        } else {

            # There are either no Chat tasks running, or the lead Chat task (and therefore all other
            #   Chat tasks) are engaged in chat sessions - so it's necessary to create a new Chat
            #   task
            $taskObj = Games::Axmud::Task::Chat->new($session, 'current');
            if (! $taskObj) {

                return $self->error(
                    $session, $inputString,
                    'General error creating a new Chat task',
                );

            } else {

                # Tell the task to make a call to the specified ip/port, when its ->init function is
                #   called
                $taskObj->ivPoke('makeCallOnInitFlag', TRUE);

                if ($contactObj->protocol == 0) {
                    $taskObj->ivPoke('makeCallOnInitType', 'MM');
                } else {
                    $taskObj->ivPoke('makeCallOnInitType', 'ZCHAT');
                }

                $taskObj->ivPoke('makeCallOnInitIP', $contactObj->ip);
                $taskObj->ivPoke('makeCallOnInitPort', $contactObj->port);
                $taskObj->ivPoke('chatContactObj', $contactObj);

                return $self->complete(
                    $session, $standardCmd,
                    'Chat task created, calling \'' . $name . '\' on ' . $contactObj->ip . ' port '
                    . $contactObj->port,
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ChatMCall;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatmcall', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mcall', 'chatmcall'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Calls someone using the MudMaster protocol';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $ip, $port,
            $check,
        ) = @_;

        # Local variables
        my $taskObj;

        # Check for improper arguments
        if (! defined $ip || defined $check) {

            return $self->improper($session, $inputString);
        }

        # If $port was specified, make sure it's an acceptable number
        if (defined $port && ! $axmud::CLIENT->intCheck($port, 0, 65535)) {

            return $self->error(
                $session, $inputString,
                'Invalid telnet port \'' . $port . '\' (must be in range 0-65535)',
            );
        }

        # If the lead Chat task exists but doesn't have a chat session open, use it
        if ($session->chatTask && ! $session->chatTask->sessionFlag) {

            if (! $session->chatTask->mcall($ip, $port)) {

                return $self->error($session, $inputString, 'Error initiating the chat session');

            } elsif (defined $port) {

                return $self->complete(
                    $session, $standardCmd,
                    'Chat session initiated, calling ' . $ip . ' port ' . $port,
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Chat session initiated, calling ' . $ip . ' port '
                    . $axmud::CLIENT->constChatPort,
                );
            }

        } else {

            # There are either no Chat tasks running, or the lead Chat task (and therefore all other
            #   Chat tasks) are engaged in chat sessions - so it's necessary to create a new Chat
            #   task
            $taskObj = Games::Axmud::Task::Chat->new($session, 'current');
            if (! $taskObj) {

                return $self->error(
                    $session, $inputString,
                    'General error creating a new Chat task',
                );

            } else {

                # Tell the task to make a call to the specified ip/port, when its ->init function is
                #   called
                if (! defined $port) {

                    # Use the default chat port
                    $port = $axmud::CLIENT->constChatPort;
                }

                $taskObj->ivPoke('makeCallOnInitFlag', TRUE);
                $taskObj->ivPoke('makeCallOnInitType', 'MM');
                $taskObj->ivPoke('makeCallOnInitIP', $ip);
                $taskObj->ivPoke('makeCallOnInitPort', $port);

                return $self->complete(
                    $session, $standardCmd,
                    'Chat task created, calling ' . $ip . ' port ' . $port,
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ChatZCall;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatzcall', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['zcall', 'chatzcall'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Calls someone using the zChat protocol';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $ip, $port,
            $check,
        ) = @_;

        # Local variables
        my $taskObj;

        # Check for improper arguments
        if (! defined $ip || defined $check) {

            return $self->improper($session, $inputString);
        }

        # If $port was specified, make sure it's an acceptable number
        if (defined $port && ! $axmud::CLIENT->intCheck($port, 0, 65535)) {

            return $self->error(
                $session, $inputString,
                'Invalid telnet port \'' . $port . '\' (must be in range 0-65535)',
            );
        }

        # If the lead Chat task exists but doesn't have a chat session open, use it
        if ($session->chatTask && ! $session->chatTask->sessionFlag) {

            if (! $session->chatTask->zcall($ip, $port)) {

                return $self->error($session, $inputString, 'Error initiating the chat session');

            } elsif (defined $port) {

                return $self->complete(
                    $session, $standardCmd,
                    'Chat session initiated, calling ' . $ip . ' port ' . $port,
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Chat session initiated, calling ' . $ip . ' port '
                    . $axmud::CLIENT->constChatPort,
                );
            }

        } else {

            # There are either no Chat tasks running, or the lead Chat task (and therefore all other
            #   Chat tasks) are engaged in chat sessions - so it's necessary to create a new Chat
            #   task
            $taskObj = Games::Axmud::Task::Chat->new($session, 'current');
            if (! $taskObj) {

                return $self->error(
                    $session, $inputString,
                    'General error creating a new Chat task',
                );

            } else {

                # Tell the task to make a call to the specified ip/port, when its ->init function is
                #   called
                if (! defined $port) {

                    # Use the default chat port
                    $port = $axmud::CLIENT->constChatPort;
                }

                $taskObj->ivPoke('makeCallOnInitFlag', TRUE);
                $taskObj->ivPoke('makeCallOnInitType', 'ZCHAT');
                $taskObj->ivPoke('makeCallOnInitIP', $ip);
                $taskObj->ivPoke('makeCallOnInitPort', $port);

                return $self->complete(
                    $session, $standardCmd,
                    'Chat task created, calling ' . $ip . ' port ' . $port,
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::Chat;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chat', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['chat'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends a message to a chat contact';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            @args,
        ) = @_;

        # Local variables
        my @taskList;

        # Check for improper arguments
        if (! defined $name || ! @args) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );
        }

        # Ask the lead Chat task to identify which task(s) are connected to someone called <name>
        @taskList = $session->chatTask->findName($name);
        if (! @taskList) {

            return $self->error(
                $session, $inputString,
                'You aren\'t connected to a chat contact called \'' . $name . '\'',
            );
        }

        # Send the text to each chat contact called <name>
        foreach my $taskObj (@taskList) {

            $taskObj->chat(join(' ', @args));
        }

        if (@taskList == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Message sent to the chat contact \'' . $name . '\''
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Message sent to ' . scalar @taskList . ' chat contacts called \'' . $name . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::Emote;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('emote', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['emote'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends an emote to a chat contact';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            @args,
        ) = @_;

        # Local variables
        my @taskList;

        # Check for improper arguments
        if (! defined $name || ! @args) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );
        }

        # Ask the lead Chat task to identify which task(s) are connected to someone called <name>
        @taskList = $session->chatTask->findName($name);
        if (! @taskList) {

            return $self->error(
                $session, $inputString,
                'You aren\'t connected to a chat contact called \'' . $name . '\'',
            );
        }

        # Send the text to each chat contact called <name>
        foreach my $taskObj (@taskList) {

            $taskObj->emote(join(' ', @args));
        }

        if (@taskList == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Emote sent to the chat contact \'' . $name . '\''
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Emote sent to ' . scalar @taskList . ' chat contacts called \'' . $name . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ChatGroup;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatgroup', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['chatg', 'chatgroup'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends a message to a group of chat contacts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $group,
            @args,
        ) = @_;

        # Local variables
        my @taskList;

        # Check for improper arguments
        if (! defined $group || ! @args) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );
        }

        # Ask the lead Chat task to identify which task(s) are in the <group>
        @taskList = $session->chatTask->findGroup($group);
        if (! @taskList) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions in the group \'' . $group . '\'',
            );
        }

        # Send the text to each chat contact in the <group>
        foreach my $taskObj (@taskList) {

            $taskObj->chat(join(' ', @args));
        }

        if (@taskList == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Message sent to 1 chat contact in the group \'' . $group . '\''
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Message sent to ' . scalar @taskList . ' chat contacts in the group \'' . $group
                . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EmoteGroup;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('emotegroup', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['emoteg', 'emotegroup'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends an emote to a group of chat contacts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $group,
            @args,
        ) = @_;

        # Local variables
        my @taskList;

        # Check for improper arguments
        if (! defined $group || ! @args) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );
        }

        # Ask the lead Chat task to identify which task(s) are in the <group>
        @taskList = $session->chatTask->findGroup($group);
        if (! @taskList) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions in the group \'' . $group . '\'',
            );
        }

        # Send the text to each chat contact in the <group>
        foreach my $taskObj (@taskList) {

            $taskObj->emote(join(' ', @args));
        }

        if (@taskList == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Emote sent to 1 chat contact in the group \'' . $group . '\''
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Emote sent to ' . scalar @taskList . ' chat contacts in the group \'' . $group
                . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ChatAll;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatall', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['chata', 'chatall'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends a message to all chat contacts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my @taskList;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );
        }

        # Ask the lead Chat task to identify all Chat tasks
        @taskList = $session->chatTask->findAllTasks();

        # Send the text to every chat contact via the lead Chat task
        $session->chatTask->chatToAll(join(' ', @args));

        if (@taskList == 1) {

            return $self->complete($session, $standardCmd, 'Message sent to 1 chat contact');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Message sent to all ' . scalar @taskList . ' chat contacts',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EmoteAll;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('emoteall', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['emotea', 'emoteall'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends an emote to all chat contacts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my @taskList;

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );
        }

        # Ask the lead Chat task to identify all Chat tasks
        @taskList = $session->chatTask->findAllTasks();

        # Send the text to every chat contact via the lead Chat task
        $session->chatTask->emoteToAll(join(' ', @args));

        if (@taskList == 1) {

            return $self->complete($session, $standardCmd, 'Emote sent to 1 chat contact');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Emote sent to all ' . scalar @taskList . ' chat contacts',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ChatPing;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatping', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ping', 'chatping'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends a ping to chat contacts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $name, $nameFlag, $group, $groupFlag,
            @taskList,
        );

        # Extract switches
        ($switch, $name, @args) = $self->extract('-n', 1, @args);
        if (defined $switch) {

            $nameFlag = TRUE;
        }

        ($switch, $group, @args) = $self->extract('-g', 1, @args);
        if (defined $switch) {

            $groupFlag = TRUE;
        }

        # There should be no arguments left
        if (@args || (($nameFlag && ! defined $name) || $groupFlag && ! defined $group)) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif ($nameFlag && $groupFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -n and -g can\'t be combined',
            );
        }

        # ;ping -n <name>
        if (defined $name) {

            # Ask the lead Chat task to identify the task connected to all chat contacts called
            #   <name>
            @taskList = $session->chatTask->findName($name);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'You aren\'t connected to a chat contact called \'' . $name . '\'',
                );
            }

            # Send the ping to every chat contact called <name>
            foreach my $taskObj (@taskList) {

                $taskObj->pingPeer('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Ping sent to \'' . $name . '\'',
                );

            } else {
                return $self->complete(
                    $session, $standardCmd,
                    'Ping sent to ' . scalar @taskList . ' chat contacts called \'' . $name . '\'',
                );
            }

        # ;ping -g <group>
        } elsif (defined $group) {

            # Ask the lead Chat task to identify every task in the <group>
            @taskList = $session->chatTask->findGroup($group);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'There are no Chat task sessions in the group \'' . $group . '\'',
                );
            }

            # Send the ping to every chat contact in the group
            foreach my $taskObj (@taskList) {

                $taskObj->pingPeer('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Ping sent to 1 chat contact in the group \'' . $group . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Ping sent to ' . scalar @taskList . ' chat contacts in the group \''
                    . $group . '\'',
                );
            }

        # ;ping
        } else {

            # Ask the lead Chat task to identify all Chat tasks
            @taskList = $session->chatTask->findAllTasks();

            # Send the ping to every chat contact
            foreach my $taskObj (@taskList) {

                $taskObj->pingPeer('');
            }

            if (@taskList == 1) {

                return $self->complete($session, $standardCmd, 'Ping sent to 1 chat contact');

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Ping sent to all ' . scalar @taskList . ' chat contacts',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ChatDND;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatdnd', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dnd', 'chatdnd'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends a \'do not disturb\' request to chat contacts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $name, $nameFlag, $group, $groupFlag,
            @taskList,
        );

        # Extract switches
        ($switch, $name, @args) = $self->extract('-n', 1, @args);
        if (defined $switch) {

            $nameFlag = TRUE;
        }

        ($switch, $group, @args) = $self->extract('-g', 1, @args);
        if (defined $switch) {

            $groupFlag = TRUE;
        }

        # There should be no arguments left
        if (@args || (($nameFlag && ! defined $name) || $groupFlag && ! defined $group)) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif ($nameFlag && $groupFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -n and -g can\'t be combined',
            );
        }

        # ;dnd -n <name>
        if (defined $name) {

            # Ask the lead Chat task to identify the task connected to all chat contacts called
            #   <name>
            @taskList = $session->chatTask->findName($name);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'You aren\'t connected to a chat contact called \'' . $name . '\'',
                );
            }

            # Send the 'do not disturb' to every chat contact called <name>
            foreach my $taskObj (@taskList) {

                $taskObj->sendDoNotDisturb('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    '\'Do not disturb\' message sent to \'' . $name . '\'',
                );

            } else {
                return $self->complete(
                    $session, $standardCmd,
                    '\'Do not disturb\' message sent to ' . scalar @taskList . ' chat contacts'
                    . ' called \'' . $name . '\'',
                );
            }

        # ;dnd -g <group>
        } elsif (defined $group) {

            # Ask the lead Chat task to identify every task in the <group>
            @taskList = $session->chatTask->findGroup($group);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'There are no Chat task sessions in the group \'' . $group . '\'',
                );
            }

            # Send the 'do not disturb' to every chat contact in the group
            foreach my $taskObj (@taskList) {

                $taskObj->sendDoNotDisturb('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    '\'Do not disturb\' message sent to 1 chat contact in the group \'' . $group
                    . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    '\'Do not disturb\' message sent to ' . scalar @taskList . ' chat contacts in'
                    . ' the group \'' . $group . '\'',
                );
            }

        # ;dnd
        } else {

            # Ask the lead Chat task to identify all Chat tasks
            @taskList = $session->chatTask->findAllTasks();

            # Send the 'do not disturb' to every chat contact
            foreach my $taskObj (@taskList) {

                $taskObj->sendDoNotDisturb('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    '\'Do not disturb\' message sent to 1 chat contact',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    '\'Do not disturb\' message sent to all ' . scalar @taskList . ' chat contacts',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ChatSubmit;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatsubmit', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['submit', 'chatsubmit'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Allows chat contacts to send remote commands';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $name, $nameFlag, $group, $groupFlag,
            @taskList,
        );

        # Extract switches
        ($switch, $name, @args) = $self->extract('-n', 1, @args);
        if (defined $switch) {

            $nameFlag = TRUE;
        }

        ($switch, $group, @args) = $self->extract('-g', 1, @args);
        if (defined $switch) {

            $groupFlag = TRUE;
        }

        # There should be no arguments left
        if (@args || (($nameFlag && ! defined $name) || $groupFlag && ! defined $group)) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif ($nameFlag && $groupFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -n and -g can\'t be combined',
            );
        }

        # ;submit -n <name>
        if (defined $name) {

            # Ask the lead Chat task to identify the task connected to all chat contacts called
            #   <name>
            @taskList = $session->chatTask->findName($name);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'You aren\'t connected to a chat contact called \'' . $name . '\'',
                );
            }

            # Send permission to every chat contact called <name>
            foreach my $taskObj (@taskList) {

                $taskObj->submitToCmds('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Permission for remote commands sent to \'' . $name . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Permission for remote commands sent to ' . scalar @taskList . ' chat contacts'
                    . ' called \'' . $name . '\'',
                );
            }

        # ;submit -g <group>
        } elsif (defined $group) {

            # Ask the lead Chat task to identify every task in the <group>
            @taskList = $session->chatTask->findGroup($group);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'There are no Chat task sessions in the group \'' . $group . '\'',
                );
            }

            # Send permission to every chat contact in the group
            foreach my $taskObj (@taskList) {

                $taskObj->submitToCmds('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Permission for remote commands sent to 1 chat contact in the group \'' . $group
                    . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Permission for remote commands sent to ' . scalar @taskList
                    . ' chat contacts in the group \'' . $group . '\'',
                );
            }

        # ;submit
        } else {

            # Ask the lead Chat task to identify all Chat tasks
            @taskList = $session->chatTask->findAllTasks();

            # Send permission to every chat contact
            foreach my $taskObj (@taskList) {

                $taskObj->submitToCmds('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Permission for remote commands sent to 1 chat contact',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Permission for remote commands sent to all ' . scalar @taskList
                    . ' chat contacts',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ChatEscape;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatescape', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['escape', 'chatescape'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Forbids chat contacts to send remote commands';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $name, $nameFlag, $group, $groupFlag,
            @taskList,
        );

        # Extract switches
        ($switch, $name, @args) = $self->extract('-n', 1, @args);
        if (defined $switch) {

            $nameFlag = TRUE;
        }

        ($switch, $group, @args) = $self->extract('-g', 1, @args);
        if (defined $switch) {

            $groupFlag = TRUE;
        }

        # There should be no arguments left
        if (@args || (($nameFlag && ! defined $name) || $groupFlag && ! defined $group)) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif ($nameFlag && $groupFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -n and -g can\'t be combined',
            );
        }

        # ;escape -n <name>
        if (defined $name) {

            # Ask the lead Chat task to identify the task connected to all chat contacts called
            #   <name>
            @taskList = $session->chatTask->findName($name);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'You aren\'t connected to a chat contact called \'' . $name . '\'',
                );
            }

            # Send the confirmation to every chat contact called <name>
            foreach my $taskObj (@taskList) {

                $taskObj->escapeFromCmds('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Permission for remote commands withdrawn from \'' . $name . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Permission for remote commands withdrawn from ' . scalar @taskList
                    . ' chat contacts called \'' . $name . '\'',
                );
            }

        # ;escape -g <group>
        } elsif (defined $group) {

            # Ask the lead Chat task to identify every task in the <group>
            @taskList = $session->chatTask->findGroup($group);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'There are no Chat task sessions in the group \'' . $group . '\'',
                );
            }

            # Send the confirmation to every chat contact in the group
            foreach my $taskObj (@taskList) {

                $taskObj->escapeFromCmds('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Permission for remote commands withdrawn from 1 chat contact in the group \''
                    . $group . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Permission for remote commands withdrawn from ' . scalar @taskList
                    . ' chat contacts in the group \'' . $group . '\'',
                );
            }

        # ;escape
        } else {

            # Ask the lead Chat task to identify all Chat tasks
            @taskList = $session->chatTask->findAllTasks();

            # Send the confirmation to every chat contact
            foreach my $taskObj (@taskList) {

                $taskObj->escapeFromCmds('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Permission for remote commands withdrawn from 1 chat contact',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Permission for remote commands withdrawn from all ' . scalar @taskList
                    . ' chat contacts',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ChatCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatcommand', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ccm', 'chatcmd', 'chatcommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends a remote command to chat contacts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $name, $nameFlag, $group, $groupFlag, $cmd,
            @taskList,
        );

        # Extract switches
        ($switch, $name, @args) = $self->extract('-n', 1, @args);
        if (defined $switch) {

            $nameFlag = TRUE;
        }

        ($switch, $group, @args) = $self->extract('-g', 1, @args);
        if (defined $switch) {

            $groupFlag = TRUE;
        }

        # The remaining argument comprise the command to send
        if (($nameFlag && ! defined $name) || ($groupFlag && ! defined $group)) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif ($nameFlag && $groupFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -n and -g can\'t be combined',
            );
        }

        # The remote command to send, <text>, is stored in @args. The join operation removes spacing
        #   between elements, unless diamond brackets were used
        $cmd = join(' ', @args);

        # ;cmd -n <name> <text>
        if (defined $name) {

            # Ask the lead Chat task to identify the task connected to all chat contacts called
            #   <name>
            @taskList = $session->chatTask->findName($name);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'You aren\'t connected to a chat contact called \'' . $name . '\'',
                );
            }

            # Send the remote command to every chat contact called <name>
            foreach my $taskObj (@taskList) {

                $taskObj->sendRemoteCmd($cmd);
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Remote command sent to \'' . $name . '\'',
                );

            } else {
                return $self->complete(
                    $session, $standardCmd,
                    'Remote command sent to ' . scalar @taskList . ' chat contacts called \''
                    . $name . '\'',
                );
            }

        # ;cmd -g <group> <text>
        } elsif (defined $group) {

            # Ask the lead Chat task to identify every task in the <group>
            @taskList = $session->chatTask->findGroup($group);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'There are no Chat task sessions in the group \'' . $group . '\'',
                );
            }

            # Send the remote command to every chat contact in the group
            foreach my $taskObj (@taskList) {

                $taskObj->sendRemoteCmd($cmd);
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Remote command sent to 1 chat contact in the group \'' . $group . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Remote command sent to ' . scalar @taskList . ' chat contacts in the group \''
                    . $group . '\'',
                );
            }

        # ;cmd <text>
        } else {

            # Ask the lead Chat task to identify all Chat tasks
            @taskList = $session->chatTask->findAllTasks();

            # Send the remote command to every chat contact
            foreach my $taskObj (@taskList) {

                $taskObj->sendRemoteCmd($cmd);
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Remote command sent to 1 chat contact',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Remote command sent to all ' . scalar @taskList . ' chat contacts',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ChatSendFile;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatsendfile', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['csf', 'sendfile', 'chatsendfile'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends a file to chat contacts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $name, $nameFlag, $group, $groupFlag, $file,
            @taskList,
        );

        # Extract switches
        ($switch, $name, @args) = $self->extract('-n', 1, @args);
        if (defined $switch) {

            $nameFlag = TRUE;
        }

        ($switch, $group, @args) = $self->extract('-g', 1, @args);
        if (defined $switch) {

            $groupFlag = TRUE;
        }

        # Get the optional <file>
        $file = shift @args;

        # There should be no arguments left
        if (@args || (($nameFlag && ! defined $name) || $groupFlag && ! defined $group)) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif ($nameFlag && $groupFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -n and -g can\'t be combined',
            );
        }

        # If the user didn't specify a <file>, prompt them for one now
        if (! $file) {

            $file = $session->mainWin->showFileChooser(
                'Select file',
                'open',
            );

            if (! $file) {

                return $self->complete($session, $standardCmd, 'Send file operation cancelled');
            }
        }

        # ;csf -n <name>
        # ;csf -n <name> <file>
        if (defined $name) {

            # Ask the lead Chat task to identify the task connected to all chat contacts called
            #   <name>
            @taskList = $session->chatTask->findName($name);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'You aren\'t connected to a chat contact called \'' . $name . '\'',
                );
            }

            # Send the file to every chat contact called <name>
            foreach my $taskObj (@taskList) {

                $taskObj->sendFile($file);
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Sending file to \'' . $name . '\'',
                );

            } else {
                return $self->complete(
                    $session, $standardCmd,
                    'Sending file to ' . scalar @taskList . ' chat contacts called \'' . $name
                    . '\'',
                );
            }

        # ;csf -g <group>
        # ;csf -g <group> <file>
        } elsif (defined $group) {

            # Ask the lead Chat task to identify every task in the <group>
            @taskList = $session->chatTask->findGroup($group);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'There are no Chat task sessions in the group \'' . $group . '\'',
                );
            }

            # Send the file to every chat contact in the group
            foreach my $taskObj (@taskList) {

                $taskObj->sendFile($file);
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Sending file to 1 chat contact in the group \'' . $group . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Sending file to ' . scalar @taskList . ' chat contacts in the group \''
                    . $group . '\'',
                );
            }

        # ;csf
        # ;csf <file>
        } else {

            # Ask the lead Chat task to identify all Chat tasks
            @taskList = $session->chatTask->findAllTasks();

            # Send the file to every chat contact
            foreach my $taskObj (@taskList) {

                $taskObj->sendFile($file);
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Sending file to 1 chat contact',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Sending file to all ' . scalar @taskList . ' chat contacts',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ChatStopFile;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatstopfile', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cst', 'stopfile', 'chatstopfile'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Stops files transfers with chat contacts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $name, $nameFlag, $group, $groupFlag,
            @taskList,
        );

        # Extract switches
        ($switch, $name, @args) = $self->extract('-n', 1, @args);
        if (defined $switch) {

            $nameFlag = TRUE;
        }

        ($switch, $group, @args) = $self->extract('-g', 1, @args);
        if (defined $switch) {

            $groupFlag = TRUE;
        }

        # There should be no arguments left
        if (@args || (($nameFlag && ! defined $name) || $groupFlag && ! defined $group)) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif ($nameFlag && $groupFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -n and -g can\'t be combined',
            );
        }

        # ;cst -n <name>
        if (defined $name) {

            # Ask the lead Chat task to identify the task connected to all chat contacts called
            #   <name>
            @taskList = $session->chatTask->findName($name);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'You aren\'t connected to a chat contact called \'' . $name . '\'',
                );
            }

            # Halt file transfers with every chat contact called <name>
            foreach my $taskObj (@taskList) {

                $taskObj->stopFile('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Halting file transfers with \'' . $name . '\'',
                );

            } else {
                return $self->complete(
                    $session, $standardCmd,
                    'Halting file transfers with ' . scalar @taskList . ' chat contacts called \''
                    . $name . '\'',
                );
            }

        # ;cst -g <group>
        } elsif (defined $group) {

            # Ask the lead Chat task to identify every task in the <group>
            @taskList = $session->chatTask->findGroup($group);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'There are no Chat task sessions in the group \'' . $group . '\'',
                );
            }

            # Halt file transfers wth every chat contact in the group
            foreach my $taskObj (@taskList) {

                $taskObj->stopFile('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Halting file transfers with 1 chat contact in the group \'' . $group . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Halting file transfers with ' . scalar @taskList . ' chat contacts in the'
                    . ' group \'' . $group . '\'',
                );
            }

        # ;cst
        } else {

            # Ask the lead Chat task to identify all Chat tasks
            @taskList = $session->chatTask->findAllTasks();

            # Halt file transfers with every chat contact
            foreach my $taskObj (@taskList) {

                $taskObj->stopFile('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Halting file transfers with 1 chat contact',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Halting file transfers with all ' . scalar @taskList . ' chat contacts',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ChatSnoop;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatsnoop', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['snp', 'snoop', 'chatsnoop'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Asks to start/stop snooping on chat contacts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $name, $nameFlag, $group, $groupFlag,
            @taskList,
        );

        # Extract switches
        ($switch, $name, @args) = $self->extract('-n', 1, @args);
        if (defined $switch) {

            $nameFlag = TRUE;
        }

        ($switch, $group, @args) = $self->extract('-g', 1, @args);
        if (defined $switch) {

            $groupFlag = TRUE;
        }

        # There should be no arguments left
        if (@args || (($nameFlag && ! defined $name) || $groupFlag && ! defined $group)) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif ($nameFlag && $groupFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -n and -g can\'t be combined',
            );
        }

        # ;snp -n <name>
        if (defined $name) {

            # Ask the lead Chat task to identify the task connected to all chat contacts called
            #   <name>
            @taskList = $session->chatTask->findName($name);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'You aren\'t connected to a chat contact called \'' . $name . '\'',
                );
            }

            # Send the snoop request to every chat contact called <name>
            foreach my $taskObj (@taskList) {

                $taskObj->requestSnoop('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Snoop request sent to \'' . $name . '\'',
                );

            } else {
                return $self->complete(
                    $session, $standardCmd,
                    'Snoop request sent to ' . scalar @taskList . ' chat contacts called \'' . $name
                    . '\'',
                );
            }

        # ;snp -g <group>
        } elsif (defined $group) {

            # Ask the lead Chat task to identify every task in the <group>
            @taskList = $session->chatTask->findGroup($group);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'There are no Chat task sessions in the group \'' . $group . '\'',
                );
            }

            # Send the snoop request to every chat contact in the group
            foreach my $taskObj (@taskList) {

                $taskObj->requestSnoop('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Snoop request sent to 1 chat contacts in the group \'' . $group . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Snoop request sent to ' . scalar @taskList . ' chat contacts in the group \''
                    . $group . '\'',
                );
            }

        # ;snp
        } else {

            # Ask the lead Chat task to identify all Chat tasks
            @taskList = $session->chatTask->findAllTasks();

            # Send the snoop request to every chat contact
            foreach my $taskObj (@taskList) {

                $taskObj->requestSnoop('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Snoop request sent to 1 chat contact',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Snoop request sent to all ' . scalar @taskList . ' chat contacts',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ChatHangUp;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chathangup', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['chu', 'hangup', 'chathangup'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Hangs up on chat contacts';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $name, $nameFlag, $group, $groupFlag,
            @taskList,
        );

        # Extract switches
        ($switch, $name, @args) = $self->extract('-n', 1, @args);
        if (defined $switch) {

            $nameFlag = TRUE;
        }

        ($switch, $group, @args) = $self->extract('-g', 1, @args);
        if (defined $switch) {

            $groupFlag = TRUE;
        }

        # There should be no arguments left
        if (@args || (($nameFlag && ! defined $name) || $groupFlag && ! defined $group)) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif ($nameFlag && $groupFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -n and -g can\'t be combined',
            );
        }

        # ;chu -n <name>
        if (defined $name) {

            # Ask the lead Chat task to identify the task connected to all chat contacts called
            #   <name>
            @taskList = $session->chatTask->findName($name);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'You aren\'t connected to a chat contact called \'' . $name . '\'',
                );
            }

            # Hang up on every chat contact called <name>
            foreach my $taskObj (@taskList) {

                $taskObj->hangUp('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Hanging up on \'' . $name . '\'',
                );

            } else {
                return $self->complete(
                    $session, $standardCmd,
                    'Hanging up on ' . scalar @taskList . ' chat contacts called \'' . $name . '\'',
                );
            }

        # ;chu -g <group>
        } elsif (defined $group) {

            # Ask the lead Chat task to identify every task in the <group>
            @taskList = $session->chatTask->findGroup($group);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'There are no Chat task sessions in the group \'' . $group . '\'',
                );
            }

            # Hang up on every chat contact in the group
            foreach my $taskObj (@taskList) {

                $taskObj->hangUp('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Hanging up on 1 chat contact in the group \'' . $group . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Hanging up on ' . scalar @taskList . ' chat contacts in the group \''
                    . $group . '\'',
                );
            }

        # ;chu
        } else {

            # Ask the lead Chat task to identify all Chat tasks
            @taskList = $session->chatTask->findAllTasks();

            # Hang up on every chat contact
            foreach my $taskObj (@taskList) {

                $taskObj->hangUp('');
            }

            if (@taskList == 1) {

                return $self->complete($session, $standardCmd, 'Hanging up on 1 chat contact');

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Hanging up on all ' . scalar @taskList . ' chat contacts',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ChatPeek;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatpeek', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['chp', 'chatpeek'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows chat contact\'s own public connections';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $name, $nameFlag, $group, $groupFlag,
            @taskList,
        );

        # Extract switches
        ($switch, $name, @args) = $self->extract('-n', 1, @args);
        if (defined $switch) {

            $nameFlag = TRUE;
        }

        ($switch, $group, @args) = $self->extract('-g', 1, @args);
        if (defined $switch) {

            $groupFlag = TRUE;
        }

        # There should be no arguments left
        if (@args || (($nameFlag && ! defined $name) || $groupFlag && ! defined $group)) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif ($nameFlag && $groupFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -n and -g can\'t be combined',
            );
        }

        # ;chp -n <name>
        if (defined $name) {

            # Ask the lead Chat task to identify the task connected to all chat contacts called
            #   <name>
            @taskList = $session->chatTask->findName($name);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'You aren\'t connected to a chat contact called \'' . $name . '\'',
                );
            }

            # Send the request to every chat contact called <name>
            foreach my $taskObj (@taskList) {

                $taskObj->peekRemoteConnections('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Peek request sent to \'' . $name . '\'',
                );

            } else {
                return $self->complete(
                    $session, $standardCmd,
                    'Peek request sent to ' . scalar @taskList . ' chat contacts called \'' . $name
                    . '\'',
                );
            }

        # ;chp -g <group>
        } elsif (defined $group) {

            # Ask the lead Chat task to identify every task in the <group>
            @taskList = $session->chatTask->findGroup($group);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'There are no Chat task sessions in the group \'' . $group . '\'',
                );
            }

            # Send the peek request to every chat contact in the group
            foreach my $taskObj (@taskList) {

                $taskObj->peekRemoteConnections('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Peek request sent to 1 chat contact in the group \'' . $group . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Peek request sent to ' . scalar @taskList . ' chat contacts in the group \''
                    . $group . '\'',
                );
            }

        # ;chp
        } else {

            # Ask the lead Chat task to identify all Chat tasks
            @taskList = $session->chatTask->findAllTasks();

            # Send the peek request to every chat contact
            foreach my $taskObj (@taskList) {

                $taskObj->peekRemoteConnections('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Peek request sent to 1 chat contact',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Peek request sent to all ' . scalar @taskList . ' chat contacts',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ChatRequest;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatrequest', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['chr', 'chatrequest'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets up a conference call';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $name, $nameFlag, $group, $groupFlag,
            @taskList,
        );

        # Extract switches
        ($switch, $name, @args) = $self->extract('-n', 1, @args);
        if (defined $switch) {

            $nameFlag = TRUE;
        }

        ($switch, $group, @args) = $self->extract('-g', 1, @args);
        if (defined $switch) {

            $groupFlag = TRUE;
        }

        # There should be no arguments left
        if (@args || (($nameFlag && ! defined $name) || $groupFlag && ! defined $group)) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif ($nameFlag && $groupFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -n and -g can\'t be combined',
            );
        }

        # ;chr -n <name>
        if (defined $name) {

            # Ask the lead Chat task to identify the task connected to all chat contacts called
            #   <name>
            @taskList = $session->chatTask->findName($name);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'You aren\'t connected to a chat contact called \'' . $name . '\'',
                );
            }

            # Send the request to every chat contact called <name>
            foreach my $taskObj (@taskList) {

                $taskObj->requestRemoteConnections('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Conference call request sent to \'' . $name . '\'',
                );

            } else {
                return $self->complete(
                    $session, $standardCmd,
                    'Conference call request sent to ' . scalar @taskList . ' chat contacts'
                    . ' called \'' . $name . '\'',
                );
            }

        # ;chr -g <group>
        } elsif (defined $group) {

            # Ask the lead Chat task to identify every task in the <group>
            @taskList = $session->chatTask->findGroup($group);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'There are no Chat task sessions in the group \'' . $group . '\'',
                );
            }

            # Send the request to every chat contact in the group
            foreach my $taskObj (@taskList) {

                $taskObj->requestRemoteConnections('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Conference call request sent to 1 chat contact in the group \'' . $group
                    . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Conference call request sent to ' . scalar @taskList . ' chat contacts in the'
                    . ' group \'' . $group . '\'',
                );
            }

        # ;chr
        } else {

            # Ask the lead Chat task to identify all Chat tasks
            @taskList = $session->chatTask->findAllTasks();

            # Send the request to every chat contact
            foreach my $taskObj (@taskList) {

                $taskObj->requestRemoteConnections('');
            }

            if (@taskList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Conference call request sent to 1 chat contact',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Conference call request sent to all ' . scalar @taskList . ' chat contacts',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ChatInfo;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatinfo', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['chi', 'chatinfo'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Shows information about chat sessions';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @taskList;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );
        }

        # Ask the lead Chat task to identify all Chat tasks
        @taskList = $session->chatTask->findAllTasks();

        # Display header
        $session->writeText(
            'List of chat sessions (A peer can snoop, S is snooping, + public, > serving)',
        );
        $session->writeText('     Name              Type  IP               Port  Group');

        # Display list
        foreach my $taskObj (@taskList) {

            my ($column, $remoteName, $protocol, $ip, $port, $group);

            if ($taskObj->allowSnoopFlag) {
                $column = 'A';
            } else {
                $column = ' ';
            }

            if ($taskObj->isSnoopedFlag) {
                $column .= 'S';
            } else {
                $column .= ' ';
            }

            if ($taskObj->publicConnectionFlag) {
                $column .= '+';
            } else {
                $column .= ' ';
            }

            if ($taskObj->servingFlag) {
                $column .= '> ';
            } else {
                $column .= '  ';
            }

            if ($taskObj->sessionFlag) {

                $remoteName = $taskObj->remoteName;
                $ip = $taskObj->remoteIP;
                $port = $taskObj->remotePort;

            } else {

                $remoteName = '<waiting>';
                $ip = '';
                $port = '';
            }

            if ($taskObj->chatType == $taskObj->ivShow('constOptHash', 'MM')) {
                $protocol = 'MM';
            } else {
                $protocol = 'zChat';
            }

            if ($taskObj->localGroup) {
                $group = $taskObj->localGroup;
            } else {
                $group = '<no group>';
            }

            $session->writeText(
                $column . sprintf(
                    '%-16.16s %-5.5s  %-16.16s %-5.5s %-15.15s',
                    $remoteName,
                    $protocol,
                    $ip,
                    $port,
                    $group,
                )
            );
        }

        if (@taskList == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 chat session displayed)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @taskList . ' chat sessions displayed)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ChatSet;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatset', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['chs', 'chatset'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets various chat session settings';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $name, $nameFlag, $group, $groupFlag, $flagCount, $autoAcceptFlag,
            $autoAcceptContactFlag, $promptFlag, $allowSnoopFlag, $forbidSnoopFlag, $publicFlag,
            $privateFlag, $servingFlag, $notServingFlag, $encoding, $encodingFlag, $msg,
            @taskList,
        );

        # Extract <switches>
        ($switch, $name, @args) = $self->extract('-n', 1, @args);
        if (defined $switch) {

            $nameFlag = TRUE;
        }

        ($switch, $group, @args) = $self->extract('-g', 1, @args);
        if (defined $switch) {

            $groupFlag = TRUE;
        }

        # Extract other switches
        $flagCount = 0;

        ($switch, @args) = $self->extract('-i', 0, @args);
        if (defined $switch) {

            $autoAcceptFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-c', 0, @args);
        if (defined $switch) {

            $autoAcceptContactFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-u', 0, @args);
        if (defined $switch) {

            $promptFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-a', 0, @args);
        if (defined $switch) {

            $allowSnoopFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-f', 0, @args);
        if (defined $switch) {

            $forbidSnoopFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-p', 0, @args);
        if (defined $switch) {

            $publicFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-v', 0, @args);
        if (defined $switch) {

            $privateFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-s', 0, @args);
        if (defined $switch) {

            $servingFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-z', 0, @args);
        if (defined $switch) {

            $notServingFlag = TRUE;
            $flagCount++;
        }

        ($switch, $encoding, @args) = $self->extract('-e', 1, @args);
        if (defined $switch) {

            $encodingFlag = TRUE;
            $flagCount++;
        }

        # There should be no arguments left
        if (@args || ($nameFlag && ! defined $name) || ($groupFlag && ! defined $group)) {

            return $self->improper($session, $inputString);
        }

        # Deal with the switches -i, -c and -u, which don't require any Chat tasks to be running
        # ;chs -i
        if ($autoAcceptFlag) {

            $axmud::CLIENT->set_chatAcceptMode('accept_all');

            return $self->complete(
                $session, $standardCmd,
                'Now auto-accepting incoming chat requests from everyone',
            );

        # ;chs -c
        } elsif ($autoAcceptContactFlag) {

            $axmud::CLIENT->set_chatAcceptMode('accept_contact');

            return $self->complete(
                $session, $standardCmd,
                'Now auto-accepting incoming chat requests from known contacts',
            );

        # ;chs -u
        } elsif ($promptFlag) {

            $axmud::CLIENT->set_chatAcceptMode('prompt');

            return $self->complete(
                $session, $standardCmd,
                'Now prompting to ask if incoming chat requests should be accepted',
            );
        }

        # The remaining switches do require at least one running Chat task. Check there are Chat
        #   tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif ($nameFlag && $groupFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -n and -g can\'t be combined',
            );

        } elsif ($flagCount > 1) {

            return $self->error(
                $session, $inputString,
                'The switches -c, -u, -a, -f, -p, -v, -s, -z and -e can\'t be combined',
            );

        } elsif (($name || $group) && ($autoAcceptFlag || $promptFlag)) {

            return $self->error(
                $session, $inputString,
                'The switches -c, -u can\'t be combined with -g and -n',
            );
        }

        # Process <switches>
        if (defined $name) {

            # Ask the lead Chat task to identify which task(s) are connected to someone called
            #   <name>
            @taskList = $session->chatTask->findName($name);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'You aren\'t connected to a chat contact called \'' . $name . '\'',
                );
            }

        } elsif (defined $group) {

            # Ask the lead Chat task to identify which task(s) are in the <group>
            @taskList = $session->chatTask->findGroup($group);
            if (! @taskList) {

                return $self->error(
                    $session, $inputString,
                    'There are no Chat task sessions in the group \'' . $group . '\'',
                );
            }

        } else {

            # Ask the lead Chat task to identify all Chat tasks
            @taskList = $session->chatTask->findAllTasks();
        }

        # ;chs -a <switches>
        if ($allowSnoopFlag) {

            foreach my $taskObj (@taskList) {

                # Allow snooping for the specified tasks
                $taskObj->acceptSnoop('');
            }

            $msg = 'Now allowing snooping for ';

        # ;chs -f <switches>
        } elsif ($forbidSnoopFlag) {

            foreach my $taskObj (@taskList) {

                # Forbid snooping for the specified tasks
                $taskObj->refuseSnoop('');
            }

            $msg = 'Now forbidding snooping for ';

        # ;chs -p <switches>
        } elsif ($publicFlag) {

            foreach my $taskObj (@taskList) {

                # Mark the specified tasks as public connections
                $taskObj->setPublic('');
            }

            $msg = 'Marked connections as \'public\' for ';

        # ;chs -v <switches>
        } elsif ($privateFlag) {

            foreach my $taskObj (@taskList) {

                # Mark the specified tasks as private connections
                $taskObj->setPrivate('');
            }

            $msg = 'Marked connections as \'private\' for ';

        # ;chs -s <switches>
        } elsif ($servingFlag) {

            foreach my $taskObj (@taskList) {

                # Mark the specified tasks as serving connections
                $taskObj->setServing('');
            }

            $msg = 'Marked connections as \'serving\' for ';

        # ;chs -z <switches>
        } elsif ($notServingFlag) {

            foreach my $taskObj (@taskList) {

                # Mark the specified tasks as non-serving connections
                $taskObj->setNotServing('');
            }

            $msg = 'Marked connections as \'not serving\' for ';

        # ;chs -e <en> <switches>
        # ;chs -e <switches>
        } elsif ($encodingFlag) {

            foreach my $taskObj (@taskList) {

                if (defined $encoding) {

                    $taskObj->ivPoke('encoding', $encoding);
                    $msg = 'Set encoding to \'' . $encoding . '\' for ';

                } else {

                    $taskObj->ivPoke('encoding', $taskObj->defaultEncoding);
                    $msg = 'Set the default encoding for ';
                }
            }
        }

        # Display confirmation
        if (@taskList == 1) {

            return $self->complete($session, $standardCmd, $msg . '1 chat contact');

        } else {

            return $self->complete(
                $session, $standardCmd,
                $msg . 'all ' . scalar @taskList . ' chat contacts',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ChatSetName;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatsetname', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cname', 'chatsetname'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets your chat nickname';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the length is OK
        if (defined $name) {

            if (length ($name) < 3 || length ($name) > 16) {

                return $self->error(
                    $session, $inputString,
                    'Your chat nickname must be between 3 and 16 characters',
                );

            } elsif (defined $axmud::CLIENT->chatName && $axmud::CLIENT->chatName eq $name) {

                return $self->complete(
                    $session, $standardCmd,
                    'Your chat nickname is already set to \'' . $name . '\'',
                );
            }

        } elsif (! defined $axmud::CLIENT->chatName) {

            return $self->complete(
                $session, $standardCmd,
                'The current character\'s name is already being used as your chat nickname',
            );
        }

        # Store the <name>. The GA::Client IV is set to 'undef' when the current character's name
        #   should be used as the chat nickname (in which case, $name will be 'undef')
        $axmud::CLIENT->set_chatName($name);

        # If there are any Chat tasks running, inform them of the new name
        if ($session->chatTask) {

            if (defined $name) {
                $session->chatTask->setName($name);
            } else {
                $session->chatTask->setName('');    # Use current character
            }
        }

        if (defined $name) {

            return $self->complete(
                $session, $standardCmd,
                'Chat nickname set to \'' . $name . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Chat nickname reset (the current character\'s name will now be used instead)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ChatSetGroup;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatsetgroup', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['csg', 'setgroup', 'chatsetgroup'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the members of a chat group';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $group,
            @nameList,
        ) = @_;

        # Local variables
        my (
            $count,
            @taskList,
        );

        # Check for improper arguments
        if (! defined $group) {

            return $self->improper($session, $inputString);
        }

        # Check there are Chat tasks running
        if (! $session->chatTask) {

            return $self->error(
                $session, $inputString,
                'There are no Chat tasks running (try \';chatmcall\' or \';chatzcall\')',
            );

        } elsif (! $session->chatTask->sessionFlag) {

            return $self->error(
                $session, $inputString,
                'There are no Chat task sessions open (try \';chatmcall\' or \';chatzcall\')',
            );
        }

        # Ask the lead Chat task to identify all Chat tasks
        @taskList = $session->chatTask->findAllTasks();

        # If a name list was specified, check each Chat task to make sure at least one of them is
        #   connected to a chat contact with that name
        if (@nameList) {

            OUTER: foreach my $name (@nameList) {

                INNER: foreach my $taskObj (@taskList) {

                    if ($taskObj->remoteName && $taskObj->remoteName eq $name) {

                        next OUTER;
                    }
                }

                # None of the running Chat tasks are connected to someone called <name>
                return $self->error(
                    $session, $inputString,
                    'There are no Chat tasks running which are connected to someone called \''
                    . $name . '\'',
                );
            }
        }

        $count = 0;

        # ;csg <group> <list>
        if (@nameList) {

            OUTER: foreach my $taskObj (@taskList) {

                INNER: foreach my $name (@nameList) {

                    if ($taskObj->remoteName && $taskObj->remoteName eq $name) {

                        if (
                            ($taskObj->localGroup && $taskObj->localGroup ne $group)
                            || (! $taskObj->localGroup)
                        ) {
                            # This task isn't already in the group, so add it
                            $taskObj->setGroup($group);
                        }

                        $count++;
                        next OUTER;
                    }
                }

                # If this task was formerly a member of <group>, then it no longer is
                if ($taskObj->localGroup && $taskObj->localGroup eq $group) {

                    $taskObj->setGroup('');
                }
            }

            if ($count == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Set 1 member of the chat group \'' . $group . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Set ' . $count . ' members of the chat group \'' . $group . '\'',
                );
            }

        # ;csg <group>
        } else {

            OUTER: foreach my $taskObj (@taskList) {

                if ($taskObj->localGroup && $taskObj->localGroup eq $group) {

                    # Remove this task from the group
                    $taskObj->setGroup('');
                    $count++;
                }
            }

            if ($count == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Removed 1 member from the chat group \'' . $group . '\'',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Removed ' . $count . ' members from the chat group \'' . $group . '\'',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ChatSetEmail;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatsetemail', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cse', 'chatsetemail'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets your chat email';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $email,
            $check,
        ) = @_;

       # Local variables
        my @taskList;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the email is valid (if specified)
        if (
            defined $email
            && (length $email < 3 || length ($email) > 128 || index($email, '@') == -1)
        ) {
            return $self->error(
                $session, $inputString,
                'Invalid chat email address (must be between 3 and 128 characters (and contain an'
                . ' \'@\' character)',
            );
        }

        if (
            defined $email
            && defined $axmud::CLIENT->chatEmail
            && $email eq $axmud::CLIENT->chatEmail
        ) {
            return $self->complete(
                $session, $standardCmd,
                'Your chat email is already set to \'' . $email . '\'',
            );

        } elsif (! defined $email && ! defined $axmud::CLIENT->chatEmail) {

            return $self->complete(
                $session, $standardCmd,
                'Your chat email is already not being broadcast during chat sessions',
            );
        }

        # Store the <email>. The GA::Client IV is set to 'undef' when the user's email shouldn't
        #   be broadcast to chat contacts (in which case, $email will be 'undef')
        $axmud::CLIENT->set_chatEmail($email);

        # If there are any Chat tasks running, inform them of the new email
        if ($session->chatTask) {

            # Ask the lead Chat task to identify all Chat tasks
            @taskList = $session->chatTask->findAllTasks();
            foreach my $taskObj (@taskList) {

                if ($taskObj->sessionFlag) {

                    if (defined $email) {
                        $session->chatTask->setEmail($email);
                    } else {
                        $session->chatTask->setEmail('');    # Don't broadcast an email
                    }
                }
            }
        }

        if (defined $email) {

            return $self->complete($session, $standardCmd, 'Chat email set to \'' . $email . '\'');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Your email will no longer be broadcast in chat sessions',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ChatSetSmiley;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatsetsmiley', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['css', 'chatsetsmiley'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Turns smileys in all chat sessions on/off';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

       # Local variables
        my @taskList;

        # Check for improper arguments
        if ((defined $arg && $arg ne 'on' && $arg ne 'off') || ! defined $arg || defined $check) {

            return $self->improper($session, $inputString);
        }

        # If there are any Chat tasks running, change settings or not, as appropriate
        #   opposite setting)
        if (! $session->chatTask) {

            return $self->error($session, $inputString, 'No Chat tasks are running');

        } else {

            # Ask the lead Chat task to identify all Chat tasks
            @taskList = $session->chatTask->findAllTasks();
            foreach my $taskObj (@taskList) {

                if ($arg eq 'on' && ! $taskObj->allowSmileyFlag) {
                    $session->chatTask->setSmiley('on');
                } elsif ($arg eq 'off' && $taskObj->allowSmileyFlag) {
                    $session->chatTask->setSmiley('off');
                }
            }
        }

        if ($arg eq 'on') {

            return $self->complete(
                $session, $standardCmd,
                'Smileys turned on in all chat sessions',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Smileys turned off in all chat sessions',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ChatSetIcon;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('chatseticon', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['csi', 'chatseticon'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the icon used in chat sessions';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $path,
            $check,
        ) = @_;

        # Local variables
        my @taskList;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (defined $path) {

            if ($path eq '-d' && $axmud::CLIENT->chatIcon eq $axmud::CLIENT->constChatIcon) {

                return $self->complete(
                    $session, $standardCmd,
                    'You are already using the default chat icon',
                );

            } elsif ($path eq $axmud::CLIENT->chatIcon) {

                return $self->complete(
                    $session, $standardCmd,
                    'You are already using \'' . $path . '\' as your chat icon',
                );
            }
        }

        if (defined $path && $path eq '-d') {

            $path = $axmud::SHARE_DIR . '/' . $axmud::CLIENT->constChatIcon;

        } elsif (! $path) {

            # Prompt the user to specify a <file>
            $path = $session->mainWin->showFileChooser(
                'Select an icon file (must be .bmp)',
                'open',
            );

            if (! $path) {

                return $self->complete($session, $standardCmd, 'Set icon operation cancelled');
            }
        }

        # Store the icon
        $axmud::CLIENT->set_chatIcon($path);

        # If there are any Chat tasks running, inform them of the new icon
        if ($session->chatTask) {

            # Ask the lead Chat task to identify all Chat tasks
            @taskList = $session->chatTask->findAllTasks();
            foreach my $taskObj (@taskList) {

                if ($taskObj->sessionFlag) {

                    $taskObj->setIcon($path);
                }
            }
        }

        return $self->complete($session, $standardCmd, 'Chat icon set to \'' . $path . '\'');
    }
}

{ package Games::Axmud::Cmd::AddSmiley;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup. This command must ignore brackets and diamond brackets
        my $self = Games::Axmud::Generic::Cmd->new('addsmiley', TRUE, FALSE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['asm', 'addsmily', 'addsmiley'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a smiley for use with the Chat task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $smiley, $path,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $smiley || ! defined $path || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the smiley doesn't already exist
        if ($axmud::CLIENT->ivExists('chatSmileyHash', $smiley)) {

            return $self->error(
                $session, $inputString,
                'The smiley ' . $smiley . ' already exists (try \';deletesmiley\' first)',
            );
        }

        # Check the smiley isn't too long
        if (length ($smiley) > 8) {

            return $self->error(
                $session, $inputString,
                'The smiley ' . $smiley . ' is too long (maximum length: 8 characters)',
            );
        }

        # Check the path exists
        if (! (-e $path)) {

            return $self->error($session, $inputString, 'File \'' . $path . '\' not found');
        }

        # Add the smiley
        $axmud::CLIENT->add_chatSmiley($smiley, $path);

        return $self->complete($session, $standardCmd, 'Added Chat task smiley ' . $smiley);
    }
}

{ package Games::Axmud::Cmd::DeleteSmiley;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup. This command must ignore brackets and diamond brackets
        my $self = Games::Axmud::Generic::Cmd->new('deletesmiley', TRUE, FALSE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dsm', 'delsmiley', 'deletesmily', 'deletesmiley'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a smiley used by the Chat task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $smiley,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $smiley || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the smiley exists
        if (! $axmud::CLIENT->ivExists('chatSmileyHash', $smiley)) {

            return $self->error(
                $session, $inputString,
                'The smiley ' . $smiley . ' doesn\'t exist',
            );
        }

        # Delete the smiley
        $axmud::CLIENT->del_chatSmiley($smiley);

        return $self->complete($session, $standardCmd, 'Deleted Chat task smiley ' . $smiley);
    }
}

{ package Games::Axmud::Cmd::ListSmiley;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup. This command must ignore brackets and diamond brackets
        my $self = Games::Axmud::Generic::Cmd->new('listsmiley', TRUE, TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lsm', 'listsmily', 'listsmiley'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists smileys used by the Chat task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            @list,
            %hash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the hash of smileys, and then get a sorted list of smileys, sorted by filename
        #   (and then by size)
        %hash = $axmud::CLIENT->chatSmileyHash;
        @list = sort {
            if ($hash{$a} ne $hash{$b}) {
                return $hash{$a} cmp $hash{$b};
            } elsif (length $hash{$a} < length $hash{$b}) {
                return -1;
            } else {
                return 1;
            }
        } (keys %hash);

        if (! @list) {

            return $self->complete($session, $standardCmd, 'The Chat task smiley list is empty');
        }

        # Display header
        $session->writeText('List of Chat task smileys');

        # Display list
        foreach my $smiley (@list) {

            $session->writeText(sprintf('   %-8.8s', $smiley) . ' ' . $hash{$smiley});
        }

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 smiley found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @list . ' smileys found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ResetSmiley;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup. This command must ignore brackets and diamond brackets
        my $self = Games::Axmud::Generic::Cmd->new('resetsmiley', TRUE, FALSE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rsm', 'resetsmily', 'resetsmiley'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets Chat task smileys to the default list';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Reset the list
        $axmud::CLIENT->set_chatSmileyHash($axmud::CLIENT->constChatSmileyHash);

        return $self->complete($session, $standardCmd, 'Chat task smiley list reset');
    }
}

# Compass task

{ package Games::Axmud::Cmd::Compass;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('compass', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cmp', 'compass'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies the Compass task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $hashRef, $otherHashRef, $taskObj, $string, $key, $keycode, $cmd,
            %hash, %otherHash,
        );

        # (No improper arguments to check)

        # %hash to convert all the <key>s that the Compass task allows us to customise
        # %otherHash of other keypad <key>s that the Compass task doesn't allow us to customise
        ($hashRef, $otherHashRef) = $self->getKeypadHashes();
        %hash = %$hashRef;
        %otherHash = %$otherHashRef;

        # In all cases, check there's a compass task running
        $taskObj = $session->compassTask;
        if (! $taskObj) {

            return $self->error($session, $inputString, 'There is no Compass task running');
        }

        # ;compass
        if (! @args) {

            # Display header
            $session->writeText('Current compass task settings');

            # Display list
            if ($taskObj->activeFlag) {
                $string = 'ENABLED';
            } else {
                $string = 'DISABLED';
            }

            $session->writeText('   Keypad:          ' . $string);

            $session->writeText('World commands corresponding to each keypad key');

            foreach my $keycode (
                'kp_0', 'kp_5', 'kp_multiply', 'kp_divide', 'kp_full_stop', 'kp_enter',
            ) {
                my $cmd;

                if ($taskObj->ivExists('keypadDirHash', $keycode)) {
                    $cmd = $taskObj->ivShow('keypadDirHash', $keycode);
                } else {
                    $cmd = $taskObj->ivShow('keypadCmdHash', $keycode);
                }

                $session->writeText(sprintf('   %-16.16s ', $keycode) . $cmd);
            }

            # Display footer
            return $self->complete($session, $standardCmd, 'End of list');

        # ;cmp on
        # ;cmp -o
        } elsif ($args[0] eq 'on' || $args[0] eq '-o') {

            # (For the benefit of visually-impaired users, ignore everything after the first
            #   argument)
            if ($taskObj->enabledFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Keypad world commands are already enabled',
                );

            } elsif (! $taskObj->enable()) {

                return $self->error(
                    $session, $inputString,
                    'Could not enable keypad world commands',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Keypad world commands have been enabled',
                );
            }

        # ;cmp off
        # ;cmp -f
        } elsif ($args[0] eq 'off' || $args[0] eq '-f') {

            # (For the benefit of visually-impaired users, ignore everything after the first
            #   argument)
            if (! $taskObj->enabledFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Keypad world commands are already disabled',
                );

            } elsif (! $taskObj->disable()) {

                return $self->error(
                    $session, $inputString,
                    'Could not disable keypad world commands',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Keypad world commands have been disabled',
                );
            }

        # ;cmp <key> <command>
        # ;cmp <key>
        } else {

            # Get the Axmud standard keycode
            $key = shift @args;
            if (! exists $hash{$key} && ! exists $otherHash{$key}) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised keypad key (try \';help compass\' for a list of recognised keys)',
                );

            } elsif (exists $otherHash{$key}) {

                return $self->error(
                    $session, $inputString,
                    'The Compass task doesn\'t allow us to customise the \'' . $otherHash{$key}
                    . '\' key',
                );

            } else {

                $keycode = $hash{$key};
            }

            # Set the corresponding world <command> (if one was specified)
            if (@args) {

                $cmd = join (' ', @args);
            }

            # Update the task
            if (! $taskObj->set_key($keycode, $cmd)) {

                return $self->error(
                    $session, $inputString,
                    'General error modifying the Compass task',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Set a world command for the keypad key \'' . $key . '\'',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::PermCompass;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('permcompass', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['pcm', 'permcompass'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies the global initial Compass task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (@taskList, @initTaskList);

        # For the benefit of visually-impaired users, display an error message rather than improper
        #   args
        if (! @args) {

            return $self->complete(
                $session, $standardCmd,
                'Do what to your Compass tasks? (Try \';help permcompass\')',
            );
        }

        # Get all tasks of this kind from the current tasklist...
        push (@taskList, $self->findTask($session, 'compass_task'));
        # ...and also from the global initial tasklist
        push (@initTaskList, $self->findGlobalInitialTask('compass_task'));

        # The called function produces its own ->complete or ->error messages
        return $self->updateCompass(
            $session,
            $inputString,
            $standardCmd,
            \@args,
            \@taskList,
            \@initTaskList,
        );
    }
}

{ package Games::Axmud::Cmd::WorldCompass;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('worldcompass', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['wcm', 'worldcompass'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Modifies the world\'s initial Compass task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (@taskList, @initTaskList);

        # For the benefit of visually-impaired users, display an error message rather than improper
        #   args
        if (! @args) {

            return $self->complete(
                $session, $standardCmd,
                'Do what to your Compass tasks? (Try \';help permcompass\')',
            );
        }

        # Get all tasks of this kind from the current tasklist...
        push (@taskList, $self->findTask($session, 'compass_task'));
        # ...and also from the global initial tasklist
        push (
            @initTaskList,
            $self->findProfileInitialTask('compass_task', $session->currentWorld),
        );

        # The called function produces its own ->complete or ->error messages
        return $self->updateCompass(
            $session,
            $inputString,
            $standardCmd,
            \@args,
            \@taskList,
            \@initTaskList,
        );
    }
}

# Inventory / Condition tasks

{ package Games::Axmud::Cmd::ActivateInventory;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('activateinventory', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['aiv', 'actinv', 'activateinventory'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Activates the Inventory/Condition tasks';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Don't activate the Condition task, if the Inventory task isn't running
        if (! $session->inventoryTask) {

            return $self->error($session, $inputString, 'The Inventory task isn\'t running');
        }

        # Activate the Inventory task
        if ($session->inventoryTask->activeFlag) {

            return $self->error(
                $session, $inputString,
                'The Inventory task is already running in \'active\' mode',
            );

        } elsif (! $session->inventoryTask->activate()) {

            return $self->error(
                $session, $inputString,
                'Error while activating the Inventory task',
            );
        }

        # If the Condition task is also running, activate it
        if ($session->conditionTask) {

            # Don't show an error if it's already in 'active' mode (very unlikely)
            if (
                ! $session->conditionTask->activeFlag
                && ! $session->conditionTask->activate()
            ) {
                return $self->error(
                    $session, $inputString,
                    'Error while activating the Condition task (but the Inventory task was'
                    . ' activated)',
                );
            }
        }

        if (! $session->conditionTask) {

            return $self->complete($session, $standardCmd, 'Inventory task activated');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Inventory and Condition tasks activated',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DisactivateInventory;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('disactivateinventory', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['div', 'disinv', 'disactivateinventory'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Disactivates the Inventory/Condition tasks';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Don't disactivate the Condition task, if the Inventory task isn't running
        if (! $session->inventoryTask) {

            return $self->error($session, $inputString, 'The Inventory task isn\'t running');
        }

        # Disctivate the Inventory task
        if (! $session->inventoryTask->activeFlag) {

            return $self->error(
                $session, $inputString,
                'The Inventory task is already running in \'active\' mode',
            );

        } elsif (! $session->inventoryTask->disactivate()) {

            return $self->error(
                $session, $inputString,
                'Error while disactivating the Inventory task',
            );
        }

        # If the Condition task is also running, disactivate it
        if ($session->conditionTask) {

            # Don't show an error if it's already not in 'active' mode (very unlikely)
            if (
                $session->conditionTask->activeFlag
                && ! $session->conditionTask->disactivate()
            ) {
                return $self->error(
                    $session, $inputString,
                    'Error while disactivating the Condition task (but the Inventory task was'
                    . ' disactivated)',
                );
            }
        }

        if (! $session->conditionTask) {

            return $self->complete($session, $standardCmd, 'Inventory task disactivated');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Inventory and Condition tasks disactivated',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ProtectObject;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('protectobject', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['prt', 'protect', 'protectobj', 'protectobject'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Grants an object semi-protection';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switchCount, $loopCount, $switch, $arg, $mainNoun, $newObj,
            @categories, @nouns, @otherNouns, @adjs, @pseudos, @unknowns, @inventoryList,
            @protectList,
        );

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Check that there is a current character
        if (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t protect objects - no current character profile',
            );
        }

        # Extract switches; there can be multiple instances of each switch, so continue extracting
        #   them until there are none left
        $switchCount = 0;
        do {

            $loopCount = 0;

            ($switch, $arg, @args) = $self->extract('-c', 1, @args);
            if (defined $switch) {

                if (! defined $arg) {

                    return $self->improper($session, $inputString);

                } else {

                    push (@categories, $arg);
                    $switchCount++;
                    $loopCount++;
                }
            }

            ($switch, $arg, @args) = $self->extract('-n', 1, @args);
            if (defined $switch) {

                if (! defined $arg) {

                    return $self->improper($session, $inputString);

                } else {

                    push (@nouns, $arg);
                    $switchCount++;
                    $loopCount++;
                }
            }

            ($switch, $arg, @args) = $self->extract('-o', 1, @args);
            if (defined $switch) {

                if (! defined $arg) {

                    return $self->improper($session, $inputString);

                } else {

                    push (@otherNouns, $arg);
                    $switchCount++;
                    $loopCount++;
                }
            }

            ($switch, $arg, @args) = $self->extract('-a', 1, @args);
            if (defined $switch) {

                if (! defined $arg) {

                    return $self->improper($session, $inputString);

                } else {

                    push (@adjs, $arg);
                    $switchCount++;
                    $loopCount++;
                }
            }

            ($switch, $arg, @args) = $self->extract('-p', 1, @args);
            if (defined $switch) {

                if (! defined $arg) {

                    return $self->improper($session, $inputString);

                } else {

                    push (@pseudos, $arg);
                    $switchCount++;
                    $loopCount++;
                }
            }

            ($switch, $arg, @args) = $self->extract('-u', 1, @args);
            if (defined $switch) {

                if (! defined $arg) {

                    return $self->improper($session, $inputString);

                } else {

                    push (@unknowns, $arg);
                    $switchCount++;
                    $loopCount++;
                }
            }

        } until (! $loopCount);

        # <list> and <switch options> can't be combined
        if ($switchCount && @args) {

            return $self->error(
                $session, $inputString,
                'The <switch options> and a <list> of words can\'t be combined with this command',
            );
        }

        # If categories were specified, check that they're all recognised
        foreach my $category (@categories) {

            if (! $axmud::CLIENT->ivExists('constModelTypeHash', $category)) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised category \'' . $category . '\'',
                );
            }
        }

        # ;prt <list>
        if (@args) {

            # Check that an Inventory task is running
            if (! $session->inventoryTask) {

                return $self->error(
                    $session, $inputString,
                    'Cannot protect items in the character\'s inventory because the Inventory task'
                    . ' isn\'t running',
                );

            # Check that the current inventory isn't empty
            } elsif (! $session->inventoryTask->inventoryList) {

                return $self->error(
                    $session, $inputString,
                    'The character\'s inventory is empty (according to the Inventory task)',
                );
            }

            # Import the Inventory task's inventory list
            @inventoryList = $session->inventoryTask->inventoryList;

            # Mark every item in the current inventory matching any of the words in <list> as
            #   needing protection
            OUTER: foreach my $obj (@inventoryList) {

                INNER: foreach my $word (@args) {

                    if (
                        $obj->category eq $word
                        || $obj->noun eq $word
                        || defined $obj->ivFind('otherNounList', $word)
                        || defined $obj->ivFind('adjList', $word)
                        || defined $obj->ivFind('pseudoAdjList', $word)
                        || defined $obj->ivFind('unknownWordList', $word)
                    ) {
                        # Protect this object
                        push (@protectList, $obj);
                        # Move on to the next object
                        next OUTER;
                    }
                }
            }

            if (@protectList) {

                # Protect each marked item by creating a GA::Obj::Protect for it
                foreach my $obj (@protectList) {

                    my $protectObj = Games::Axmud::Obj::Protect->new($session);
                    if (! $protectObj) {

                        return $self->error(
                            $session, $inputString,
                            'General error creating protected object',
                        );
                    }

                    # Set the protected object's IVs to match the inventory object's IVs
                    $protectObj->ivPoke('noun', $obj->noun);
                    $protectObj->ivPoke('categoryList', $obj->category);
                    $protectObj->ivPoke('otherNounList', $obj->otherNounList);
                    $protectObj->ivPoke('adjList', $obj->adjList);
                    $protectObj->ivPoke('pseudoAdjList', $obj->pseudoAdjList);
                    $protectObj->ivPoke('unknownWordList', $obj->unknownWordList);

                    # Add the protected object to the character profile's list
                    $session->currentChar->ivPush('protectObjList', $protectObj);
                }

                # Refresh the Inventory task's window (which shows protected objects)
                $session->inventoryTask->set_refreshWinFlag(TRUE);
            }

            # Display confirmation
            if (! @protectList) {

                return $self->error(
                    $session, $inputString,
                    'No matching objects in the character\'s inventory were found',
                );

            } elsif (@protectList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Protected 1 matching object in the character\'s inventory',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Protected ' . scalar @protectList . ' matching objects in the character\'s'
                    . ' inventory',
                );
            }

        # ;prt <switch options>
        } else {

            # Create a new protected object, which isn't necessarily based on any specific object in
            #   the world
            $newObj = Games::Axmud::Obj::Protect->new($session);
            if (! $newObj) {

                return $self->error(
                    $session, $inputString,
                    'General error creating protected object',
                );
            }

            # Only the first -n switch option is used as the protected object's main noun; if there
            #   were any other switch options, they must be used as other nouns
            if (@nouns) {

                $mainNoun = shift (@nouns);
                push (@otherNouns, @nouns);
            }

            # Set the protected object's IVs to match the inventory object's IVs
            $newObj->ivPoke('noun', $mainNoun);
            $newObj->ivPoke('categoryList', @categories);
            $newObj->ivPoke('otherNounList', @otherNouns);
            $newObj->ivPoke('adjList', @adjs);
            $newObj->ivPoke('pseudoAdjList', @pseudos);
            $newObj->ivPoke('unknownWordList', @unknowns);

            # Add the protected object to the character profile's list
            $session->currentChar->ivPush('protectObjList', $newObj);

            # Refresh the Inventory task window (which shows protected objects)
            $session->inventoryTask->set_refreshWinFlag(TRUE);

            if ($mainNoun) {

                return $self->complete(
                    $session, $standardCmd,
                    'Added a new object called \'' . $mainNoun . '\' to the character\'s protected'
                    . ' object list',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Added a new (unnamed) object to the character\'s protected object list',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::UnprotectObject;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('unprotectobject', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['upr', 'unprotect', 'unprotectobj', 'unprotectobject'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Removes an object\'s semi-protection';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (! defined $number || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that there is a current character
        if (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t unprotect objects - no current character profile',
            );
        }

        # Check that the protected objects list isn't empty
        if (! $session->currentChar->protectObjList) {

            return $self->complete(
                $session, $standardCmd,
                'The character\'s protected object list is empty',
            );

        } else {

            # Import the list of protected objects
            @list = $session->currentChar->protectObjList;
        }

        # Check that <number> is a valid value
        if (
            ! $axmud::CLIENT->intCheck($number, 1, (scalar @list))
            || ! defined $list[$number - 1]
        ) {
            return $self->error(
                $session, $inputString,
                'Invalid number \'' . $number . '\' - try \';listprotectobject\' command first',
            );
        }

        # Remove the object from the protected items list
        $session->currentChar->ivSplice('protectObjList', ($number - 1), 1);
        # Refresh the Inventory task's window
        $session->inventoryTask->set_refreshWinFlag(TRUE);

        return $self->complete(
            $session, $standardCmd,
            'Object #' . $number . ' removed from the character\'s protected object list',
        );
    }
}

{ package Games::Axmud::Cmd::ListProtectObject;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listprotectobject', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lpo', 'listprotect', 'listprotectobject'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists objects enjoying semi-protection';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $count,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that there is a current character
        if (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t display protected objects - no current character profile',
            );
        }

        # Check that the protected objects list isn't empty
        if (! $session->currentChar->protectObjList) {

            return $self->complete(
                $session, $standardCmd,
                'The character\'s protected object list is empty',
            );
        }

        # Display header
        $session->writeText('Protected object list for \'' . $session->currentChar->name . '\'');

        # Display list
        @list = $session->currentChar->protectObjList;
        $count = 0;

        foreach my $obj (@list) {

            $count++;

            if ($obj->noun) {
                $session->writeText(' ' . sprintf('%-3.3s', $count) . ' Object: ' . $obj->noun);
            } else {
                $session->writeText(' ' . sprintf('%-3.3s', $count) . ' Object: <unnamed>');
            }

            if ($obj->categoryList) {

                $session->writeText('        Categories  : ' . join(' / ', $obj->categoryList));
            }

            if ($obj->otherNounList) {

                $session->writeText('        Other nouns : ' . join(' / ', $obj->otherNounList));
            }

            if ($obj->adjList) {

                $session->writeText('        Adjectives  : ' . join(' / ', $obj->adjList));
            }

            if ($obj->pseudoAdjList) {

                $session->writeText('        Pseudo-adjs : ' . join(' / ', $obj->pseudoAdjList));
            }

            if ($obj->unknownWordList) {

                $session->writeText('        Unknowns    : ' . join(' / ', $obj->unknownWordList));
            }
        }

        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'List displayed (1 object found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'List displayed (' . @list . ' objects found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::MonitorObject;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('monitorobject', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mno', 'monitor', 'monitorobj', 'monitorobject'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets objects monitored by the Condition task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args
        ) = @_;

        # Local variables
        my (
            $switchCount, $loopCount, $switch, $arg, $mainNoun, $newObj,
            @categories, @nouns, @otherNouns, @adjs, @pseudos, @unknowns, @inventoryList,
            @monitorList,
        );

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Check that there is a current character
        if (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t monitor objects - no current character profile',
            );
        }

        # Extract switches; there can be multiple instances of each switch, so continue extracting
        #   them until there are none left
        $switchCount = 0;
        do {

            $loopCount = 0;

            ($switch, $arg, @args) = $self->extract('-c', 1, @args);
            if (defined $switch) {

                if (! defined $arg) {

                    return $self->improper($session, $inputString);

                } else {

                    push (@categories, $arg);
                    $switchCount++;
                    $loopCount++;
                }
            }

            ($switch, $arg, @args) = $self->extract('-n', 1, @args);
            if (defined $switch) {

                if (! defined $arg) {

                    return $self->improper($session, $inputString);

                } else {

                    push (@nouns, $arg);
                    $switchCount++;
                    $loopCount++;
                }
            }

            ($switch, $arg, @args) = $self->extract('-o', 1, @args);
            if (defined $switch) {

                if (! defined $arg) {

                    return $self->improper($session, $inputString);

                } else {

                    push (@otherNouns, $arg);
                    $switchCount++;
                    $loopCount++;
                }
            }

            ($switch, $arg, @args) = $self->extract('-a', 1, @args);
            if (defined $switch) {

                if (! defined $arg) {

                    return $self->improper($session, $inputString);

                } else {

                    push (@adjs, $arg);
                    $switchCount++;
                    $loopCount++;
                }
            }

            ($switch, $arg, @args) = $self->extract('-p', 1, @args);
            if (defined $switch) {

                if (! defined $arg) {

                    return $self->improper($session, $inputString);

                } else {

                    push (@pseudos, $arg);
                    $switchCount++;
                    $loopCount++;
                }
            }

            ($switch, $arg, @args) = $self->extract('-u', 1, @args);
            if (defined $switch) {

                if (! defined $arg) {

                    return $self->improper($session, $inputString);

                } else {

                    push (@unknowns, $arg);
                    $switchCount++;
                    $loopCount++;
                }
            }

        } until (! $loopCount);

        # <list> and <switch options> can't be combined
        if ($switchCount && @args) {

            return $self->error(
                $session, $inputString,
                'The <switch options> and a <list> of words can\'t be combined with this command',
            );
        }

        # If categories were specified, check that they're all recognised
        foreach my $category (@categories) {

            if (! $axmud::CLIENT->ivExists('constModelTypeHash', $category)) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised category \'' . $category . '\'',
                );
            }
        }

        # ;mno <list>
        if (@args) {

            # Check that an Inventory task is running
            if (! $session->inventoryTask) {

                return $self->error(
                    $session, $inputString,
                    'Cannot monitor items in the character\'s inventory because the Inventory task'
                    . ' isn\'t running',
                );

            # Check that the current inventory isn't empty
            } elsif (! $session->inventoryTask->inventoryList) {

                return $self->error(
                    $session, $inputString,
                    'The character\'s inventory is empty (according to the Inventory task)',
                );
            }

            # Import the Inventory task's inventory list
            @inventoryList = $session->inventoryTask->inventoryList;

            # Mark every item in the current inventory matching any of the words in <list> as
            #   needing monitoring
            OUTER: foreach my $obj (@inventoryList) {

                INNER: foreach my $word (@args) {

                    if (
                        $obj->category eq $word
                        || $obj->noun eq $word
                        || defined $obj->ivFind('otherNounList', $word)
                        || defined $obj->ivFind('adjList', $word)
                        || defined $obj->ivFind('pseudoAdjList', $word)
                        || defined $obj->ivFind('unknownWordList', $word)
                    ) {
                        # Monitor this object
                        push (@monitorList, $obj);
                        # Move on to the next object
                        next OUTER;
                    }
                }
            }

            if (! @monitorList) {

                return $self->error(
                    $session, $inputString,
                    'No matching objects in the character\'s inventory were found',
                );
            }

            # Monitor each marked item by creating a GA::Obj::Monitor for it
            foreach my $obj (@monitorList) {

                my $monitorObj = Games::Axmud::Obj::Monitor->new($session);
                if (! $monitorObj) {

                    return $self->error(
                        $session, $inputString,
                        'General error creating monitored object',
                    );
                }

                # Set the monitored object's IVs to match the inventory object's IVs
                $monitorObj->ivPoke('noun', $obj->noun);
                $monitorObj->ivPoke('categoryList', $obj->category);
                $monitorObj->ivPoke('otherNounList', $obj->otherNounList);
                $monitorObj->ivPoke('adjList', $obj->adjList);
                $monitorObj->ivPoke('pseudoAdjList', $obj->pseudoAdjList);
                $monitorObj->ivPoke('unknownWordList', $obj->unknownWordList);

                # Add the monitored object to the character profile's list
                $session->currentChar->ivPush('monitorObjList', $monitorObj);
            }

            # Refresh the Inventory task's window (which shows monitored objects)
            $session->inventoryTask->set_refreshWinFlag(TRUE);
            # If the Condition task is running, tell it to stop any check that it's already doing
            #   (so it doesn't check an unmonitored object)
            if ($session->conditionTask) {

                $session->conditionTask->endCheck();
            }

            # Display confirmation
            if (@monitorList == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'Now monitoring 1 matching object in the character\'s inventory',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Now monitoring ' . scalar @monitorList . ' matching objects in the'
                    . ' character\'s inventory',
                );
            }

        # ;mno <switch options>
        } else {

            # Create a new monitored object, which isn't necessarily based on any specific object in
            #   the world
            $newObj = Games::Axmud::Obj::Monitor->new($session);
            if (! $newObj) {

                return $self->error(
                    $session, $inputString,
                    'General error creating monitored object',
                );
            }

            # Only the first -n switch option is used as the monitored object's main noun; if there
            #   were any other switch options, they must be used as other nouns
            if (@nouns) {

                $mainNoun = shift (@nouns);
                push (@otherNouns, @nouns);
            }

            # Set the monitored object's IVs to match the inventory object's IVs
            $newObj->ivPoke('noun', $mainNoun);
            $newObj->ivPoke('categoryList', @categories);
            $newObj->ivPoke('otherNounList', @otherNouns);
            $newObj->ivPoke('adjList', @adjs);
            $newObj->ivPoke('pseudoAdjList', @pseudos);
            $newObj->ivPoke('unknownWordList', @unknowns);

            # Add the monitored object to the character profile's list
            $session->currentChar->ivPush('monitorObjList', $newObj);

            # Refresh the Inventory task window (which shows monitored objects)
            $session->inventoryTask->set_refreshWinFlag(TRUE);
            # If the Condition task is running, tell it to stop any check that it's already doing
            #   (so it doesn't check an unmonitored object)
            if ($session->conditionTask) {

                $session->conditionTask->endCheck();
            }

            if ($mainNoun) {

                return $self->complete(
                    $session, $standardCmd,
                    'Added a new object called \'' . $mainNoun . '\' to the character\'s monitored'
                    . ' object list',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Added a new (unnamed) object to the character\'s monitored object list',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::UnmonitorObject;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('unmonitorobject', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['umo', 'unmonitor', 'unmonitorobj', 'unmonitorobject'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Removes objects monitored by the Condition task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (! defined $number || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that there is a current character
        if (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t unmonitor objects - no current character profile',
            );
        }

        # Check that the monitored objects list isn't empty
        if (! $session->currentChar->monitorObjList) {

            return $self->complete(
                $session, $standardCmd,
                'The character\'s monitored object list is empty',
            );

        } else {

            # Import the list of monitored objects
            @list = $session->currentChar->monitorObjList;
        }

        # Check that <number> is a valid value
        if (
            ! $axmud::CLIENT->intCheck($number, 1, (scalar @list))
            || ! defined $list[$number - 1]
        ) {
            return $self->error(
                $session, $inputString,
                'Invalid number \'' . $number . '\' - try \';listmonitorobject\' command first',
            );
        }

        # Remove the object from the monitored items list
        $session->currentChar->ivSplice('monitorObjList', ($number - 1), 1);
        # Refresh the Inventory task's window
        $session->inventoryTask->set_refreshWinFlag(TRUE);
        # If the Condition task is running, tell it to stop any check that it's already doing
        #   (so it doesn't check an unmonitored object)
        if ($session->conditionTask) {

            $session->conditionTask->endCheck();
        }

        return $self->complete(
            $session, $standardCmd,
            'Object #' . $number . ' removed from the character\'s monitored object list',
        );
    }
}

{ package Games::Axmud::Cmd::ListMonitorObject;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listmonitorobject', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'lmo',
            'listmo',
            'listmonitor',
            'listmonitorobject',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists objects monitored by the Condition task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $count,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that there is a current character
        if (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t display monitored objects - no current character profile',
            );
        }

        # Check that the monitored objects list isn't empty
        if (! $session->currentChar->monitorObjList) {

            return $self->complete(
                $session, $standardCmd,
                'The character\'s monitored object list is empty',
            );
        }

        # Display header
        $session->writeText('Monitored object list for \'' . $session->currentChar->name . '\'');

        # Display list
        @list = $session->currentChar->monitorObjList;
        $count = 0;

        foreach my $obj (@list) {

            $count++;

            if ($obj->noun) {
                $session->writeText(' ' . sprintf('%-3.3s', $count) . ' Object: ' . $obj->noun);
            } else {
                $session->writeText(' ' . sprintf('%-3.3s', $count) . ' Object: <unnamed>');
            }

            if ($obj->categoryList) {

                $session->writeText('        Categories  : ' . join(' / ', $obj->categoryList));
            }

            if ($obj->otherNounList) {

                $session->writeText('        Other nouns : ' . join(' / ', $obj->otherNounList));
            }

            if ($obj->adjList) {

                $session->writeText('        Adjectives  : ' . join(' / ', $obj->adjList));
            }

            if ($obj->pseudoAdjList) {

                $session->writeText('        Pseudo-adjs : ' . join(' / ', $obj->pseudoAdjList));
            }

            if ($obj->unknownWordList) {

                $session->writeText('        Unknowns    : ' . join(' / ', $obj->unknownWordList));
            }
        }

        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'List displayed (1 object found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'List displayed (' . @list . ' objects found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SellAll;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('sellall', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sall', 'sellall'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sells all unprotected objects in the inventory';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $count,
            @inventoryList, @protectList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there is an Inventory task running (without which, the current inventory is unknown)
        if (! $session->inventoryTask) {

            return $self->error(
                $session, $inputString,
                'Can\'t sell unprotected objects - no Inventory task running',
            );

        # Check that there is a current character
        } elsif (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t sell unprotected objects - no current character selected',
            );
        }

        # Import the current inventory
        @inventoryList = $session->inventoryTask->inventoryList;
        # Import the list of protected objects
        @protectList = $session->currentChar->protectObjList;

        if (! @inventoryList) {

            return $self->complete(
                $session, $standardCmd,
                'The character\'s inventory is empty (according to the Inventory task)',
            );
        }

        $count = 0;
        OUTER: foreach my $obj (@inventoryList) {

            # Check whether the object is on the protected object list
            if (
                ! @protectList
                || (
                    ! $session->worldModelObj->objCompare(
                        $session->inventoryTask->sensitivity,
                        $obj,
                        @protectList,
                    )
                )
            ) {
                # The object isn't protected, so we can sell it
                $session->sendModCmd('sell', 'object', $obj->noun);
                $count++;
            }
        }

        if (! $count) {

            return $self->complete($session, $standardCmd, 'No inventory objects sold');

        } elsif ($count == 1) {

            return $self->complete($session, $standardCmd, 'Attempted to sell 1 inventory object');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Attempted to sell ' . $count . ' inventory objects',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DropAll;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('dropall', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dall', 'dropall'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Drops all unprotected objects in the inventory';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $count,
            @inventoryList, @protectList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check there is an Inventory task running (without which, the current inventory is unknown)
        if (! $session->inventoryTask) {

            return $self->error(
                $session, $inputString,
                'Can\'t drop unprotected objects - no Inventory task running',
            );

        # Check that there is a current character
        } elsif (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t drop unprotected objects - no current character selected',
            );
        }

        # Import the current inventory
        @inventoryList = $session->inventoryTask->inventoryList;
        # Import the list of protected objects
        @protectList = $session->currentChar->protectObjList;

        if (! @inventoryList) {

            return $self->complete(
                $session, $standardCmd,
                'The character\'s inventory is empty (according to the Inventory task)',
            );
        }

        $count = 0;
        OUTER: foreach my $obj (@inventoryList) {

            # Check whether the object is on the protected object list
            if (
                ! @protectList
                || (
                    ! $session->worldModelObj->objCompare(
                        $session->inventoryTask->sensitivity,
                        $obj,
                        @protectList,
                    )
                )
            ) {
                # The object isn't protected, so we can drop it
                $session->sendModCmd('drop', 'object', $obj->noun);
                $count++;
            }
        }

        if (! $count) {

            return $self->complete($session, $standardCmd, 'No inventory objects dropped');

        } elsif ($count == 1) {

            return $self->complete($session, $standardCmd, 'Attempted to drop 1 inventory object');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Attempted to drop ' . $count . ' inventory objects',
            );
        }
    }
}

{ package Games::Axmud::Cmd::UseAll;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('useall', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['uall', 'useall'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Performs action on all unprotected objects';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $count,
            @inventoryList, @protectList,
        );

        # Check for improper arguments
        if (! @args) {

            return $self->improper($session, $inputString);
        }

        # Check there is an Inventory task running (without which, the current inventory is unknown)
        if (! $session->inventoryTask) {

            return $self->error(
                $session, $inputString,
                'Can\'t drop unprotected objects - no Inventory task running',
            );

        # Check that there is a current character
        } elsif (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t drop unprotected objects - no current character selected',
            );
        }

        # Import the current inventory
        @inventoryList = $session->inventoryTask->inventoryList;
        # Import the list of protected objects
        @protectList = $session->currentChar->protectObjList;

        if (! @inventoryList) {

            return $self->complete(
                $session, $standardCmd,
                'The character\'s inventory is empty (according to the Inventory task)',
            );
        }

        $count = 0;
        OUTER: foreach my $obj (@inventoryList) {

            my ($cmd, $noun);

            # Check whether the object is on the protected object list
            if (
                ! @protectList
                || (
                    ! $session->worldModelObj->objCompare(
                        $session->inventoryTask->sensitivity,
                        $obj,
                        @protectList,
                    )
                )
            ) {
                # The object isn't protected, so we can use it
                $count++;

                # Combine everything in @args into a single string
                $cmd = join (' ', @args);
                # Substitute the @ for the object to be used
                $noun = $obj->noun;
                $cmd =~ s/\@/$noun/g;

                # Send the command to the world (we can use ->relayCmd because it's definitely not a
                #   movement command, and we don't want to interpolate it)
                $session->relayCmd($cmd);
            }
        }

        if (! $count) {

            return $self->complete($session, $standardCmd, 'No inventory objects used');

        } elsif ($count == 1) {

            return $self->complete($session, $standardCmd, 'Attempted to use 1 inventory object');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Attempted to use ' . $count . ' inventory objects',
            );
        }
    }
}

# Locator task

{ package Games::Axmud::Cmd::MoveDirection;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('movedirection', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['move', 'movedir', 'movedirection'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends a movement command to the world';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $dir,
            $check,
        ) = @_;

        # Local variables
        my $cmd;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Special error message for ;go (as it would be easy to type by mistake)
        if (! defined $dir) {

            return $self->error($session, $inputString, 'Usage: \';godirection <direction\'');

        } else {

            # Prepare the command to send
            $cmd = $session->prepareCmd('go', 'direction', $dir);
            if (! $cmd) {

                # Failsafe: just use the direction as the command (e.g. 'north')
                $cmd = $dir;
            }

            # Send the world command
            if (! $session->moveCmd($dir)) {

                return $self->error(
                    $session, $inputString,
                    'Failed to send movement world command',
                );

            } else {

                return $self->complete($session, $standardCmd, 'Movement world command sent');
            }
        }
    }
}

{ package Games::Axmud::Cmd::RelayDirection;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('relaydirection', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rel', 'relay', 'relaydir', 'relaydirection'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sends a non-movement command to the world';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $dir,
            $check,
        ) = @_;

        # Local variables
        my $cmd;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Special error message for ;relaydirection (as ;godirection has one, and it would be easy
        #   to ;go type by mistake)
        if (! defined $dir) {

            return $self->error($session, $inputString, 'Usage: \';relaydirection <direction\'');

        } else {

            # Send the world command
            if (! $session->relayCmd($dir)) {

                return $self->error(
                    $session, $inputString,
                    'Failed to send non-movement world command',
                );

            } else {

                return $self->complete($session, $standardCmd, 'Non-movement world command sent');
            }
        }
    }
}

{ package Games::Axmud::Cmd::Teleport;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('teleport', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['tp', 'tport', 'teleport'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Teleports to another part of the world';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $roomName,
            $check,
        ) = @_;

        # Local variables
        my ($wmObj, $cmd, $roomNum, $roomObj, $msg);

        # Check for improper arguments
        if (! defined $roomName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the world model (for convenience)
        $wmObj = $session->worldModelObj;

        # The world model has a hash of rooms to which the user can teleport with a world command
        #   like 'goto <destination>'. This hash is in the form
        #       $teleportHash{tower} = 'goto /domains/town/room/start'
        #       $teleportHash{2} = 'goto /domains/town/room/start'
        #
        # In this client command, the destination is specified by the $roomName argument
        # If the destination ('tower' in the example above) exists as a room tag in the world model,
        #   or if it is the number of a world model room ('2' in the example above), the automapper/
        #   Locator task assume that this is the target destination. Otherwise, the target
        #   destination is unknown, and the automapper will get lost
        # If the destination ('tower' / '2' in the examples above) does NOT appear in the hash, the
        #   highest-priority command cage's standard command 'teleport' is used instead. Assuming
        #   that the standard command is in the form 'teleport room', the word 'room' is substituted
        #   for the destination specified by the $roomName argument. If it's a recognised room tag,
        #   the automapper/Locator task assume that this room is the target destination (world model
        #   room numbers can't be used in this case). Otherwise, the target destination is unknown,
        #   and the automapper will get lost

        # See if the world model's ->teleportHash contains a destination called <room>
        if ($wmObj->ivExists('teleportHash', $roomName)) {

            # Get the corresponding world command (e.g. 'goto /domains/town/room/start')
            $cmd = $wmObj->ivShow('teleportHash', $roomName);

            # If $roomName is a recognised room tag or world model room number, set the destination
            #   room
            if ($wmObj->ivExists('roomTagHash', lc($roomName))) {

                # Room tag
                $roomNum = $wmObj->ivShow('roomTagHash', lc($roomName));

            } elsif ($wmObj->ivExists('modelHash', $roomName)) {

                # World model room number
                $roomObj = $wmObj->ivShow('modelHash', $roomName);
                if ($roomObj->category eq 'room') {

                    $roomNum = $roomObj->number;
                }
            }

        } else {

            # Get the highest-priority command cage's standard command 'teleport'
            $cmd = $session->findCmd('teleport');
            if (! $cmd) {

                # No command cages available
                return $self->error(
                    $session, $inputString,
                    'Teleport failure: could not find corresponding world command',
                );
            }

            # The standard command should be in the general form 'teleport room'. Replace the
            #   world 'room' with the room tag, to get the corresponding world command
            $cmd =~ s/room/$roomName/;

            # If $roomName is a recognised room tag, set the destination room
            if ($wmObj->ivExists('roomTagHash', lc($roomName))) {

                $roomNum = $wmObj->ivShow('roomTagHash', lc($roomName));
            }
        }

        # Send the world command. If the destination room in the world model is known, $roomNum
        #   is set; otherwise it is 'undef'
        $msg = 'room \'' . $roomName . '\'';
        if ($roomNum) {

            $msg .= ' (#' . $roomNum . ')';
        }

        if (! $session->teleportCmd($cmd, $roomNum)) {

            return $self->error(
                $session, $inputString,
                'Teleport failure: could not teleport to ' . $msg,
            );

        } else {

            return $self->complete($session, $standardCmd, 'Teleported to ' . $msg);
        }
    }
}

{ package Games::Axmud::Cmd::AddTeleport;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addteleport', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['adt', 'addport', 'addteleport'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a teleport destination to the world model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $room, $cmd,
            $check,
        ) = @_;

        # Local variables
        my $wmObj;

        # Check for improper arguments
        if (! defined $room || ! defined $cmd || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the world model (for convenience)
        $wmObj = $session->worldModelObj;

        # Check that the teleport location doesn't already exist
        if ($wmObj->ivExists('teleportHash', $room)) {

            return $self->error(
                $session, $inputString,
                'The teleport location \'' . $room . '\' has already been added',
            );
        }

        # Add the teleport location
        if (! $wmObj->add_teleport($room, $cmd)) {

            return $self->error(
                $session, $inputString,
                'Couldn\'t add the teleport location \'' . $room . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Added the teleport location \'' . $room . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteTeleport;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteteleport', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dlt', 'delport', 'deleteteleport'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes an existing teleport location';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $room,
            $check,
        ) = @_;

        # Local variables
        my $wmObj;

        # Check for improper arguments
        if (! defined $room || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the world model (for convenience)
        $wmObj = $session->worldModelObj;

        # Check that the teleport location already exists
        if (! $wmObj->ivExists('teleportHash', $room)) {

            return $self->error(
                $session, $inputString,
                'The teleport location \'' . $room . '\' hasn\'t been added',
            );
        }

        # Delete the teleport location
        if (! $wmObj->del_teleport($room)) {

            return $self->error(
                $session, $inputString,
                'Couldn\'t delete the teleport location \'' . $room . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Deleted the teleport location \'' . $room . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListTeleport;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listteleport', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lpt', 'listport', 'listteleport'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists teleport locations';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            @list,
            %tpHash, %tagHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the teleport location hash and the room tag hash
        %tpHash = $session->worldModelObj->teleportHash;
        %tagHash = $session->worldModelObj->roomTagHash;
        # Get a sorted list of teleport locations
        @list = sort {lc($a) cmp lc($b)} (keys %tpHash);
        if (! @list) {

            return $self->complete($session, $standardCmd, 'The teleport location list is empty');
        }

        # Display header
        $session->writeText('List of teleport locations (* = name is a recognised room tag)');
        $session->writeText('   Room name                        Corresponding world command');

        # Display list
        foreach my $room (@list) {

            my ($cmd, $column);

            $cmd = $tpHash{$room};

            if (exists $tagHash{$room}) {
                $column = ' * ';
            } else {
                $column = '   ';
            }

            $session->writeText($column . sprintf('%-32.32s', $room) . ' ' . $cmd);
        }

        # Display footer
        if (@list == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 teleport location found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @list . ' teleport locations found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::InsertLook;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('insertlook', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ilk', 'ilook', 'insertlook'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Inserts fake \'look\' into the Locator movement list';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the Locator task is running
        if (! $session->locatorTask) {

            return $self->error($session, $inputString, 'The Locator task is not running');

        } elsif (! $session->locatorTask->insertLook()) {

            return $self->error(
                $session, $inputString,
                'Failed to insert a \'look\' command into the Locator task\'s movement list',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Inserted a \'look\' command into the Locator task\'s movement list',
            );
        }
    }
}

{ package Games::Axmud::Cmd::InsertFailedExit;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('insertfailedexit', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'ife', 'ifail', 'insertfail', 'insertfailexit', 'insertfailedexit',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Automatically adds a failed exit message';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my $mode;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Prepare arguments for the call to the Locator task
        if (! $switch || $switch eq '-r') {
            $mode = 'room';
        } elsif ($switch eq '-w') {
            $mode = 'world';
        } elsif ($switch eq '-u') {
            $mode = 'update';
        } else {

            return $self->error(
                $session, $inputString,
                'Unrecognised switch \'' . $switch . '\'',
            );
        }

        # Do same basic checks
        if (! $session->locatorTask) {

            return $self->error($session, $inputString, 'The Locator task is not running');

        } elsif (! $session->locatorTask->roomObj) {

            return $self->error(
                $session, $inputString,
                'The Locator task doesn\'t know your character\'s location',
            );

        } elsif ($mode eq 'room' && ! $session->mapObj->currentRoom) {

            return $self->error(
                $session, $inputString,
                'The automapper\'s current room is not set',
            );

        } elsif (! defined $session->locatorTask->prevCmdBufferNum) {

            return $self->error(
                $session, $inputString,
                'This command can only be used when the Locator task is expecting one room'
                . ' statement in response to a movement or a look/glance command',
            );
        }

        # Insert the failed exit message
        if (! $session->locatorTask->insertFailedExit($mode)) {

            if ($mode eq 'room') {

                return $self->error(
                    $session, $inputString,
                    'Could not insert a failed exit message into the current room\'s list of such'
                    . ' messages',
                );

            } elsif ($mode eq 'world') {

                return $self->error(
                    $session, $inputString,
                    'Could not insert a failed exit message into the world profile\'s list of such'
                    . ' messages',
                );

            } else {

                return $self->error(
                    $session, $inputString,
                    'Could not update the Locator task after a failed exit message',
                );
            }

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Inserted a failed exit message into the current room\'s list of such messages',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ResetLocatorTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resetlocatortask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rlc', 'resetloc', 'resetlocator', 'resetlocatortask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets the Locator task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $dir,
            $check,
        ) = @_;

        # Local variables
        my $standardDir;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the Locator task is running
        if (! $session->locatorTask) {

            return $self->error($session, $inputString, 'There is no Locator task running');
        }

        # If $dir was specified, it must be a standard or custom primary cardinal or intercardinal
        #   direction (i.e. n/ne/e/se/s/sw/w/nw - not up or down)
        if (defined $dir) {

            $standardDir = $session->currentDict->convertStandardDir($dir);
            if (
                ! defined $standardDir
                || ! $axmud::CLIENT->ivExists('constShortPrimaryDirHash', $standardDir)
                || $standardDir eq 'up'
                || $standardDir eq 'down'
            ) {
                return $self->error(
                    $session, $inputString,
                    'If you specify the direction the character is facing, it must be a cardinal'
                    . ' or intercardinal direction (i.e. n/ne/e/se/s/sw/w/nw)',
                );

            } else {

                # Update the automapper object's IV
                $session->mapObj->set_facingDir($standardDir);
            }
        }

        # Mark the task as needing to be reset
        $session->locatorTask->set_status('reset');
        $session->locatorTask->set_manualResetFlag(TRUE);

        # Tell the automapper object to mark the character as lost, if a current room is set
        if ($session->mapObj->currentRoom) {

            # (Let the automapper display a system message)
            return $session->mapObj->setCurrentRoom();

        } else {

            # (Display our own system message)
            return $self->complete($session, $standardCmd, 'Locator task told to reset');
        }
    }
}

{ package Games::Axmud::Cmd::SetFacing;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setfacing', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sfc', 'facing', 'setfacing'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the direction the character is facing';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $dir,
            $check,
        ) = @_;

        # Local variables
        my $standardDir;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;sfc
        if (! defined $dir) {

            $session->mapObj->set_facingDir();

            return $self->complete(
                $session, $standardCmd,
                'The character\'s facing directon has been reset',
            );

        # ;sfc <dir>
        } else {

            # $dir must be a standard or custom primary cardinal or intercardinal direction (i.e.
            #   n/ne/e/se/s/sw/w/nw - not up or down)
            $standardDir = $session->currentDict->convertStandardDir($dir);
            if (
                ! defined $standardDir
                || ! $axmud::CLIENT->ivExists('constShortPrimaryDirHash', $standardDir)
                || $standardDir eq 'up'
                || $standardDir eq 'down'
            ) {
                return $self->error(
                    $session, $inputString,
                    'If you specify the direction the character is facing, it must be a cardinal'
                    . ' or intercardinal direction (i.e. n/ne/e/se/s/sw/w/nw)',
                );

            } else {

                # Update the automapper object's IV
                $session->mapObj->set_facingDir($standardDir);

                return $self->complete(
                    $session, $standardCmd,
                    'The character\'s facing directon has been set as \'' . $standardDir . '\'',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::AddExitPattern;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addexitpattern', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['aep', 'addexitpattern'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds exit pattern to current world profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $pattern,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $switch || ! defined $pattern || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check $pattern is valid
        if ($axmud::CLIENT->regexCheck($pattern)) {

            return $self->error(
                $session, $inputString,
                'The pattern \'' . $pattern . '\' isn\'t a valid regular expression',
            );
        }

        # ;aep -d <pattern>
        if ($switch eq '-d') {

            # Check the pattern isn't already there
            if (defined $session->currentWorld->ivFind('doorPatternList', $pattern)) {

                return $self->error(
                    $session, $inputString,
                    'The current world profile\'s door pattern list already contains that pattern',
                );

            } else {

                $session->currentWorld->ivPush('doorPatternList', $pattern);

                return $self->complete(
                    $session, $standardCmd,
                    'Added \'' . $pattern . '\' to the current world profile\'s door pattern'
                    . ' list',
                );
            }

        # ;aep -l <pattern>
        } elsif ($switch eq '-l') {

            # Check the pattern isn't already there
            if (defined $session->currentWorld->ivFind('lockedPatternList', $pattern)) {

                return $self->error(
                    $session, $inputString,
                    'The current world profile\'s locked door pattern list already contains that'
                    . ' pattern',
                );

            } else {

                $session->currentWorld->ivPush('lockedPatternList', $pattern);

                return $self->complete(
                    $session, $standardCmd,
                    'Added \'' . $pattern . '\' to the current world profile\'s locked door'
                    . ' pattern list',
                );
            }

        # ;aep -f <pattern>
        } elsif ($switch eq '-f') {

            # Check the pattern isn't already there
            if (defined $session->currentWorld->ivFind('failExitPatternList', $pattern)) {

                return $self->error(
                    $session, $inputString,
                    'The current world profile\'s failed exit pattern list already contains that'
                    . ' pattern',
                );

            } else {

                $session->currentWorld->ivPush('failExitPatternList', $pattern);

                return $self->complete(
                    $session, $standardCmd,
                    'Added \'' . $pattern . '\' to the current world profile\'s failed exit pattern'
                    . ' list',
                );
            }

        # ;aep -r <pattern>
        } elsif ($switch eq '-r') {

            # Check the pattern isn't already there
            if (defined $session->currentWorld->ivFind('darkRoomPatternList', $pattern)) {

                return $self->error(
                    $session, $inputString,
                    'The current world profile\'s dark room pattern list already contains that'
                    . ' pattern',
                );

            } else {

                $session->currentWorld->ivPush('darkRoomPatternList', $pattern);

                return $self->complete(
                    $session, $standardCmd,
                    'Added \'' . $pattern . '\' to the current world profile\'s dark room pattern'
                    . ' list',
                );
            }
        # ;aep -u <pattern>
        } elsif ($switch eq '-u') {

            # Check the pattern isn't already there
            if (defined $session->currentWorld->ivFind('unspecifiedRoomPatternList', $pattern)) {

                return $self->error(
                    $session, $inputString,
                    'The current world profile\'s unspecified room pattern list already contains'
                    . ' that pattern',
                );

            } else {

                $session->currentWorld->ivPush('unspecifiedRoomPatternList', $pattern);

                return $self->complete(
                    $session, $standardCmd,
                    'Added \'' . $pattern . '\' to the current world profile\'s unspecified room'
                    . ' pattern list',
                );
            }

        # ;aep -i <pattern>
        } elsif ($switch eq '-i') {

            # Check the pattern isn't already there
            if (defined $session->currentWorld->ivFind('involuntaryExitPatternList', $pattern)) {

                return $self->error(
                    $session, $inputString,
                    'The current world\'s involuntary exit pattern list already contains that'
                    . ' pattern',
                );

            } else {

                $session->currentWorld->ivPush('involuntaryExitPatternList', $pattern);

                return $self->complete(
                    $session, $standardCmd,
                    'Added \'' . $pattern . '\' to the current world profile \'s involuntary exit'
                    . ' pattern list',
                );
            }

        } else {

            return $self->error($session, $inputString, 'Invalid switch \'' . $switch . '\'');
        }
    }
}

{ package Games::Axmud::Cmd::DeleteExitPattern;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteexitpattern', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dep', 'delexitpattern', 'deleteexitpattern'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes exit pattern from current world profile';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $number,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $switch || ! defined $number || defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;dep -d <pattern>
        if ($switch eq '-d') {

            # Check the pattern exists
            if (! defined $session->currentWorld->ivIndex('doorPatternList', ($number - 1))) {

                return $self->error(
                    $session, $inputString,
                    'The current world profile\'s door pattern list doesn\'t contain that'
                    . ' pattern',
                );

            } else {

                $session->currentWorld->ivSplice('doorPatternList', ($number - 1));

                return $self->complete(
                    $session, $standardCmd,
                    'Deleted pattern #' . $number . ' from the current world profile\'s door'
                    . ' pattern list',
                );
            }

        # ;dep -l <pattern>
        } elsif ($switch eq '-l') {

            # Check the pattern exists
            if (! defined $session->currentWorld->ivIndex('lockedPatternList', ($number - 1))) {

                return $self->error(
                    $session, $inputString,
                    'The current world profile\'s locked door pattern list doesn\'t contain that'
                    . ' pattern',
                );

            } else {

                $session->currentWorld->ivSplice('lockedPatternList', ($number - 1));

                return $self->complete(
                    $session, $standardCmd,
                    'Deleted pattern #' . $number . ' from the current world profile\'s locked door'
                    . ' pattern list',
                );
            }

        # ;dep -f <pattern>
        } elsif ($switch eq '-f') {

            # Check the pattern exists
            if (! defined $session->currentWorld->ivIndex('failExitPatternList', ($number - 1))) {

                return $self->error(
                    $session, $inputString,
                    'The current world profile\'s failed exit pattern list doesn\'t contain that'
                    . ' pattern',
                );

            } else {

                $session->currentWorld->ivSplice('failExitPatternList', ($number - 1));

                return $self->complete(
                    $session, $standardCmd,
                    'Deleted pattern #' . $number . ' from the current world profile\'s failed'
                    . ' exit pattern list',
                );
            }

        # ;dep -r <pattern>
        } elsif ($switch eq '-r') {

            # Check the pattern exists
            if (! defined $session->currentWorld->ivIndex('darkRoomPatternList', ($number - 1))) {

                return $self->error(
                    $session, $inputString,
                    'The current world profile\'s dark room pattern list doesn\'t contain that'
                    . ' pattern',
                );

            } else {

                $session->currentWorld->ivSplice('darkRoomPatternList', ($number - 1));

                return $self->complete(
                    $session, $standardCmd,
                    'Deleted pattern #' . $number . ' from the current world profile\'s dark room'
                    . ' pattern list',
                );
            }

        # ;dep -u <pattern>
        } elsif ($switch eq '-u') {

            # Check the pattern exists
            if (
                ! defined $session->currentWorld->ivIndex(
                    'unspecifiedRoomPatternList',
                    ($number - 1),
                )
            ) {
                return $self->error(
                    $session, $inputString,
                    'The current world profile\'s unspecified room pattern list doesn\'t contain'
                    . ' that pattern',
                );

            } else {

                $session->currentWorld->ivSplice('unspecifiedRoomPatternList', ($number - 1));

                return $self->complete(
                    $session, $standardCmd,
                    'Deleted pattern #' . $number . ' from the current world profile\'s unspecified'
                    . ' room pattern list',
                );
            }

        # ;dep -i <pattern>
        } elsif ($switch eq '-i') {

            # Check the pattern exists
            if (
                ! defined $session->currentWorld->ivIndex(
                    'involuntaryExitPatternList',
                    ($number - 1),
                )
            ) {
                return $self->error(
                    $session, $inputString,
                    'The current world profile\'s involuntary exit pattern list doesn\'t contain'
                    . ' that pattern',
                );

            } else {

                $session->currentWorld->ivSplice('involuntaryExitPatternList', ($number - 1));

                return $self->complete(
                    $session, $standardCmd,
                    'Deleted pattern #' . $number . ' from the current world profile\'s'
                    . ' involuntary exit pattern list',
                );
            }

        } else {

            return $self->error($session, $inputString, 'Invalid switch \'' . $switch . '\'');
        }
    }
}

{ package Games::Axmud::Cmd::ListExitPattern;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listexitpattern', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lep', 'listexitpattern'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists the world profile\'s exit patterns';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $allPatternFlag, $doorPatternFlag, $lockedPatternFlag, $failedPatternFlag,
            $darkPatternFlag, $unspecifiedPatternFlag, $involuntaryPatternFlag, $count,
            @list,
        );

        # Extract switches
        ($switch, @args) = $self->extract('-a', 0, @args);
        if (defined $switch) {

            $allPatternFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-d', 0, @args);
        if (defined $switch) {

            $doorPatternFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-l', 0, @args);
        if (defined $switch) {

            $lockedPatternFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-f', 0, @args);
        if (defined $switch) {

            $failedPatternFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-r', 0, @args);
        if (defined $switch) {

            $darkPatternFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-u', 0, @args);
        if (defined $switch) {

            $unspecifiedPatternFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-i', 0, @args);
        if (defined $switch) {

            $involuntaryPatternFlag = TRUE;
        }

        # There should be nothing left in @args
        if (@args) {

            return $self->improper($session, $inputString);
        }

        $count = 0;
        if ($doorPatternFlag || $allPatternFlag) {

            @list = $session->currentWorld->doorPatternList;
            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'The world profile door pattern list is empty',
                );
            }

            # Display header
            $session->writeText(
                'List of world profile door patterns (' . scalar @list . ' items)',
            );

            # Display list
            foreach my $pattern (@list) {

                $session->writeText(sprintf('   %-4.4s %-64.64s', $count, $pattern));
            }
        }

        if ($lockedPatternFlag || $allPatternFlag) {

            @list = $session->currentWorld->lockedPatternList;
            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'The world profile locked door pattern list is empty',
                );
            }

            # Display header
            $session->writeText(
                'List of world profile locked door patterns (' . scalar @list . ' items)',
            );

            # Display list
            foreach my $pattern (@list) {

                $session->writeText(sprintf('   %-4.4s %-64.64s', $count, $pattern));
            }
        }

        if ($failedPatternFlag || $allPatternFlag) {

            @list = $session->currentWorld->failExitPatternList;
            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'The world profile failed exit pattern list is empty',
                );
            }

            # Display header
            $session->writeText(
                'List of world profile failed exit patterns (' . scalar @list . ' items)',
            );

            # Display list
            foreach my $pattern (@list) {

                $session->writeText(sprintf('   %-4.4s %-64.64s', $count, $pattern));
            }
        }

        if ($darkPatternFlag || $allPatternFlag) {

            @list = $session->currentWorld->darkRoomPatternList;
            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'The world profile dark room pattern list is empty',
                );
            }

            # Display header
            $session->writeText(
                'List of world profile darm room patterns (' . scalar @list . ' items)',
            );

            # Display list
            foreach my $pattern (@list) {

                $session->writeText(sprintf('   %-4.4s %-64.64s', $count, $pattern));
            }
        }

        if ($unspecifiedPatternFlag || $allPatternFlag) {

            @list = $session->currentWorld->unspecifiedRoomPatternList;
            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'The world profile unspecified room pattern list is empty',
                );
            }

            # Display header
            $session->writeText(
                'List of world profile unspecified room patterns (' . scalar @list . ' items)',
            );

            # Display list
            foreach my $pattern (@list) {

                $session->writeText(sprintf('   %-4.4s %-64.64s', $count, $pattern));
            }
        }

        if ($involuntaryPatternFlag || $allPatternFlag) {

            @list = $session->currentWorld->involuntaryExitPatternList;
            if (! @list) {

                return $self->complete(
                    $session, $standardCmd,
                    'The world profile involuntary exit pattern list is empty',
                );
            }

            # Display header
            $session->writeText(
                'List of world profile involuntary exit patterns (' . scalar @list . ' items)',
            );

            # Display list
            foreach my $pattern (@list) {

                $session->writeText(sprintf('   %-4.4s %-64.64s', $count, $pattern));
            }
        }

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of lists (1 pattern found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of lists (' . scalar @list . ' patterns found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::CollectUnknownWords;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('collectunknownwords', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'cuw',
            'collectuw',
            'collectunknown',
            'collectunknownwords',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles collection of unrecognised words';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if ($session->currentWorld->collectUnknownWordFlag) {

            # Turn collection off
            $session->currentWorld->ivPoke('collectUnknownWordFlag', FALSE);

            return $self->complete(
                $session, $standardCmd,
                'Unrecognised word collection turned off',
            );

        } else {

            # Turn collection on
            $session->currentWorld->ivPoke('collectUnknownWordFlag', TRUE);

            return $self->complete(
                $session, $standardCmd,
                'Unknown word collection turned on',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EmptyUnknownWords;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('emptyunknownwords', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} =  ['euw', 'emptyuw', 'emptyunknown', 'emptyunknownwords'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Empties the collection of unrecognised words';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Empty the hash
        $session->currentDict->ivEmpty('unknownWordHash');

        return $self->complete(
            $session, $standardCmd,
            'The current dictionary\'s collection of unrecognised words has been emptied',
        );
    }
}

{ package Games::Axmud::Cmd::ListUnknownWords;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listunknownwords', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} =  ['luw', 'listuw', 'listunknown', 'listunknownwords'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists the collection of unrecognised words';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $text,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the collection of unknown words, and sort alphabetically
        @list = sort {lc($a) cmp lc($b)} ($session->currentDict->ivKeys('unknownWordHash'));
        if (! @list) {

            return $self->complete(
                $session, $standardCmd,
                'The current dictionary\'s collection of unrecognised words is empty',
            );
        }

        # Display header
        $session->writeText('List of current dictionary\'s unrecognised words');

        # Display list
        foreach my $word (@list) {

            if (! $text) {
                $text = $word;
            } else {
                $text .= ', ' . $word;
            }
        }

        $session->writeText($text);

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 word found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . @list . ' words found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::CollectContentsLines;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('collectcontentslines', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = [
            'ccl',
            'collectcl',
            'collectcontents',
            'collectcontentslines',
        ];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Toggles collection of contents lines';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if ($session->currentWorld->collectContentsFlag) {

            # Turn collection off
            $session->currentWorld->ivPoke('collectContentsFlag', FALSE);

            return $self->complete(
                $session, $standardCmd,
                'Contents lines collection turned off',
            );

        } else {

            # Turn collection on
            $session->currentWorld->ivPoke('collectContentsFlag', TRUE);

            return $self->complete(
                $session, $standardCmd,
                'Contents lines collection turned on',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EmptyContentsLines;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('emptycontentslines', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} =  ['ecl', 'emptycl', 'emptycontents', 'emptycontentslines'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Empties the collection of contents lines';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Empty the hash
        $session->currentDict->ivEmpty('contentsLinesHash');

        return $self->complete(
            $session, $standardCmd,
            'The current dictionary\'s collection of contents lines has been emptied',
        );
    }
}

{ package Games::Axmud::Cmd::ListContentsLines;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listcontentslines', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} =  ['lcl', 'listcl', 'listcontents', 'listcontentslines'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists the collection of contentslines';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the collection of contents lines, and sort alphabetically
        @list = sort {lc($a) cmp lc($b)} ($session->currentDict->ivKeys('contentsLinesHash'));
        if (! @list) {

            return $self->complete(
                $session, $standardCmd,
                'The current dictionary\'s collection of contents lines is empty',
            );
        }

        # Display header
        $session->writeText('List of current dictionary\'s contents lines');

        # Display list
        foreach my $text (@list) {

            $session->writeText('   ' . $text);
        }

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 contents line found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . @list . ' contents lines found)',
            );
        }
    }
}

# Status task

{ package Games::Axmud::Cmd::ActivateStatusTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('activatestatustask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ast', 'activatestatustask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Activates the Status task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that it's possible to activate the Status task
        if (! $session->statusTask) {

            return $self->error($session, $inputString, 'The Status task isn\'t running');

        } elsif (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'The Status task needs a current character profile to run in \'active\' mode',
            );

        } elsif ($session->statusTask->activeFlag) {

            return $self->complete(
                $session, $standardCmd,
                'The Status task is already running in \'active\' mode',
            );
        }

        # Activate the Status task
        if (! $session->statusTask->activate()) {

            return $self->error($session, $inputString, 'Error while activating the Status task');

        } else {

            # Store the task's ->activeFlag value in the character profile (so that the Status task
            #   starts in the same mode it was in at the end of the last session)
            $session->currentChar->ivPoke('statusCmdFlag', TRUE);

            return $self->complete($session, $standardCmd, 'Status task now in \'active\' mode');
        }
    }
}

{ package Games::Axmud::Cmd::DisactivateStatusTask;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('disactivatestatustask', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dst', 'disactivatestatustask'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Disactivates the Status task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that it's possible to disactivate the Status task
        if (! $session->statusTask) {

            return $self->error($session, $inputString, 'The Status task isn\'t running');

        } elsif (! $session->statusTask->activeFlag) {

        } elsif (! $session->statusTask->activeFlag) {

            return $self->complete(
                $session, $standardCmd,
                'The Status task is already running in \'disactived\' mode',
            );
        }

        # Disactivate the Status task
        if (! $session->statusTask->disactivate()) {

            return $self->error(
                $session, $inputString,
                'Error while disactivating the Status task',
            );

        } else {

            # Store the task's ->activeFlag value in the character profile (so that the Status task
            #   starts in the same mode it was in at the end of the last session)
            $session->currentChar->ivPoke('statusCmdFlag', TRUE);

            return $self->complete(
                $session, $standardCmd,
                'Status task now in \'disactivated\' mode',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ResetCounter;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resetcounter', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rsc', 'resetcounter'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets the Status task\'s counters';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args
        ) = @_;

        # Local variables
        my (
            @switchList, @foundList,
            $count, $foundSwitch, $allSwitch, $result,
        );

        # Try to extract all switches but -a
        @switchList = qw(f i x q m p b);        # Contains all switches but -a
        $count = 0;
        foreach my $switch (@switchList) {

            ($foundSwitch, @args) = $self->extract($switch, 0, @args);
            if (defined $foundSwitch) {

                $count++;
                push (@foundList, $switch);
            }
        }

        # Try to extract the switch -a
        ($allSwitch, @args) = $self->extract('a', 0, @args);
        if (defined $allSwitch) {

            $count = 7;
            @foundList = ('a');
        }

        # @args should now empty
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # If @foundList is empty, it's the same as using the switch -a
        if (! @foundList) {

            $count = 7;
            @foundList = ('a');
        }

        # Check the Status task is running
        if (! $session->statusTask) {

            return $self->error(
                $session, $inputString,
                'Can\'t reset counters because the Status task isn\'t running',
            );
        }

        # Reset the counter(s)
        if (! $session->statusTask->reset_counters(@foundList)) {

            return $self->error(
                $session, $inputString,
                'General error while resetting Status task counters',
            );

        } elsif ($count == 7) {
            return $self->complete($session, $standardCmd, 'All Status task counters reset');
        } elsif ($count == 1) {
            return $self->complete($session, $standardCmd, '1 Status task counter reset');
        } else {
            return $self->complete($session, $standardCmd, $count . ' Status task counters reset');
        }
    }
}

{ package Games::Axmud::Cmd::AddStatusCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addstatuscommand', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['asc', 'addstatuscommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a command to those sent by Status task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $cmd, $interval,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $cmd || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the Status task is running
        if (! $session->statusTask) {

            return $self->error($session, $inputString, 'The Status task isn\'t running');

        # Check it is in 'disactivated' mode
        } elsif ($session->statusTask->activeFlag) {

            return $self->error(
                $session, $inputString,
                'The Status task must be in \'disactivated\' mode before using this command',
            );
        }

        # Use default interval, if necessary
        if (! defined $interval) {

            $interval = 30;

        # Otherwise make sure $interval is a positive integer
        } elsif (! $axmud::CLIENT->intCheck($interval, 1)) {

            return $self->error($session, $inputString, '<interval> must be a positive integer');
        }

        # Add the command to both the Status task's list, and the current character profile's list
        #   (very unlikely that there is no current character, but we'll check anyway)
        $session->statusTask->ivAdd('cmdHash', $cmd, $interval);
        if ($session->currentChar) {

            $session->currentChar->ivAdd('statusCmdHash', $cmd, $interval);
        }

        return $self->complete(
            $session, $standardCmd,
            'Status command \'' . $cmd . '\' added at interval of \'' . $interval
            . '\' seconds',
        );
    }
}

{ package Games::Axmud::Cmd::DeleteStatusCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletestatuscommand', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dsc', 'delstatuscommand', 'deletestatuscommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a command from those sent by Status task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $cmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $cmd || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the Status task is running
        if (! $session->statusTask) {

            return $self->error($session, $inputString, 'The Status task isn\'t running');

        # Check it is in 'disactivated' mode
        } elsif ($session->statusTask->activeFlag) {

            return $self->error(
                $session, $inputString,
                'The Status task must be in \'disactivated\' mode before using this command',
            );

        # Check the command exists
        } elsif (! $session->statusTask->ivExists('cmdHash', $cmd)) {

            return $self->error(
                $session, $inputString,
                'The Status task isn\'t currently sending the command \'' . $cmd . '\'',
            );
        }

        # Delete the command from both the Status task's list, and the current character
        #   profile's list (very unlikely that there is no current character, but we'll check
        #   anyway)
        $session->statusTask->ivDelete('cmdHash', $cmd);
        if ($session->currentChar) {

            $session->currentChar->ivDelete('statusCmdHash', $cmd);
        }

        return $self->complete(
            $session, $standardCmd,
            'Status command \'' . $cmd . '\' deleted',
        );
    }
}

{ package Games::Axmud::Cmd::ListStatusCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('liststatuscommand', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lsc', 'liststatuscommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists commands sent to world by Status task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $text,
            @list,
            %hash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the Status task is running
        if (! $session->statusTask) {

            return $self->error($session, $inputString, 'The Status task isn\'t running');
        }

        %hash = $session->statusTask->cmdHash;
        @list = sort {lc($a) cmp lc($b)} (keys %hash);
        if (! @list) {

            return $self->complete(
                $session, $standardCmd,
                'The Status task\'s command list is empty',
            );
        }

        # Display header
        if ($session->statusTask->activeFlag) {
            $text = '\'active\'';
        } else {
            $text = '\'passive\'';
        }

        $session->writeText(
            'List of commands automatically sent by Status task (currently in ' . $text . ' mode)',
        );
        $session->writeText('   Command                          Interval (secs)');

        # Display list
        foreach my $cmd (@list) {

            $session->writeText('   ' .  sprintf('%-32.32s %-16.16s', $cmd, $hash{$cmd}));
        }

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 command found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . @list . ' commands found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetWimpy;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setwimpy', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['wmp', 'wimpy', 'setwimpy'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets local/remote wimpy level';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($switch, $number, $worldObj, $charObj);

        # Extract the switch
        ($switch, @args) = $self->extract('-r', 0, @args);

        # Should be no more than one argument left
        if (($switch && ! @args) || @args > 1) {

            return $self->improper($session, $inputString);

        } else {

            $number = shift @args;
        }

        # Check there is a current character; without it, we don't know the wimpy setting
        if (! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'Can\'t show/set wimpy - no current character profile',
            );

        } else {

            $worldObj = $session->currentWorld;
            $charObj = $session->currentChar;
        }

        # ;wmp
        if (! defined $number) {

            $session->writeText(
                'Local wimpy  : ' . $charObj->localWimpy . '/' . $charObj->constLocalWimpyMax,
            );

            $session->writeText(
                'Remote wimpy : ' . $charObj->remoteWimpy . '/' . $charObj->remoteWimpyMax,
            );

            return $self->complete($session, $standardCmd, 'Wimpy settings displayed');

        # ;wmp <number>
        } elsif (! $switch) {

            # Check that <number> is valid
            if ($number =~ /\D/ || $number < 0 || $number > $charObj->constLocalWimpyMax) {

                return $self->error(
                    $session, $inputString,
                    'Invalid wimpy level \'' . $number . '\' - must be 0-'
                    . $charObj->constLocalWimpyMax,
                );

            } else {

                $charObj->ivPoke('localWimpy', $number);
                if ($number == 0) {
                    return $self->complete($session, $standardCmd, 'Local wimpy set to 0 (brave)');
                } else {
                    return $self->complete($session, $standardCmd, 'Local wimpy set to ' . $number);
                }
            }

        # ;wmp -r <number>
        } else {

            # Check that <number> is valid
            if ($number =~ /\D/ || $number < 0) {

                return $self->error(
                    $session, $inputString,
                    'Invalid wimpy level \'' . $number . '\' - must be 0-'
                    . $charObj->remoteWimpyMax,
                );

            } else {

                # Set the wimpy level remotely
                $session->sendCmd('wimpy', 'number', $number);
                $charObj->ivPoke('remoteWimpy', $number);

                if ($number == 0) {

                    return $self->complete($session, $standardCmd, 'Remote wimpy set to 0 (brave)');

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'Remote wimpy set to ' . $number,
                    );
                }
            }
        }
    }
}

{ package Games::Axmud::Cmd::SetLife;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setlife', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['slf', 'setlife'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the Status task\'s current life status';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $status,
            $check,
        ) = @_;

        # Local variables
        my $result;

        # Check for improper arguments
        if (! defined $status || defined $check) {

            return $self->improper($session, $inputString);
        }

        if (! $session->statusTask || ! $session->currentChar) {

            return $self->error(
                $session, $inputString,
                'This command can only be used when the Status task is running and when there\'s'
                . ' a current character profile',
            );
        }

        if ($status eq '-a' || $status eq 'alive') {
            $session->currentChar->ivPoke('lifeStatus', 'alive');
        } elsif ($status eq '-s' || $status eq 'sleep') {
            $session->currentChar->ivPoke('lifeStatus', 'sleep');
        } elsif ($status eq '-p' || $status eq 'passout') {
            $session->currentChar->ivPoke('lifeStatus', 'passout');
        } elsif ($status eq '-d' || $status eq 'dead') {
            $session->currentChar->ivPoke('lifeStatus', 'dead');
        } else {

            return $self->error(
                $session, $inputString,
                'Invalid life status (try -a -s -p -d / alive sleep passout dead)',
            );
        }

        # Tell the Status task that its task window must be updated with a new background colour
        $session->statusTask->set_lifeStatusChangeFlag();

        # Operation complete
        return $self->complete(
            $session, $standardCmd,
            'Current character\'s life status set to \'' . $session->currentChar->lifeStatus
            . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::SetStatusEvent;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setstatusevent', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sev', 'setevent', 'setstatusevent'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Handles settings for Status task events';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($switch, $beforeCount, $beforeFlag, $afterCount, $afterFlag,);

        # Extract the switches
        ($switch, $beforeCount, @args) = $self->extract('-b', 1, @args);
        if (defined $switch) {

            $beforeFlag = TRUE;
        }

        ($switch, $afterCount, @args) = $self->extract('-a', 1, @args);
        if (defined $switch) {

            $afterFlag = TRUE;
        }

        # Should be no more arguments left
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # ;sse
        if (! $beforeFlag && ! $afterFlag) {

            # Display header
            $session->writeText('Logfile lines to write after a Status task event');

            # Display list
            $session->writeText(
                '   Write lines before event: ' . $axmud::CLIENT->statusEventBeforeCount,
            );


            $session->writeText(
                '   Write lines after event : ' . $axmud::CLIENT->statusEventAfterCount,
            );

            # Display footer
            return $self->complete($session, $standardCmd, 'End of list');

        # ;sse <switches>
        } else {

            # Check values, before setting either
            if (
                ($beforeFlag && ! $axmud::CLIENT->intCheck($beforeCount, 0))
                || ($afterFlag && ! $axmud::CLIENT->intCheck($afterCount, 0))
            ) {
                return $self->error(
                    $session, $inputString,
                    'Invalid value(s) for number of lines to write (must be an integer, 0 or'
                    . ' above)',
                );
            }

            if ($beforeFlag) {

                $axmud::CLIENT->set_statusEvent('before', $beforeCount);
            }

            if ($afterFlag) {

                $axmud::CLIENT->set_statusEvent('after', $afterCount);
            }

            return $self->complete(
                $session, $standardCmd,
                'Lines to write before/after Status task events: '
                . $axmud::CLIENT->statusEventBeforeCount . '/'
                . $axmud::CLIENT->statusEventAfterCount,
            );
        }
    }
}

{ package Games::Axmud::Cmd::ShowStatusGauge;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('showstatusgauge', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ssg', 'showstatusgauge'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Enables/disables the Status task\'s gauges';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;


        # Check for improper arguments
        if ((defined $switch && $switch ne '-l') || defined $check) {

            return $self->improper($session, $inputString);
        }

        if (! $session->statusTask) {

            return $self->error(
                $session, $inputString,
                'This command can only be used when the Status task is running',
            );
        }

        # ;ssg
        if (! $switch) {

            if (! $session->statusTask->gaugeFlag) {

                $session->statusTask->set_gaugeFlag(TRUE);

                return $self->complete(
                    $session, $standardCmd,
                    'The current Status task will now display gauges in the \'main\' window',
                );

            } else {

                $session->statusTask->set_gaugeFlag(FALSE);

                return $self->complete(
                    $session, $standardCmd,
                    'The current Status task will no longer display gauges in the \'main\' window',
                );
            }

        # ;ssg -l
        } else {

            if (! $session->statusTask->gaugeValueFlag) {

                $session->statusTask->set_gaugeValueFlag(TRUE);

                return $self->complete(
                    $session, $standardCmd,
                    'The current Status task will now display gauge labels',
                );

            } else {

                $session->statusTask->set_gaugeValueFlag(FALSE);

                return $self->complete(
                    $session, $standardCmd,
                    'The current Status task will no longer display gauge labels',
                );
            }
        }
    }
}

# System task

{ package Games::Axmud::Cmd::SetSystemMode;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setsystemmode', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ssm', 'systemmode', 'setsystemmode'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Starts or configures the System task';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $textFlag, $errorFlag, $warningFlag, $debugFlag, $improperFlag, $allFlag,
            $msgCount, $winCount, $taskObj, $mode, $startFlag, $string, $string2,
            @list,
        );

        # Extract switches
        $msgCount = 0;

        ($switch, @args) = $self->extract('-s', 0, @args);
        if (defined $switch) {

            $textFlag = TRUE;
            $msgCount++;
        }

        ($switch, @args) = $self->extract('-e', 0, @args);
        if (defined $switch) {

            $errorFlag = TRUE;
            $msgCount++;
        }

        ($switch, @args) = $self->extract('-w', 0, @args);
        if (defined $switch) {

            $warningFlag = TRUE;
            $msgCount++;
        }

        ($switch, @args) = $self->extract('-d', 0, @args);
        if (defined $switch) {

            $debugFlag = TRUE;
            $msgCount++;
        }

        ($switch, @args) = $self->extract('-i', 0, @args);
        if (defined $switch) {

            $improperFlag = TRUE;
            $msgCount++;
        }

        ($switch, @args) = $self->extract('-a', 0, @args);
        if (defined $switch) {

            $allFlag = TRUE;
            $msgCount++;
        }

        $winCount = 0;

        ($switch, @args) = $self->extract('-m', 0, @args);
        if (defined $switch) {

            $mode = 'original';
            $winCount++;
        }

        ($switch, @args) = $self->extract('-b', 0, @args);
        if (defined $switch) {

            $mode = 'both';
            $winCount++;
        }

        ($switch, @args) = $self->extract('-t', 0, @args);
        if (defined $switch) {

            $mode = 'task';
            $winCount++;
        }

        # There should be nothing left in @args
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # Some switches can't be combined
        if ($winCount > 1) {

            return $self->error(
                $session, $inputString,
                'The switches -m, -t and -b can\'t be combined',
            );
        }

        # If the user didn't specify the switches -s, -e, -w, -d or -i, and if the task is already
        #   running, then we display a list of the System task's current settings
        $taskObj = $session->systemTask;
        if ($taskObj && ! $msgCount) {

            # Display header
            $session->writeText('Current System task modes');

            # Display list
            @list = ('system', 'error', 'warning', 'debug', 'improper');
            foreach my $type (@list) {

                my $iv = $type . 'Mode';        # e.g. ->errorMode

                if ($taskObj->$iv == 1) {

                    $session->writeText(
                        sprintf('   %-8.8s', $type) . ' : displayed in \'main\'/task windows',
                    );

                } elsif ($taskObj->$iv == 2) {

                    $session->writeText(
                        sprintf('   %-8.8s', $type) . ' : displayed in task window only',
                    );

                } else {

                    # Mode 0
                    $session->writeText(
                        sprintf('   %-8.8s', $type) . ' : displayed in \'main\' window only',
                    );
                }
            }

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'Current System task modes displayed',
            );
        }

        # Now we can check for missing switches
        if (! $winCount) {

            return $self->error(
                $session, $inputString,
                'You must specify one of the switches -m, -t and -b',
            );
        }

        # If the System task isn't already running, start it
        if (! $taskObj) {

            $taskObj = Games::Axmud::Task::System->new($session, 'current');
            if (! $taskObj) {

                return $self->error(
                    $session, $inputString,
                    'General error creating a new System task',
                );

            } else {

                # Task started
                $startFlag = TRUE;
            }
        }

        # Update the task's IVs
        if ($textFlag || $allFlag) {

            $taskObj->set_mode('system', $mode);
            $string .= 'system',
        }

        if ($errorFlag || $allFlag) {

            $taskObj->set_mode('error', $mode);
            if ($string) {
                $string .= ' error',
            } else {
                $string = 'error',
            }
        }

        if ($warningFlag || $allFlag) {

            $taskObj->set_mode('warning', $mode);
            if ($string) {
                $string .= ' warning',
            } else {
                $string = 'warning',
            }
        }

        if ($debugFlag || $allFlag) {

            $taskObj->set_mode('debug', $mode);
            if ($string) {
                $string .= ' debug',
            } else {
                $string = 'debug',
            }
        }

        if ($improperFlag || $allFlag) {

            $taskObj->set_mode('improper', $mode);
            if ($string) {
                $string .= ' improper',
            } else {
                $string = 'improper',
            }
        }

        if (! $msgCount) {

            $string = '(none)';
        }

        if (defined $mode) {

            $string .= ', mode set: ';

            if ($mode eq 'both') {
                $string = 'use \'main\' and task windows';
            } elsif ($mode eq 'task') {
                $string = 'use task window only';
            } else {
                $string = 'use \'main\' window only';       # Mode 'original'
            }
        }

        $string .= ')';

        return $self->complete(
            $session, $standardCmd,
            'Current System task updated (messages affected: ' . $string,
        );
    }
}

# Watch task

{ package Games::Axmud::Cmd::EmptyWatchWindow;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('emptywatchwindow', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['eww', 'emptywatch', 'emptywatchwindow'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Empties the Watch task window';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the Watch task is running
        if (! $session->watchTask) {

            return $self->error(
                $session, $inputString,
                'The Watch task is not currently running',
            );

        } else {

            # Reset the task's window
            $session->watchTask->resetWin();

            return $self->complete(
                $session, $standardCmd,
                'The Watch task\'s window has been reset',
            );
        }
    }
}

# World model commands

{ package Games::Axmud::Cmd::AddRegion;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addregion', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['arn', 'addregion'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a region to the world model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($switch, $tempFlag, $name, $parent, $parentObj, $parentNum, $obj);

        # Extract the optional switch
        ($switch, @args) = $self->extract('-t', 0, @args);
        if (defined $switch) {

            $tempFlag = TRUE;
        }

        # Extract the remaining arguments
        $name = shift @args;
        $parent = shift @args;
        # There should be nothing left in @args
        if (! defined $name || @args) {

            return $self->improper($session, $inputString);
        }

        # If $name is longer than 32 characters, it must be shortened
        if (length $name > 32) {

            $name = substr($name, 0, 32);
        }

        # Check the new region doesn't already exist
        foreach my $regionObj ($session->worldModelObj->ivValues('regionModelHash')) {

            if ($regionObj->name eq $name) {

                # Region called $name already exists
                return $self->error(
                    $session, $inputString,
                    'There is already a region called \'' . $name . '\' in the world model',
                );
            }
        }

        # If a parent is specified, check it's a valid region name
        if (defined $parent) {

            OUTER: foreach my $regionObj ($session->worldModelObj->ivValues('regionModelHash')) {

                if ($regionObj->name eq $parent) {

                    $parentObj = $regionObj;
                    last OUTER;
                }
            }

            if (! $parentObj) {

                return $self->error(
                    $session, $inputString,
                    'There is no region called \'' . $parent . '\' in the world model',
                );

            } else {

                $parentNum = $parentObj->number;
            }
        }

        # Create the region model object
        $obj = $session->worldModelObj->addRegion(
            $session,
            TRUE,           # Update Automapper windows
            $name,
            $parentNum,     # May be 'undef'
            $tempFlag,
        );

        # Operation complete
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Could not add region object \'' . $name . '\'',
            );

        } elsif ($parentObj) {

            return $self->complete(
                $session, $standardCmd,
                'Added region #' . $obj->number . ' \'' . $name . '\' (parent: \''
                . $parent . '\' #' . $parentObj->number . ')',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Added region #' . $obj->number . ' \'' . $name . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddRoom;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addroom', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['arm', 'addroom'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a room to the world model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $region, $xPos, $yPos, $zPos,
            $check,
        ) = @_;

        # Local variables
        my ($matchFlag, $obj);

        # Check for improper arguments
        if (
            ! defined $region || ! defined $xPos || ! defined $yPos || ! defined $zPos
            || defined $check
        ) {
            return $self->improper($session, $inputString);
        }

        # Check the new region exists
        OUTER: foreach my $thisObj ($session->worldModelObj->ivValues('regionModelHash')) {

            if ($thisObj->name eq $region) {

                $matchFlag = TRUE;
                last OUTER;
            }
        }

        if (! $matchFlag) {

            return $self->error(
                $session, $inputString,
                'There is no region called \'' . $region . '\' in the world model',
            );
        }

        # Create the room model object
        $obj = $session->worldModelObj->addRoom(
            $session,
            TRUE,           # Update Automapper windows
            $region,
            $xPos,
            $yPos,
            $zPos,
        );

        # Operation complete
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Could not add room object',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Added room #' . $obj->number . ' in the region \'' . $region . '\' at x=' . $xPos
                . ', y=' . $yPos . ', z=' . $zPos,
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddModelObject;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addmodelobject', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['amo', 'addmo', 'addmodel', 'addmodelobject'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds an object to the world model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch, $name, $parent,
            $check,
        ) = @_;

        # Local variables
        my (
            $obj, $parentObj,
            %hash,
        );

        # Check for improper arguments
        if (! defined $switch || ! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # If $name is longer than 32 characters, it must be shortened
        if (length $name > 32) {

            $name = substr($name, 0, 32);
        }

        # If a parent is specified, check it's a valid model object
        if (defined $parent && ! $session->worldModelObj->ivExists('modelHash', $parent)) {

            return $self->error(
                $session, $inputString,
                'The parent #' . $parent . ' isn\'t the number of an existing world model object',
            );
        }

        # Create a hash mapping acceptable switches onto model object types
        %hash = (
            '-w'    => 'weapon',
            '-a'    => 'armour',
            '-g'    => 'garment',
            '-c'    => 'char',
            '-m'    => 'minion',
            '-s'    => 'sentient',
            '-k'    => 'creature',
            '-p'    => 'portable',
            '-d'    => 'decoration',
            '-u'    => 'custom',
        );

        # Regions, rooms and exits can't be added with this command
        if ($switch eq '-n') {

            return $self->error(
                $session, $inputString,
                'Regions can\'t be added to the world model with this command (try \';addregion\''
                . ' instead)',
            );

        } elsif ($switch eq '-r') {

            return $self->error(
                $session, $inputString,
                'Rooms can\'t be added to the world model with this command (try \';addroom\''
                . ' instead)',
            );

        } elsif ($switch eq '-x') {

            return $self->error(
                $session, $inputString,
                'Exits can\'t be added to the exit model with this command (try \';addexitobject\''
                . ' instead)',
            );

        } elsif (! exists $hash{$switch}) {

            return $self->error(
                $session, $inputString,
                'Unrecognised switch - try -w, -a, -g, -c, -m, -s, -k, -p, -d or -u',
            );
        }

        # Add the new model object

        # Create a character
        if ($switch eq '-c') {

            # Check the character doesn't already exist
            if ($session->worldModelObj->ivExists('knownCharHash', $name)) {

                # Character called $name already exists
                return $self->error(
                    $session, $inputString,
                    'There is already a player character called \'' . $name . '\' in the world'
                    . ' model (duplicate player characters are not allowed)',
                );
            }

            # Create the character model object
            $obj = $session->worldModelObj->addChar(
                $session,
                TRUE,       # Update Automapper windows
                $name,
                $parent,
            );

        # Create any other kind of model object (except regions and rooms)
        } else {

            $obj = $session->worldModelObj->addOther(
                $session,
                TRUE,       # Update Automapper windows
                $hash{$switch},     # e.g. 'weapon'
                $name,
                $parent,
            );
        }

        # Operation complete
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Could not add model object \'' . $name . '\'',
            );

        } elsif ($parent) {

            $parentObj = $session->worldModelObj->ivShow('modelHash', $parent);

            return $self->complete(
                $session, $standardCmd,
                'Added ' . $hash{$switch} . ' model object #' . $obj->number . ' \'' . $name
                . '\' (parent #' . $parent . ' \'' . $parentObj->name . '\')',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Added ' . $hash{$switch} . ' model object #' . $obj->number . ' \'' . $name . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetModelParent;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setmodelparent', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['smp', 'setparent', 'setmodelparent'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets (or resets) the parent of a model object';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number, $parent,
            $check,
        ) = @_;

        # Local variables
        my ($obj, $parentObj);

        # Check for improper arguments
        if (! defined $number || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that $number (and $parent, if specified) are valid world model numbers
        if (! $session->worldModelObj->ivExists('modelHash', $number)) {

            return $self->error(
                $session, $inputString,
                'The object #' . $number . ' isn\'t the number of an existing world model object',
            );

        } elsif (defined $parent && ! $session->worldModelObj->ivExists('modelHash', $parent)) {

            return $self->error(
                $session, $inputString,
                'The parent #' . $parent . ' isn\'t the number of an existing world model object',
            );
        }

        # Get the corresponding model objects
        $obj = $session->worldModelObj->ivShow('modelHash', $number);
        if ($parent) {

            $parentObj = $session->worldModelObj->ivShow('modelHash', $parent);
        }

        # Check the rules for setting parents (see the comments in $self->new)
        if ($obj->category eq 'region' && $parentObj && $parentObj->category ne 'region') {

            return $self->error(
                $session, $inputString,
                'A region object can only have another region object as its parent, and the object'
                . ' #' . $parent . ' is not a region',
            );

        } elsif ($obj->category eq 'room') {

            return $self->error(
                $session, $inputString,
                'This command can\'t be used to change the parent of a room object',
            );

        } elsif (defined $parent && $number == $parent) {

            return $self->error(
                $session, $inputString,
                'An object can\'t set itself as its own parent',
            );
        }

        # Set (or reset) the object's parent
        if (
            ! $session->worldModelObj->setParent(
                TRUE,       # Update Automapper windows
                $number,
                $parent,    # May be 'undef'
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not set the parent of the model object #' . $number,
            );

        } elsif ($parent) {

            return $self->complete(
                $session, $standardCmd,
                'Set the parent of the model object #' . $number . ' (\'' . $obj->name . '\') to #'
                . $parent . ' (\'' . $parentObj->name . '\')',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Reet the parent of the model object #' . $number. ' (\'' . $obj->name . '\')',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditModelObject;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editmodelobject', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['emo', 'editmo', 'editmodelobject'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a world model object';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $number || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that $number is a valid world model number
        if (! $session->worldModelObj->ivExists('modelHash', $number)) {

            return $self->error(
                $session, $inputString,
                'Could not edit the model object #' . $number . ' - object does not exist',
            );

        } else {

            $obj = $session->worldModelObj->ivShow('modelHash', $number);
        }

        # Open an 'edit' window for the model object
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::ModelObj::' . ucfirst($obj->category),
                $session->mainWin,
                $session,
                'Edit ' . $obj->category . ' model object #' . $obj->number,
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the model object #' . $number,
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the model object #' . $number,
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditRegionmap;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editregionmap', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['erm', 'editregion', 'editregionmap'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a regionmap';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the regionmap exists
        $obj = $session->worldModelObj->ivShow('regionmapHash', $name);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'The world model doesn\'t contain a regionmap called \'' . $name . '\'',
            );
        }

        # Open an \'edit\' window for the regionmap
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Regionmap',
                $session->mainWin,
                $session,
                'Edit regionmap \'' . $name . '\'',
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the regionmap \'' . $name . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the regionmap \'' . $name . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EmptyRegion;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('emptyregion', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ern', 'emptyregion'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Empties a world model region';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my (
            $regionObj, $regionmapObj, $msg, $result,
            @roomList, @otherList, @labelList,
        );

        # Check for improper arguments
        if (! defined $arg || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get the region model object specified by <number> or <name>

        # ;ern <number>
        if ($axmud::CLIENT->intCheck($arg, 0)) {

            if ($session->worldModelObj->ivExists('regionModelHash', $arg)) {

                $regionObj = $session->worldModelObj->ivShow('regionModelHash', $arg);
            }
        }

        # ;ern <name>
        if (! $regionObj) {

            OUTER: foreach my $obj ($session->worldModelObj->ivValues('regionModelHash')) {

                if ($obj->name eq $arg) {

                    $regionObj = $obj;
                    last OUTER;
                }
            }
        }

        if (! $regionObj) {

            return $self->error(
                $session, $inputString,
                'There is no region object matching \'' . $arg . '\' in the world model',
            );
        }

        # (The rest of the code adapted from GA::Win::Map->emptyRegionCallback)

        # Get a list of the region's child room objects, but don't include child regions as they
        #   won't be deleted
        foreach my $childNum ($regionObj->ivKeys('childHash')) {

            my $childObj = $session->worldModelObj->ivShow('modelHash', $childNum);

            if ($childObj->category eq 'room') {
                push (@roomList, $childObj);
            } elsif ($childObj->category ne 'region') {
                push (@otherList, $childObj);
            }
        }

        # Get a list of the corresponding regionmap's labels
        $regionmapObj = $session->worldModelObj->ivShow('regionmapHash', $regionObj->name);
        @labelList = $regionmapObj->ivValues('gridLabelHash');

        if (! @roomList && ! @otherList && ! @labelList) {

            return $self->error(
                $session, $inputString,
                'The region contains no rooms, no other model objects and no labels',
            );

        } else {

            # Give the user a chance to change their minds, before emptying the region
            $msg = "Are you sure you want to empty the\n\'" . $regionObj->name
                    . "\'? region? It contains:\n\n   Rooms: " . scalar @roomList
                    . "\n   Other model objects: " . scalar @otherList
                    . "\n   Labels: " . scalar @labelList;


            $result = $session->mainWin->showMsgDialogue(
                'Empty region',
                'question',
                $msg,
                'yes-no',
                undef,
                TRUE,           # Preserve newline characters in $msg
            );

            if ($result ne 'yes') {

                return $self->complete(
                    $session, $standardCmd,
                    'Empty region operation cancelled',
                );

            } else {

                # Empty the region
                $result = $session->worldModelObj->emptyRegion(
                    $session,
                    TRUE,              # Update Automapper windows now
                    $regionObj,
                );

                if (! $result) {

                    return $self->error(
                        $session, $inputString,
                        'Empty region operation failed',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        'The region #' . $regionObj->number . ' \'' . $regionObj->name . '\' has'
                        . ' been emptied',
                    );
                }
            }
        }
    }
}

{ package Games::Axmud::Cmd::DeleteRegion;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteregion', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['drn', 'delregion', 'deleteregion'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a world model region';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my (
            $regionObj, $regionmapObj, $msg, $result,
            @roomList, @otherList, @labelList,
        );

        # Check for improper arguments
        if (! defined $arg || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get the region model object specified by <number> or <name>

        # ;drn <number>
        if ($axmud::CLIENT->intCheck($arg, 0)) {

            if ($session->worldModelObj->ivExists('regionModelHash', $arg)) {

                $regionObj = $session->worldModelObj->ivShow('regionModelHash', $arg);
            }
        }

        # ;drn <name>
        if (! $regionObj) {

            OUTER: foreach my $obj ($session->worldModelObj->ivValues('regionModelHash')) {

                if ($obj->name eq $arg) {

                    $regionObj = $obj;
                    last OUTER;
                }
            }
        }

        if (! $regionObj) {

            return $self->error(
                $session, $inputString,
                'There is no region object matching \'' . $arg . '\' in the world model',
            );
        }

        # (The rest of the code adapted from GA::Win::Map->deleteRegionCallback)

        # Get a list of the region's child room objects, but don't include child regions as they
        #   won't be deleted
        foreach my $childNum ($regionObj->ivKeys('childHash')) {

            my $childObj = $session->worldModelObj->ivShow('modelHash', $childNum);

            if ($childObj->category eq 'room') {
                push (@roomList, $childObj);
            } elsif ($childObj->category ne 'region') {
                push (@otherList, $childObj);
            }
        }

        # Get a list of the corresponding regionmap's labels
        $regionmapObj = $session->worldModelObj->ivShow('regionmapHash', $regionObj->name);
        @labelList = $regionmapObj->ivValues('gridLabelHash');

        if (@roomList || @otherList || @labelList) {

            # Give the user a chance to change their minds, before emptying the region
            $msg = "Are you sure you want to delete the\n\'" . $regionObj->name
                    . "\'? region? It contains:\n\n   Rooms: " . scalar @roomList
                    . "\n   Other model objects: " . scalar @otherList
                    . "\n   Labels: " . scalar @labelList;


            $result = $session->mainWin->showMsgDialogue(
                'Delete region',
                'question',
                $msg,
                'yes-no',
                undef,
                TRUE,           # Preserve newline characters in $msg
            );

            if ($result ne 'yes') {

                return $self->complete(
                    $session, $standardCmd,
                    'Delete region operation cancelled',
                );
            }
        }

        # Otherwise, Delete the region
        $result = $session->worldModelObj->deleteRegions(
            $session,
            TRUE,              # Update Automapper windows now
            $regionObj,
        );

        if (! $result) {

            return $self->error(
                $session, $inputString,
                'Delete region operation failed',
                );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'The region #' . $regionObj->number . ' \'' . $regionObj->name . '\' has'
                . ' been deleted',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteTemporaryRegion;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletetemporaryregion', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dtn', 'deltempregion', 'deletetemporaryregion'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes all temporary regions';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my ($count, $msg, $result);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Count the number of temporary region objects
        $count = 0;
        foreach my $regionObj ($session->worldModelObj->ivValues('regionModelHash')) {

            if ($regionObj->tempRegionFlag) {

                $count++;
            }
        }

        if (! $count) {

            return $self->error(
                $session, $inputString,
                'There are no temporary regions in the world model',
            );

        } else {

            # (The rest of the code adapted from GA::Win::Map->deleteTempRegionsCallback)

            # Give the user a chance to change their minds, before emptying the region
            if ($count == 1) {

                $msg = 'There is 1 temporary region in the world model. Are you sure you want to'
                            . ' delete it?';
            } else {

                $msg = 'There are ' . $count . ' temporary regions in the world model. Are you sure'
                            . ' you want to delete them all?'
            }

            $result = $session->mainWin->showMsgDialogue(
                'Delete temporary regions',
                'question',
                $msg,
                'yes-no',
            );

            if ($result ne 'yes') {

                return $self->complete(
                    $session, $standardCmd,
                    'Delete temporary regions operation cancelled',
                );

            } else {

                # Delete all temporary regions
                $result = $session->worldModelObj->deleteTempRegions(
                    $session,
                    TRUE,              # Update Automapper windows now
                );

                if (! $result) {

                    return $self->error(
                        $session, $inputString,
                        'Delete temporary regions operation failed',
                    );

                } elsif ($count == 1) {

                    return $self->complete(
                        $session, $standardCmd,
                        '1 temporary region deleted',
                    );

                } else {

                    return $self->complete(
                        $session, $standardCmd,
                        $count . ' temporary regions deleted',
                    );
                }
            }
        }
    }
}

{ package Games::Axmud::Cmd::DeleteRoom;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteroom', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['drm', 'delroom', 'deleteroom'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a world model room';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my ($roomObj, $result);

        # Check for improper arguments
        if (! defined $number || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get the room model object specified by <number>
        $roomObj = $session->worldModelObj->ivShow('modelHash', $number);
        if (! $roomObj) {

            return $self->error(
                $session, $inputString,
                'There is no object #' . $number . ' in the world model',
            );

        } elsif ($roomObj->category ne 'room') {

            return $self->error(
                $session, $inputString,
                'The model object #' . $number . ' is a ' . $roomObj->category . ' object',
            );
        }

        # Delete the room
        $result = $session->worldModelObj->deleteRooms(
            $session,
            TRUE,              # Update Automapper windows now
            $roomObj,
        );

        if (! $result) {

            return $self->error(
                $session, $inputString,
                'Delete room operation failed',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'The room #' . $roomObj->number . ' \'' . $roomObj->name . '\' has'
                . ' been deleted',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteModelObject;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletemodelobject', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dmo', 'delmo', 'delmodel', 'deletemodelobject'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes an object from the world model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my ($obj, $result);

        # Check for improper arguments
        if (! defined $number || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get the model object specified by <number>
        $obj = $session->worldModelObj->ivShow('modelHash', $number);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'There is no object #' . $number . ' in the world model',
            );

        } elsif ($obj->category eq 'region' || $obj->category eq 'room') {

            return $self->error(
                $session, $inputString,
                'Region and room model objects can\'t be deleted with this command (try'
                . ' \';deleteregion\' or \';deleteroom\' instead)',
            );
        }

        # Delete the object
        $result = $session->worldModelObj->deleteObj(
            $session,
            TRUE,              # Update Automapper windows now
            $obj,
        );

        if (! $result) {

            return $self->error(
                $session, $inputString,
                'Delete model object operation failed',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'The model object #' . $obj->number . ' \'' . $obj->name . '\' has'
                . ' been deleted',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listmodel', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lmd', 'listmd', 'listmodel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists world model objects';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $flagCount, $switch, $start, $stop, $recentNum, $recentObj, $numberString,
            $categoryString,
            @list,
            %categoryHash,
        );

        # Extract switches
        #   -n region, -r room, -w weapon, -a armour,  -g garment, -c char, -s sentient,
        #       -k creature, -p portable, -d decoration, -u custom
        $flagCount = 0;

        ($switch, @args) = $self->extract('-n', 0, @args);
        if (defined $switch) {

            $categoryHash{'region'} = undef;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-r', 0, @args);
        if (defined $switch) {

            $categoryHash{'room'} = undef;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-w', 0, @args);
        if (defined $switch) {

            $categoryHash{'weapon'} = undef;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-a', 0, @args);
        if (defined $switch) {

            $categoryHash{'armour'} = undef;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-g', 0, @args);
        if (defined $switch) {

            $categoryHash{'garment'} = undef;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-c', 0, @args);
        if (defined $switch) {

            $categoryHash{'char'} = undef;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-m', 0, @args);
        if (defined $switch) {

            $categoryHash{'minion'} = undef;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-s', 0, @args);
        if (defined $switch) {

            $categoryHash{'sentient'} = undef;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-k', 0, @args);
        if (defined $switch) {
            $categoryHash{'creature'} = undef;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-p', 0, @args);
        if (defined $switch) {

            $categoryHash{'portable'} = undef;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-d', 0, @args);
        if (defined $switch) {

            $categoryHash{'decoration'} = undef;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-u', 0, @args);
        if (defined $switch) {

            $categoryHash{'custom'} = undef;
            $flagCount++;
        }

        # @args should now have between 0 and 2 arguments
        $start = shift @args;
        $stop = shift @args;
        if (@args) {

            return $self->improper($session, $inputString);
        }

        # Check that <start> and <stop> are valid world model numbers, if specified
        if ((defined $start || defined $stop) && ! $session->worldModelObj->modelObjCount) {

            return $self->complete($session, $standardCmd, 'The world model is empty');

        } elsif (
            defined $start
            && ! $axmud::CLIENT->intCheck($start, 1, $session->worldModelObj->modelObjCount)
        ) {
            return $self->error(
                $session, $inputString,
                'Can\'t process model #' . $start . ': must be a number between 1 and '
                . $session->worldModelObj->modelObjCount,
            );

        } elsif (
            defined $stop
            && ! $axmud::CLIENT->intCheck($stop, 1, $session->worldModelObj->modelObjCount)
        ) {
            return $self->error(
                $session, $inputString,
                'Can\'t process model #' . $stop . ': must be a number between 1 and '
                . $session->worldModelObj->modelObjCount,
            );

        } elsif (defined $start && defined $stop && $start > $stop) {

            return $self->error(
                $session, $inputString,
                'Format: \';listmodel <start> <stop>\'',
            );
        }

        # The command ;lmd <type> <number> can't be used
        if (defined $start && ! defined $stop && $flagCount) {

            return $self->error(
                $session, $inputString,
                'Format: \';listmodel <type> <start> <stop>\'',
            );
        }

        # ;lmd
        if (! defined $start && ! $flagCount) {

            $session->writeText('World model status');
            $session->writeText(
                '   No. objects    : ' . $session->worldModelObj->modelActualCount,
            );
            $session->writeText(
                '   Apparent size  : ' . $session->worldModelObj->modelObjCount,
            );

            # (Import the most-recently created model object)
            $recentNum = $session->worldModelObj->mostRecentNum;
            if ($recentNum) {

                $recentObj = $session->worldModelObj->ivShow('modelHash', $recentNum);
                if ($recentObj) {

                    $session->writeText('   Newest object  : #' . $recentNum);
                    $session->writeText(
                        '      ' . sprintf('Name        : %-32.32s', $recentObj->name),
                    );
                    $session->writeText('      Category    : ' . $recentObj->category);

                    if ($recentObj->parent) {
                        $session->writeText('      Parent      : ' . $recentObj->parent);
                    } else {
                        $session->writeText('      Parent      : (none)');
                    }

                } else {

                    $session->writeText(
                        '   Newest object  : #' . sprintf(
                            '%-8.8s',
                            $session->worldModelObj->mostRecentNum
                        ) . ' (deleted)',
                    );
                }

            } else {

                $session->writeText('   Newest object  : (none)');
            }

            $session->writeText(
                '   No. exit objs  : ' . $session->worldModelObj->exitActualCount,
            );

            return $self->complete($session, $standardCmd, 'World model status displayed');

        # ;lmd <number>
        # ;lmd <start> <stop>
        # ;lmd <type>
        # ;lmd <type> <start> <stop>
        } else {

            # Compile a list of blessed references to world model objects

            # ;lmd <number>
            if (defined $start && ! defined $stop) {

                if (! $session->worldModelObj->ivExists('modelHash', $start)) {

                    if ($start > $session->worldModelObj->modelObjCount) {

                        return $self->error(
                            $session, $inputString,
                            'World model object #' . $start . ' hasn\'t been created yet',
                        );

                    } else {

                        return $self->error(
                            $session, $inputString,
                            'World model object #' . $start . ' has been deleted',
                        );
                    }

                } else {

                    # Only one object in the list
                    push (@list, $session->worldModelObj->ivShow('modelHash', $start));
                }

            # ;lmd <start stop>
            # ;lmd <type>
            # ;lmd <type> <start> <stop>
            } else {

                # ;lmd <type>
                if (! defined $start && ! defined $stop) {

                    $start = 1;
                    $stop = $session->worldModelObj->modelObjCount;
                }

                # Add to the list every existing object between $start and $stop. If a <type> or
                #   <type>s were specified, only add matching types to the list
                for (my $count = $start; $count <= $stop; $count++) {

                    my $obj;

                    if ($session->worldModelObj->ivExists('modelHash', $count)) {

                        $obj = $session->worldModelObj->ivShow('modelHash', $count);

                        if (
                            ! $flagCount
                            || ($flagCount && exists $categoryHash{$obj->category})
                        ) {
                            push (@list, $obj);
                        }
                    }
                }
            }

            if (! @list) {

                return $self->complete($session, $standardCmd, 'The specified list is empty');
            }

            # Display header
            if (defined $start && defined $stop) {
                $numberString = '(#'. $start . '-'. $stop . ') ';
            } else {
                $numberString = '';
            }

            if ($flagCount) {
                $categoryString = '(' . join (' ', (sort {$a cmp $b} keys %categoryHash)) . ')';
            } else {
                $categoryString = '';
            }

            $session->writeText('World model object list ' . $numberString . $categoryString);
            $session->writeText(
                '   Number    Category     Object name                      Parent',
            );

            # Display list
            foreach my $obj (@list) {

                if ($obj->parent) {

                    $session->writeText(
                        '   #' . sprintf(
                            '%-8.8s %-12.12s %-32.32s #%-8.8s',
                            $obj->number,
                            $obj->category,
                            $obj->name,
                            $obj->parent,
                        )
                    );

                } else {

                    $session->writeText(
                        '   #' . sprintf(
                            '%-8.8s %-12.12s %-32.32s (none)',
                            $obj->number,
                            $obj->category,
                            $obj->name,
                        )
                    );
                }
            }

            # Display footer
            if (@list == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'End of world model list (1 object found)',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'End of world model list (' . @list . ' objects found)',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::ListOrphan;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listorphan', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lor', 'listor', 'listorphan'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists world model orphan objects';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $string,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If a switch was specified, check it's valid
        if (defined $switch && $switch ne '-r' && $switch ne '-o') {

            return $self->error(
                $session, $inputString,
                'Invalid switch \'' . $switch . '\' - try \'-r\' or \'-o\'',
            );
        }

        # Don't display anything if the world model is empty
        if (! $session->worldModelObj->modelHash) {

            return $self->error($session, $inputString, 'The world model is empty');
        }

        # Compile an ordered list of world model objects to display
        for (my $count = 1; $count <= $session->worldModelObj->modelObjCount; $count++) {

            my $obj;

            if ($session->worldModelObj->ivExists('modelHash', $count)) {

                $obj = $session->worldModelObj->ivShow('modelHash', $count);

                # If the model is an orphan, add it to the list (depending on the value of $switch)
                if (! $obj->parent) {

                    if (
                        ! $switch
                        || ($switch eq '-r' && $obj->category eq 'room')
                        || (
                            $switch eq '-o' && $obj->category ne 'region'
                            && $obj->category ne 'room'
                        )
                    ) {
                        push (@list, $obj);
                    }
                }
            }
        }

        if (! @list) {

            return $self->complete(
                $session, $standardCmd,
                'The world model\'s orphan list is empty',
            );
        }

        # Display header
        if (! $switch) {
            $string = 'all object types';
        } elsif ($switch eq '-r') {
            $string = 'rooms only';
        } else {
            $string = 'all objects except rooms/regions';
        }

        $session->writeText('World model orphan list (' . $string . ')');
        $session->writeText('   Number    Category     Object name');

        # Display list
        foreach my $obj (@list) {

            $session->writeText(
                '   #' . sprintf(
                    '%-8.8s %-12.12s %-32.32s',
                    $obj->number,
                    $obj->category,
                    $obj->name,
                ),
            );
        }

        # Display footer
        if (@list == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of world model list (1 orphan object found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of world model list (' . @list . ' orphan objects found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DumpModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('dumpmodel', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dmd', 'dumpmd', 'dumpmodel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists all world model objects';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my ($number, $count);

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if ($session->worldModelObj->modelObjCount) {

            return $self->complete($session, $standardCmd, 'The world model object list is empty');
        }

        # Display header
        $session->writeText('World model object dump');
        $session->writeText('   Number    Category     Object name                      Parent');

        # Display list
        $count = 0;
        for ($number = 0; $number <= $session->worldModelObj->modelObjCount; $number++) {

            my $obj = $session->worldModelObj->ivShow('modelHash', $number);
            if ($obj) {

                $count++;

                if ($obj->parent) {

                    $session->writeText(
                        '   #' . sprintf(
                            '%-8.8s %-12.12s %-32.32s #%-8.8s',
                            $number,
                            $obj->category,
                            $obj->name,
                            $obj->number,
                        )
                    );

                } else {

                    $session->writeText(
                        '   #' . sprintf(
                            '%-8.8s %-12.12s %-32.32s (none)',
                            $number,
                            $obj->category,
                            $obj->name,
                        )
                    );
                }
            }
        }

        # Display footer
        if ($count == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of world model list (1 object found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of world model list (' . $count  . ' objects found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SplitModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('splitmodel', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['smd', 'splitmodel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets hows ' . $axmud::SCRIPT . ' stores large world models';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my $msg;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;smd
        if (! defined $arg) {

            # Display header
            $session->writeText('World model split settings');

            # Display list
            $msg = '   Split large world models into multiple data files: ';
            if (! $axmud::CLIENT->allowModelSplitFlag) {
                $session->writeText($msg . 'NO');
            } else {
                $session->writeText($msg . 'YES');
            }

            $session->writeText(
                '   Default size of split files (# model objects):     '
                . $axmud::CLIENT->constModelSplitSize,
            );

            $session->writeText(
                '   Actual size of split files (# model objects):      '
                . $axmud::CLIENT->modelSplitSize,
            );

            # Display footer
            return $self->complete($session, $standardCmd, 'End of list');

        } elsif ($arg eq 'on') {

            if ($axmud::CLIENT->allowModelSplitFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Large world models can already be split into multiple data files',
                );

            } else {

                $axmud::CLIENT->set_allowModelSplitFlag(TRUE);

                return $self->complete(
                    $session, $standardCmd,
                    'Large world models can now be split into multiple data files',
                );
            }

        } elsif ($arg eq 'off') {

            if ($axmud::CLIENT->allowModelSplitFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Large world models are already saved as a single data file',
                );

            } else {

                $axmud::CLIENT->set_allowModelSplitFlag(TRUE);

                return $self->complete(
                    $session, $standardCmd,
                    'Large world models are now saved as a single data file',
                );
            }

        } elsif ($arg eq '-d') {

            $axmud::CLIENT->set_modelSplitSize($axmud::CLIENT->constModelSplitSize);

            return $self->complete(
                $session, $standardCmd,
                'Maximum size of split world model data files set to '
                . $axmud::CLIENT->modelSplitSize,
            );

        } else {

            if (! $axmud::CLIENT->intCheck($arg, 1000)) {

                return $self->error(
                    $session, $inputString,
                    'Invalid value (must be an integer, 1000 or above)',
                );

            } else {

                $axmud::CLIENT->set_modelSplitSize($arg);

                return $self->complete(
                    $session, $standardCmd,
                    'Maximum size of split world model data files set to ' . $arg,
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::EditModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editmodel', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['emd', 'editmd', 'editmodel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for the world model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Open an \'edit\' window for the world model
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::WorldModel',
                $session->mainWin,
                $session,
                'Edit world model',
                $session->worldModelObj,
                FALSE,                          # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the world model',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the world model',
            );
        }
    }
}

{ package Games::Axmud::Cmd::MergeModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('mergemodel', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mmd', 'mergemd', 'mergemodel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Imports and merges a world model file';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $importPath,
            $check,
        ) = @_;

        # Local variables
        my (
            $extractObj, $tempDir, $modelPath, $hashRef, $wmObj, $tempSession, $tempWorldObj,
            $tempFileObj, $tempMapObj, $fileVersion,
            $errorCount, $fixCount,
            @fileList, @outputList,
            %loadHash,
        );

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->improper($session, $inputString);
        }

        # Check that loading is allowed at all
        if (! $axmud::CLIENT->loadDataFlag) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'File load/save is disabled in all sessions',
            );
        }

        # If a file path was not specified, open a file chooser dialog to decide which file to
        #   import
        if (! $importPath) {

            $importPath = $session->mainWin->showFileChooser(
                'Merge world model',
                'open',
            );

            if (! $importPath) {

                $axmud::CLIENT->set_fileFailFlag(TRUE);

                return $self->complete($session, $standardCmd, 'File(s) not imported');
            }
        }

        # Check that $importPath is a valid compressed file (ending .tar, .tar.gz, .tgz, .gz, .zip,
        #   .bz2, .tar.bz2, .tbz or .lzma)
        if (
            ! ($importPath =~ m/\.tar$/)
            && ! ($importPath =~ m/\.tgz$/)
            && ! ($importPath =~ m/\.gz$/)
            && ! ($importPath =~ m/\.zip$/)
            && ! ($importPath =~ m/\.bz2$/)
            && ! ($importPath =~ m/\.tbz$/)
            && ! ($importPath =~ m/\.lzma$/)
        ) {
            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'File(s) not imported (you specified something that doesn\'t appear to be a'
                . ' compressed archive, e.g. a .zip or .tar.gz file)',
            );
        }

        # For large files (e.g. world models containing tens of thousands of rooms), we need to
        #   display an initial message to explain the pause
        $session->writeText('Importing file(s)...');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->do');

        # Build an Archive::Extract object
        $extractObj = Archive::Extract->new(archive => $importPath);
        if (! $extractObj) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'No files imported (file decompression error)',
            );
        }

        # Extract the object to a temporary directory
        $tempDir = $axmud::DATA_DIR . '/data/temp';
        if (! $extractObj->extract(to => $tempDir)) {

            $axmud::CLIENT->set_fileFailFlag(TRUE);

            return $self->error(
                $session, $inputString,
                'No files imported (file decompression error)',
            );
        }

        # All the files are now in /data/temp/export. Get a list of paths, relative to $tempDir, of
        #   all the extracted files
        @fileList = @{$extractObj->files};  # e.g. export/tasks.axm
        # Convert all the paths into absolute paths
        foreach my $file (@fileList) {

            $file = $axmud::DATA_DIR . '/data/temp/' . $file;
        }

        # Extract from @fileList the world model file (there should only be one, if any)
        OUTER: foreach my $file (@fileList) {

            my (
                $matchFlag,
                %headerHash,
            );

            # Ignore files that don't end with a compatible file extension (like .axm)
            INNER: foreach my $ext (@axmud::COMPAT_EXT_LIST) {

                if ($file =~ m/\.$ext$/) {

                    $matchFlag = TRUE;
                    last INNER;
                }
            }

            if (! $matchFlag) {

                next OUTER;
            }

            # Check it's really an Axmud file by loading the file into a hash
            %headerHash = $axmud::CLIENT->configFileObj->examineDataFile($file, 'return_header');
            if (
                ! %headerHash
                || ! $axmud::CLIENT->configFileObj->checkCompatibility($headerHash{'script_name'})
                || $axmud::CLIENT->convertVersion($headerHash{'script_version'})
                        > $axmud::CLIENT->convertVersion($axmud::VERSION)
            ) {
                next OUTER;
            }

            # If it's a world model file, we can use it
            if ($headerHash{'file_type'} eq 'worldmodel') {

                $modelPath = $file;
                last OUTER;
            }
        }

        if (! $modelPath) {

            return $self->error(
                $session, $inputString,
                'The specified archive doesn\'t contain a readable world model file',
            );
        }

        # Now retrieve the world model data from $modelPath
        # (We already have a world model in memory that we don't want to overwrite, so we can't call
        #   GA::Obj::File->loadDataFile)
        eval { $hashRef = Storable::lock_retrieve($modelPath); };
        if (! $hashRef) {

            # ->lock_retrieve() failed
            return $self->error(
                $session, $inputString,
                'Failed to merge the world model file (internal error)',
            );
        }

        # Retrieve the world model itself
        $wmObj = $$hashRef{'world_model_obj'};
        if (
            ! $wmObj
            # After v1.0.868
            || (exists $wmObj->{_objName} && $wmObj->{_objName} ne 'world_model')
            # Before v1.0.868
            || (exists $wmObj->{_name} && $wmObj->{_name} ne 'world_model')
        ) {
            return $self->error(
                $session, $inputString,
                'The specified archive doesn\'t contain a readable world model file',
            );
        }

        # Much of the code in GA::Obj::File for updating data from previous Axmud versions
        #   assumes that every GA::Session has a world model; this means that we can't simply
        #   call GA::Obj::File->updateExtractedData for this extra world model
        # Solution is to create a fake (temporary) session and assign the imported world model
        #   to it
        $tempSession = Games::Axmud::Session->new(-1, $session->currentWorld->name);
        $tempSession->ivPoke('worldModelObj', $wmObj);
        # Set the 'main' window, as GA::File::Obj->updateExtractedData needs it
        $tempSession->ivPoke('mainWin', $session->mainWin);
        # Also create a temporary world profile
        $tempWorldObj = Games::Axmud::Profile::World->new(
            $tempSession,
            $session->currentWorld->name,
            TRUE,
        );

        # Also create a temporary file object for the world model
        $tempFileObj = Games::Axmud::Obj::File->new(
            'worldmodel',
            $session->currentWorld->name,
               $tempSession,
        );

        # Also create a temporary map object
        $tempMapObj = Games::Axmud::Obj::Map->new($tempSession);

        $tempSession->ivPoke('currentWorld', $tempWorldObj);
        $tempSession->ivAdd('sessionFileObjHash', 'worldmodel', $tempFileObj);
        $tempSession->ivPoke('mapObj', $tempMapObj);

        # If the file was created by an earlier version of Axmud, we have to update the model's
        #   data in the usual way
        $fileVersion = $axmud::CLIENT->convertVersion($$hashRef{'script_version'});
        if ($fileVersion < $axmud::CLIENT->convertVersion($axmud::VERSION)) {

            $session->writeText('Updating data for this ' . $axmud::SCRIPT . ' version...');
            $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->do');

            # Now we can update the imported world model
            if ($fileVersion < 1_000_868) {

                $tempFileObj->update_obj_world_model($wmObj);
            }

            $tempFileObj->updateExtractedData($fileVersion);
        }

        # If the imported world model has any temporary regions, remove them
        $wmObj->deleteTempRegions($tempSession, FALSE);

        # To avoid unforeseen complications, get rid of the temporary objects immediately
        $tempSession = undef;
        $tempWorldObj = undef;
        $tempFileObj = undef;
        $tempMapObj = undef;

        # Now we're ready to merge one model into another
        $session->writeText('Merging data...');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->do');

        # Pass this imported world model to this session's own model, so it can merge rooms, labels
        #   and so on
        if (! $session->worldModelObj->mergeModel($session, $wmObj)) {

            return $self->error(
                $session, $inputString,
                'The specified archive contained a readable world model file, but ' . $axmud::SCRIPT
                . ' was not able to merge its contents into the existing world model',
            );
        }

        $session->writeText('Running model test...');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->do');

        # Run the usual model test
        ($errorCount, $fixCount, @outputList) = $session->worldModelObj->testModel(
            $session,
            FALSE,          # Don't problems
            TRUE,           # Use a list of return values
        );

        if ($errorCount) {

            $session->writeText(
                'Model test reports the following inconsistencies (use \';testmodel -f\' to fix'
                . ' them):',
            );
        }

        # That's the end of the test. Display any output
        foreach my $msg (@outputList) {

            $session->writeText($msg);
        }

        return $self->complete(
            $session, $standardCmd,
            'Merge operation complete (new regions: ' . $wmObj->ivPairs('regionmapHash')
            . ', new rooms: ' . $wmObj->ivPairs('roomModelHash') . ', new exits: '
            . $wmObj->ivPairs('exitModelHash') . ')',
        );
    }
}

{ package Games::Axmud::Cmd::UpdateModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('updatemodel', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['umd', 'updatemd', 'updatemodel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Updates the world model\'s lists of unknown words';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $dictObj, $string, $nounString, $adjString, $unknownString,
            @objList, @sortedList,
            %nounHash, %adjHash, %unknownHash,
        );

        # Check for improper arguments
        if ((defined $switch && $switch ne '-t') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the current dictionary
        $dictObj = $session->currentDict;

        # Check every model object that isn't a region or room. We'll assume that the majority
        #   of objects in the world model will be a region or a room, so the quickest way to get a
        #   list of all non-region, non-room objects is to not search ->model directly
        @objList = (
            $session->worldModelObj->ivValues('weaponModelHash'),
            $session->worldModelObj->ivValues('armourModelHash'),
            $session->worldModelObj->ivValues('garmentModelHash'),
            $session->worldModelObj->ivValues('charModelHash'),
            $session->worldModelObj->ivValues('minionModelHash'),
            $session->worldModelObj->ivValues('sentientModelHash'),
            $session->worldModelObj->ivValues('creatureModelHash'),
            $session->worldModelObj->ivValues('portableModelHash'),
            $session->worldModelObj->ivValues('decorationModelHash'),
            $session->worldModelObj->ivValues('customModelHash'),
        );

        foreach my $obj (@objList) {

            my (@nounList, @adjList, @unknownList);

            # Import the object's unknown word list
            foreach my $word ($obj->unknownWordList) {

                if ($dictObj->ivExists('combNounHash', $word)) {
                    push (@nounList, $word);
                } elsif ($dictObj->ivExists('combAdjHash', $word)) {
                    push (@adjList, $word);
                } else {
                    push (@unknownList, $word);
                }
            }

            # Re-classify any nouns or adjectives found
            if (@nounList) {

                $obj->ivPush('otherNounList', @nounList);

                if (! $switch) {

                    # Store the re-classified words in a hash, ready to display at the end of the
                    #   operation
                    foreach my $noun (@nounList) {

                        if (exists $nounHash{$noun}) {
                            $nounHash{$noun} = $nounHash{$noun} + 1;
                        } else {
                            $nounHash{$noun} = 1;
                        }
                    }
                }
            }

            if (@adjList) {

                $obj->ivPush('adjList', @adjList);

                if (! $switch) {

                    foreach my $adj (@adjList) {

                        if (exists $adjHash{$adj}) {
                            $adjHash{$adj} = $adjHash{$adj} + 1;
                        } else {
                            $adjHash{$adj} = 1;
                        }
                    }
                }
            }

            # Return any still-unrecognised words to the unknown word list (or empty the object's
            #   list, if there are no unknown words left)
            if (! @unknownList) {
                $obj->ivEmpty('unknownWordList');
            } else {
                $obj->ivPoke('unknownWordList', @unknownList);
            }

            if (! $switch) {

                foreach my $unknown (@unknownList) {

                    if (exists $unknownHash{$unknown}) {
                        $unknownHash{$unknown} = $unknownHash{$unknown};
                    } else {
                        $unknownHash{$unknown} = 1;
                    }
                }
            }
        }

        # Display the output
        if (! $switch) {

            # Display nouns
            if (%nounHash) {

                @sortedList = sort {lc($a) cmp lc($b)} (keys %nounHash);
                $string = '';

                foreach my $noun (@sortedList) {

                    if ($string) {

                        $string .= ' ';
                    }

                    if ($nounHash{$noun} == 1) {
                        $string .= $noun;
                    } else {
                        $string .= $noun . ' x' . $nounHash{$noun};    # e.g. 'orc x2'
                    }
                }
            }

            if ($string) {

                $session->writeText('Reclassified nouns: ' . $string);
            }

            # Display adjectives
            if (%adjHash) {

                @sortedList = sort {lc($a) cmp lc($b)} (keys %adjHash);
                $string = '';

                foreach my $adj (@sortedList) {

                    if ($string) {

                        $string .= ' ';
                    }

                    if ($adjHash{$adj} == 1) {
                        $string .= $adj;
                    } else {
                        $string .= $adj . ' x' . $adjHash{$adj};    # e.g. 'hairy x2'
                    }
                }
            }

            if ($string) {

                $session->writeText('Reclassified adjectives: ' . $string);
            }

            # Display unrecognised words
            if (%unknownHash) {

                @sortedList = sort {lc($a) cmp lc($b)} (keys %unknownHash);
                $string = '';

                foreach my $unknown (@sortedList) {

                    if ($string) {$string .= ' '}

                    if ($unknownHash{$unknown} == 1) {
                        $string .= $unknown;
                    } else {
                        $string .= $unknown . ' x' . $unknownHash{$unknown};
                    }
                }
            }

            if ($string) {

                $session->writeText('Unrecognised words: ' . $string);
            }
        }

        # Display confirmation
        if (! @objList) {

             return $self->complete(
                $session, $standardCmd,
                'Update complete, no world model objects found (besides regions and rooms)',
            );

        } elsif (! %nounHash && ! %adjHash) {

             return $self->complete(
                $session, $standardCmd,
                'Update complete, no words re-classified',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Update complete. Objects: ' . scalar @objList . ', reclassified nouns: '
                . scalar (keys %nounHash) . ', reclassified adjectives: ' . scalar (keys %adjHash)
                . ', still unrecognised: ' . scalar (keys %unknownHash),
            );
        }
    }
}

{ package Games::Axmud::Cmd::CompressModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('compressmodel', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['cmd', 'compmd', 'compressmodel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Reduces amount of memory used by the world model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # It might be a long wait, so make sure the message is visible right away
        $session->writeText('Reducing amount of memory used by world model...');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->do');

        foreach my $roomObj ($session->worldModelObj->ivValues('roomModelHash')) {

            $roomObj->compress();
        }

        foreach my $exitObj ($session->worldModelObj->ivValues('exitModelHash')) {

            $exitObj->compress();
        }

        # (This line makes sure the correct file object's ->modifyFlag is set)
        $session->worldModelObj->ivPoke('author', $session->worldModelObj->author);

        return $self->complete(
            $session, $standardCmd,
            'Operation complete (don\'t forget to \';save\' the changes)',
        );
    }
}

{ package Games::Axmud::Cmd::ModelReport;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('modelreport', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['mrp', 'modelreport'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Compiles a report on the world model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

       # Local variables
        my (
            $mainCount, $suppCount, $matchFlag, $visitsFlag, $guildsFlag, $roomFlagsFlag,
            $roomsFlag, $exitsFlag, $checkedFlag, $quickFlag,
            @charList, @regionList, @guildNameList, @roomFlagList, @regionObjList,
        );

        # Extract switches
        $mainCount = 0;
        $suppCount = 0;
        if (@args) {

            do {

                my ($switch, $char, $region, $guildName, $roomFlag);

                $matchFlag = FALSE;

                # Extract the main switches: -v (visits), -g (guilds), -f (room flags),
                #   -m (room titles/descrips), -x (exits), -h (checked directions), -q (quick)
                ($switch, @args) = $self->extract('-v', 0, @args);
                if (defined $switch) {

                    $matchFlag = TRUE;
                    $visitsFlag = TRUE;
                    $mainCount++;
                }

                ($switch, @args) = $self->extract('-g', 0, @args);
                if (defined $switch) {

                    $matchFlag = TRUE;
                    $guildsFlag = TRUE;
                    $mainCount++;
                }

                ($switch, @args) = $self->extract('-f', 0, @args);
                if (defined $switch) {

                    $matchFlag = TRUE;
                    $roomFlagsFlag = TRUE;
                    $mainCount++;
                }

                ($switch, @args) = $self->extract('-m', 0, @args);
                if (defined $switch) {

                    $matchFlag = TRUE;
                    $roomsFlag = TRUE;
                    $mainCount++;
                }

                ($switch, @args) = $self->extract('-x', 0, @args);
                if (defined $switch) {

                    $matchFlag = TRUE;
                    $exitsFlag = TRUE;
                    $mainCount++;
                }

                ($switch, @args) = $self->extract('-h', 0, @args);
                if (defined $switch) {

                    $matchFlag = TRUE;
                    $checkedFlag = TRUE;
                    $mainCount++;
                }

                ($switch, @args) = $self->extract('-q', 0, @args);
                if (defined $switch) {

                    $matchFlag = TRUE;
                    $quickFlag = TRUE;
                    $mainCount++;
                }

                # Extract the supplementary switches: -c <character>, -r <region>, -g <guild>,
                #   -l <flag>
                ($switch, $char, @args) = $self->extract('-c', 1, @args);
                if (defined $switch) {

                    $matchFlag = TRUE;
                    $suppCount++;

                    if (! $char) {

                        return $self->error(
                            $session, $inputString,
                            'Which character? (Try \'-c <character>\')',
                        );

                    } else {

                        push (@charList, $char);
                    }
                }

                ($switch, $region, @args) = $self->extract('-r', 1, @args);
                if (defined $switch) {

                    $matchFlag = TRUE;
                    $suppCount++;

                    if (! $region) {

                        return $self->error(
                            $session, $inputString,
                            'Which region? (Try \'-r <region>\')',
                        );

                    } else {

                        push (@regionList, $region);
                    }
                }

                ($switch, $guildName, @args) = $self->extract('-n', 1, @args);
                if (defined $switch) {

                    $matchFlag = TRUE;
                    $suppCount++;

                    if (! $guildName) {

                        return $self->error(
                            $session, $inputString,
                            'Which guild? (Try \'-n <guild>\')',
                        );

                    } else {

                        push (@guildNameList, $guildName);
                    }
                }

                ($switch, $roomFlag, @args) = $self->extract('-l', 1, @args);
                if (defined $switch) {

                    $matchFlag = TRUE;
                    $suppCount++;

                    if (! $roomFlag) {

                        return $self->error(
                            $session, $inputString,
                            'Which room flag? (Try \'-l <flag>\')',
                        );

                    } else {

                        push (@roomFlagList, $roomFlag);
                    }
                }

            } until (! @args || ! $matchFlag);

            if (! $matchFlag) {

                # Invalid values in @args
                return $self->improper($session, $inputString);
            }
        }

        # Only one main switch allowed
        if ($mainCount > 1) {

            return $self->error(
                $session, $inputString,
                'Only one switch from the group -v, -g, -f, -m, -x, -h, -q allowed',
            );
        }

        # Supplementary switch -c only allowed when the main switch is -v
        if (! $visitsFlag && @charList) {

            return $self->error(
                $session, $inputString,
                'Which report? (Try \'-v -c <character>\')',
            );
        }

        # Supplementary switch -n only allowed when the main switch is -g
        if (! $guildsFlag && @guildNameList) {

            return $self->error(
                $session, $inputString,
                'Which report? (Try \'-g -n <region>\')',
            );
        }

        # Supplementary switch -l only allowed when the main switch is -f
        if (! $roomFlagsFlag && @roomFlagList) {

            return $self->error(
                $session, $inputString,
                'Which report? (Try \'-f -l <flag>\')',
            );
        }

        # No supplementary switches allowed when the main switch is -q
        if ($quickFlag && $suppCount) {

            return $self->error(
                $session, $inputString,
                'The switch -q can\'t be combined with other switches',
            );
        }

        # Main switch -q can only be used when the Automapper window is open
        if ($quickFlag && (! $session->mapWin || ! $session->mapWin->currentRegionmap)) {

             return $self->error(
                $session, $inputString,
                'The switch -q can only be used when the Automapper window is open, and is showing'
                . ' a region',
            );
        }

        # If there is no main switch, one region is allowed, but multiple regions are not
        if (! $mainCount && (scalar @regionList) > 1) {

            return $self->error(
                $session, $inputString,
                'You can\'t specify multiple regions without using one of the switches \'-v\','
                . ' \'-g\', \'-f\' or \'-x\'',
            );
        }

        # Check that any specified characters actually exist
        foreach my $char (@charList) {

            my $profObj;

            if (! $session->ivExists('profHash', $char)) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised character profile \'' . $char . '\'',
                );

            } else {

                $profObj = $session->ivShow('profHash', $char);
                if ($profObj->category ne 'char') {

                    return $self->error(
                        $session, $inputString,
                        '\'' . $char . '\' isn\'t a character profile',
                    );
                }
            }
        }

        # Check that any specified regions actually exist
        OUTER: foreach my $region (@regionList) {

            my $flag;

            INNER: foreach my $regionObj ($session->worldModelObj->ivValues('regionModelHash')) {

                if (lc($regionObj->name) eq lc($region)) {

                    $flag = TRUE;
                    # Compile a parallel list, which contains the blessed references of region
                    #   objects
                    push (@regionObjList, $regionObj);

                    last INNER;
                }
            }

            if (! $flag) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised region \'' . $region . '\'',
                );
            }
        }

        # Check that any specified guildnames actually exist
        foreach my $guildName (@guildNameList) {

            my $profObj;

            if (! $session->ivExists('profHash', $guildName)) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised guild profile \'' . $guildName . '\'',
                );

            } else {

                $profObj = $session->ivShow('profHash', $guildName);
                if ($profObj->category ne 'guild') {

                    return $self->error(
                        $session, $inputString,
                        '\'' . $guildName . '\' isn\'t a guild profile',
                    );
                }
            }
        }

        # (NB Any specified room flags, even those which aren't defined in the session's
        #   GA::Obj::WorldModel object, are allowed)

        # Checks complete.

        # ;mrp
        if (! $mainCount && ! @regionList) {

            my (@sortedList);

            # Get a list of regions, sorted alphabetically
            @sortedList = sort {lc($a->name) cmp lc($b->name)}
                            ($session->worldModelObj->ivValues('regionModelHash'));

            # Display header
            $session->writeText('World model general report');
            $session->writeText(' ');

            # Display list
            $session->writeText(
                '   Total objects:    '
                . $session->worldModelObj->ivPairs('modelHash'),
            );
            $session->writeText(
                '   Regions:          '
                . $session->worldModelObj->ivPairs('regionModelHash'),
            );
            $session->writeText(
                '   Rooms:            '
                . $session->worldModelObj->ivPairs('roomModelHash'),
            );
            $session->writeText(
                '   Exits:            '
                . $session->worldModelObj->ivPairs('exitModelHash'),
            );
            $session->writeText(
                '   Weapons:          '
                . $session->worldModelObj->ivPairs('weaponModelHash'),
            );
            $session->writeText(
                '   Armours:          '
                . $session->worldModelObj->ivPairs('armourModelHash'),
            );
            $session->writeText(
                '   Garments:         '
                . $session->worldModelObj->ivPairs('garmentModelHash'),
            );
            $session->writeText(
                '   Characters:       '
                . $session->worldModelObj->ivPairs('charModelHash'),
            );
            $session->writeText(
                '   Minions:          '
                . $session->worldModelObj->ivPairs('minionModelHash'),
            );
            $session->writeText(
                '   Sentients:        '
                . $session->worldModelObj->ivPairs('sentientModelHash'),
            );
            $session->writeText(
                '   Creatures:        '
                . $session->worldModelObj->ivPairs('creatureModelHash'),
            );
            $session->writeText(
                '   Portables:        '
                . $session->worldModelObj->ivPairs('portableModelHash'),
            );
            $session->writeText(
                '   Decorations:      '
                . $session->worldModelObj->ivPairs('decorationModelHash'),
            );
            $session->writeText(
                '   Custom objects:   '
                . $session->worldModelObj->ivPairs('customModelHash'),
            );
            $session->writeText(' ');

            $session->writeText(
                '   List of regions             Rooms    Exits    Others   Child regions',
            );

            foreach my $regionObj (@sortedList) {

                # Get the number of rooms in this region, the number of exits in those rooms,
                #   and the number of non-room objects in this region (including those that are
                #   children of the region itself, and those that are children of rooms in the
                #   region)
                my ($roomCount, $exitCount, $otherCount, $childRegionCount)
                    = $regionObj->countChildren($session);

                if (! defined $roomCount) {

                    # Emergency default
                    $session->writeText('      ' . $regionObj->name);

                } else {

                    # (Assume region names aren't much longer than 24 characters, although they
                    #   can be any length)
                    $session->writeText(
                        sprintf(
                            '      %-24.24s %-8.8s %-8.8s %-8.8s %-16.16s',
                            $regionObj->name,
                            $roomCount,
                            $exitCount,
                            $otherCount,
                            $childRegionCount,
                        ),
                    );
                }
            }

            # Display footer
            $session->writeText(' ');
            return $self->complete($session, $standardCmd, 'End of general report');

        # ;mrp -r <region>
        } elsif (! $mainCount && @regionList) {

            my ($regionObj, $roomCount, $exitCount, $otherCount, $hashRef);

            # Find the region object matching <region>
            $regionObj = shift @regionObjList;
            ($roomCount, $exitCount, $otherCount, $hashRef)
                = $regionObj->countChildrenCategories($session);

             # Display header
            $session->writeText('World model report for region \'' . $regionObj->name . '\'');
            $session->writeText(' ');

            # Display list
            $session->writeText(
                '   Total objects:    ' . ($roomCount + $exitCount + $otherCount),
            );

            $session->writeText('   Rooms:            ' . $roomCount);
            $session->writeText('   Exits:            ' . $exitCount);
            $session->writeText('   Weapons:          ' . $$hashRef{'weapon'});
            $session->writeText('   Armours:          ' . $$hashRef{'armour'});
            $session->writeText('   Garments:         ' . $$hashRef{'garment'});
            $session->writeText('   Characters:       ' . $$hashRef{'character'});
            $session->writeText('   Minions:          ' . $$hashRef{'minion'});
            $session->writeText('   Sentients:        ' . $$hashRef{'sentient'});
            $session->writeText('   Creatures:        ' . $$hashRef{'creature'});
            $session->writeText('   Portables:        ' . $$hashRef{'portable'});
            $session->writeText('   Decorations:      ' . $$hashRef{'decoration'});
            $session->writeText('   Custom objects:   ' . $$hashRef{'custom'});

            # Display footer
            $session->writeText(' ');
            return $self->complete($session, $standardCmd, 'End of region report');
        }

        # If no regions were specified, use them all
        if (! @regionObjList) {

            @regionObjList = sort {lc($a->name) cmp lc($b->name)}
                                ($session->worldModelObj->ivValues('regionModelHash'));

        # If regions were specified, put them in alphabetical order
        } else {

            @regionObjList = sort {lc($a->name) cmp lc($b->name)} (@regionObjList);
        }

        # ;mrp -v
        # ;mrp -v -r <region>
        # ;mrp -v -c <character>
        # ;mrp -v -c <character> -r <region>
        if ($visitsFlag) {

            # ;mrp -v
            # ;mrp -v -r <region>
            if (! @charList) {

                my ($totalRoomCount, $totalDummyCount, $totalNoVisitCount);

                # Initialise counts
                $totalRoomCount = 0;
                $totalDummyCount = 0;
                $totalNoVisitCount = 0;

                 # Display header
                $session->writeText('World model character visit report');
                $session->writeText(' ');

                # Display list
                foreach my $regionObj (@regionObjList) {

                    my ($roomCount, $dummyCount, $noVisitCount, $visitHashRef, $string);

                    ($roomCount, $dummyCount, $noVisitCount, $visitHashRef)
                        = $regionObj->countVisits($session);

                    $totalRoomCount += $roomCount;
                    $totalDummyCount += $dummyCount;
                    $totalNoVisitCount += $noVisitCount;

                    $string = '   \'' . $regionObj->name . '\' (' . $roomCount . ')';
                    if ($dummyCount) {

                        $string .= ' [dummy rooms: ' . $dummyCount . ']';
                    }

                    $session->writeText($string);

                    # Get a sorted list of characters who have visited this region
                    @charList = sort {lc($a) cmp lc($b)} (keys %$visitHashRef);
                    foreach my $char (@charList) {

                        $session->writeText(
                            '      ' . sprintf('%-16.16s : %-6.6s (', $char, $$visitHashRef{$char})
                            . $self->getPercentage($$visitHashRef{$char}, $roomCount) . ')',
                        );
                    }

                    if ($noVisitCount) {

                        $session->writeText('      (Never visited)  : ' . $noVisitCount);
                    }
                }

                # Display footer
                $session->writeText(' ');

                return $self->complete(
                    $session, $standardCmd,
                    'End of visit report (regions: ' . scalar @regionObjList . ', rooms: '
                    . $totalRoomCount . ', dummy rooms: ' . $totalDummyCount . ', never visited: '
                    . $totalNoVisitCount . ')',
                );

            # ;mrp -v -c <character>
            # ;mrp -v -c <character> -r <region>
            } else {

                # Sort the list of characters
                @charList = sort {lc($a) cmp lc($b)} (@charList);

                 # Display header
                $session->writeText('World model character visit report');
                $session->writeText(' ');

                OUTER: foreach my $char (@charList) {

                    my (
                        $totalRoomCount, $totalDummyCount, $totalVisitCount, $totalNoVisitCount,
                        $regionSomeCount, $regionAllCount, $regionNoCount,
                    );

                    # Initialise counts
                    $totalRoomCount = 0;
                    $totalDummyCount = 0;
                    $totalVisitCount = 0;
                    $totalNoVisitCount = 0;
                    $regionSomeCount = 0;
                    $regionAllCount = 0;
                    $regionNoCount = 0;

                    $session->writeText('   Visits for \'' . $char . '\'');

                    INNER: foreach my $regionObj (@regionObjList) {

                        my ($roomCount, $dummyCount, $visitCount, $noVisitCount, $string);

                        ($roomCount, $dummyCount, $visitCount, $noVisitCount)
                            = $regionObj->countCharVisits($session, $char);

                        $totalRoomCount += $roomCount;
                        $totalDummyCount += $dummyCount;
                        $totalVisitCount += $visitCount;
                        $totalNoVisitCount += $noVisitCount;

                        if (! $visitCount) {
                            $regionNoCount++;
                        } elsif ($visitCount < $roomCount) {
                            $regionSomeCount++;
                        } else {
                            $regionAllCount++;
                        }

                        $string = '      \'' . $regionObj->name . '\' (' . $roomCount . ')';
                        if ($dummyCount) {

                            $string .= ' [dummy rooms: ' . $dummyCount . ']';
                        }

                        $session->writeText($string);

                        if ($visitCount == $roomCount) {

                            $session->writeText('         All rooms visited (100%)');

                        } else {

                            $session->writeText(
                                '         Visited: ' . $visitCount . ', not visited: '
                                . $noVisitCount . ' ('
                                . $self->getPercentage($visitCount, $roomCount) . ')',
                            );
                        }

                    }

                    $session->writeText(' ');
                    $session->writeText(
                        '      Rooms: ' . $totalRoomCount . ', dummy rooms: ' . $totalDummyCount
                        . ', visited: ' . $totalVisitCount . ', never visited: '
                        . $totalNoVisitCount,
                    );
                    $session->writeText(
                        '      Regions: ' . scalar @regionObjList . ', visited all rooms: '
                        . $regionAllCount . ', visited some: ' . $regionSomeCount
                        . ', visisted none: ' . $regionNoCount,
                    );
                    $session->writeText(' ')
                }

                # Display footer
                # (line break already displayed)
                return $self->complete($session, $standardCmd, 'End of visit report');
            }

        # ;mrp -g
        # ;mrp -g -r <region>
        # ;mrp -g -n <guild>
        # ;mrp -g -r <region> -n <guild>
        } elsif ($guildsFlag) {

            my (
                @sortedGuildList,
                %combinedHash, %locationHash,
            );

            # Display header
            $session->writeText('World model guild report');
            $session->writeText(' ');

            # Display list
            foreach my $regionObj (@regionObjList) {

                my ($roomCount, $guildCount, $guildHashRef)
                    = $regionObj->countGuilds($session, @guildNameList);

                # ;mrp -g
                # ;mrp -g -r <region>
                if (! @guildNameList) {

                    $session->writeText(
                        '   \'' . $regionObj->name . '\' (guild rooms: '
                        . $guildCount . '/' . $roomCount . ')',
                    );

                    # Get a sorted list of guild names in this region
                    @sortedGuildList = sort {lc($a) cmp lc($b)} (keys %$guildHashRef);
                    if (@sortedGuildList) {

                        foreach my $guild (@sortedGuildList) {

                            $session->writeText(
                                sprintf('      %-16.16s', $guild) . ' : ' . $$guildHashRef{$guild},
                            );
                        }
                    }
                }

                foreach my $guild (keys %$guildHashRef) {

                    my $string;

                    # Add the returned hash to the combined hash
                    if (exists $combinedHash{$guild}) {
                        $combinedHash{$guild} = $combinedHash{$guild} + $$guildHashRef{$guild};
                    } else {
                        $combinedHash{$guild} = $$guildHashRef{$guild};
                    }

                    # Add the region to the location hash
                    $string = $regionObj->name;
                    if ($$guildHashRef{$guild} > 1) {

                        # e.g. 'castle x2'
                        $string .= ' x' . $$guildHashRef{$guild};
                    }

                    if (exists $locationHash{$guild}) {
                        $locationHash{$guild} = $locationHash{$guild} . ', ' . $string;
                    } else {
                        $locationHash{$guild} = $string;
                    }
                }
            }

            if (! @guildNameList) {

                $session->writeText(' ');
            }

            if (%combinedHash) {

                # Get a sorted list of all guilds found
                @sortedGuildList = sort {lc($a) cmp lc($b)} (keys %combinedHash);

                if (! @guildNameList) {
                    $session->writeText('   Combined guild rooms:');
                } else {
                    $session->writeText('   Specified guild rooms:');
                }

                foreach my $guild (@sortedGuildList) {

                    $session->writeText(
                        sprintf('      %-16.16s', $guild) . ' : ' . $combinedHash{$guild}
                        . ' (' . $locationHash{$guild} . ')',
                    );
                }
            }

            # Display footer
            $session->writeText(' ');
            return $self->complete($session, $standardCmd, 'End of guild report');

        # ;mrp -f
        # ;mrp -f -r <region>
        # ;mrp -f -l <flag>
        # ;mrp -f -r <region> -l <flag>
        } elsif ($roomFlagsFlag) {

            my (
                @sortedRoomFlagList,
                %combinedHash, %locationHash,
            );

            # Display header
            $session->writeText('World model room flag report');
            $session->writeText(' ');

            # Display list
            foreach my $regionObj (@regionObjList) {

                my ($roomCount, $roomFlagCount, $roomFlagHashRef)
                    = $regionObj->countRoomFlags($session, @roomFlagList);

                # ;mrp -f
                # ;mrp -f -r <region>
                if (! @roomFlagList) {

                    $session->writeText(
                        '   Region \'' . $regionObj->name . '\' (total rooms with flags: '
                        . $roomFlagCount . '/' . $roomCount . ')',
                    );

                    # Get a sorted list of room flags in this region
                    @sortedRoomFlagList = sort {lc($a) cmp lc($b)} (keys %$roomFlagHashRef);
                    if (@sortedRoomFlagList) {

                        foreach my $flag (@sortedRoomFlagList) {

                            $session->writeText(
                                sprintf('      %-16.16s', $flag) . ' : ' . $$roomFlagHashRef{$flag},
                            );
                        }

                    } else {

                        $session->writeText('      (no rooms with flags)');
                    }
                }

                foreach my $flag (keys %$roomFlagHashRef) {

                    my $string;

                    # Add the returned hash to the combined hash
                    if (exists $combinedHash{$flag}) {
                        $combinedHash{$flag} = $combinedHash{$flag} + $$roomFlagHashRef{$flag};
                    } else {
                        $combinedHash{$flag} = $$roomFlagHashRef{$flag};
                    }

                    # Add the region to the location hash
                    $string = $regionObj->name;
                    if ($$roomFlagHashRef{$flag} > 1) {

                        # e.g. 'castle x2'
                        $string .= ' x' . $$roomFlagHashRef{$flag};
                    }

                    if (exists $locationHash{$flag}) {
                        $locationHash{$flag} = $locationHash{$flag} . ', ' . $string;
                    } else {
                        $locationHash{$flag} = $string;
                    }
                }
            }

            if (%combinedHash) {

                if (! @roomFlagList) {

                    $session->writeText(' ');
                }

                # Get a sorted list of all room flags found
                @sortedRoomFlagList = sort {lc($a) cmp lc($b)} (keys %combinedHash);

                if (! @roomFlagList) {
                    $session->writeText('   Combined rooms with flags:');
                } else {
                    $session->writeText('   Rooms with specified flags:');
                }

                foreach my $flag (@sortedRoomFlagList) {

                    $session->writeText(
                        sprintf('      %-16.16s', $flag) . ' : ' . $combinedHash{$flag}
                    );
                    $session->writeText('         (' . $locationHash{$flag} . ')',);
                }
            }

            # Display footer
            $session->writeText(' ');
            return $self->complete($session, $standardCmd, 'End of room flag report');

        # ;mrp -m
        # ;mrp -m -r <region>
        } elsif ($roomsFlag) {

            my ($roomTotal, $bothTotal, $titleTotal, $descripTotal, $noneTotal);

            # Initialise counts
            $roomTotal = 0;
            $bothTotal = 0;     # Both a title and verbose descrip
            $titleTotal = 0;    # Title only
            $descripTotal = 0;  # Descrip only
            $noneTotal = 0;     # Neither title nor descrip

            # Display header
            $session->writeText('World model room report');
            $session->writeText(' ');

            # Display list
            foreach my $regionObj (@regionObjList) {

                my (
                    $roomCount, $bothCount, $titleCount, $descripCount, $noneCount
                ) = $regionObj->countRooms($session);

                $roomTotal += $roomCount;
                $bothTotal += $bothCount;
                $titleTotal += $titleCount;
                $descripTotal += $descripCount;
                $noneTotal += $noneCount;

                $session->writeText(
                    '   Region \'' . $regionObj->name . '\' (total rooms: ' . $roomCount . ')',
                );

                $session->writeText(
                    '      Both title(s) & descrip(s) : ' . $bothCount . '    ('
                    . $self->getPercentage ($bothCount, $roomCount) . ')',
                );

                if ($titleCount) {

                    $session->writeText('      Title(s) only              : ' . $titleCount);
                }

                if ($descripCount) {

                    $session->writeText('      Descrip(s) only            : ' . $descripCount);
                }

                if ($noneCount) {

                    $session->writeText('      Neither title nor descrip  : ' . $noneCount);
                }

                $session->writeText(' ');
            }

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of room report (total rooms: ' . $roomTotal . ', both title and descrip: '
                . $bothTotal . ', title(s) only: ' . $titleTotal . ', descrip(s) only: '
                . $descripTotal . ', neither title nor descrip: ' . $noneTotal . ')',
            );

        # ;mrp -x
        # ;mrp -x -r <region>
        } elsif ($exitsFlag) {

            my (
                $exitTotal, $unallocatedTotal, $unallocatableTotal, $uncertainTotal,
                $incompleteTotal, $incompleteImpassTotal, $incompleteMysteryTotal,
            );

            # Initialise counts
            $exitTotal = 0;
            $unallocatedTotal = 0;
            $unallocatableTotal = 0;
            $uncertainTotal = 0;
            $incompleteTotal = 0;           # Incomplete, not impassable
            $incompleteImpassTotal = 0;     # Incomplete and impassable

            # Display header
            $session->writeText('World model exit report');
            $session->writeText(' ');

            # Display list
            foreach my $regionObj (@regionObjList) {

                my (
                    $exitCount, $unallocatedCount, $unallocatableCount, $uncertainCount,
                    $incompleteCount, $incompImpassCount, $incompMysteryCount,
                ) = $regionObj->countExits($session);

                $exitTotal += $exitCount;
                $unallocatedTotal += $unallocatedCount;
                $unallocatableTotal += $unallocatableCount;
                $uncertainTotal += $uncertainCount;
                $incompleteTotal += $incompleteCount;
                $incompleteImpassTotal += $incompImpassCount;
                $incompleteMysteryTotal += $incompMysteryCount;

                $session->writeText(
                    '   Region \'' . $regionObj->name . '\' (total exits: ' . $exitCount . ')',
                );

                if ($unallocatedCount) {

                    $session->writeText('      Unallocated             : ' . $unallocatedCount);
                }

                if ($unallocatableCount) {

                    $session->writeText('      Unallocatable           : ' . $unallocatableCount);
                }

                if ($uncertainCount) {

                    $session->writeText('      Uncertain               : ' . $uncertainCount);
                }

                if ($incompleteCount) {

                    $session->writeText('      Incomplete              : ' . $incompleteCount);
                }

                if ($incompImpassCount) {

                    $session->writeText('      Incomplete (impassable) : ' . $incompImpassCount);
                }

                if ($incompImpassCount) {

                    $session->writeText('      Incomplete (mystery)    : ' . $incompMysteryCount);
                }

                if (
                    ! $unallocatedCount && ! $unallocatableCount && ! $uncertainCount
                    && ! $incompleteCount && ! $incompImpassCount && ! $incompMysteryCount
                ) {
                    $session->writeText('      (no unfinished exits)');
                }

                $session->writeText(' ');
            }

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of exit report (total exits: ' . $exitTotal . ', unallocated: '
                . $unallocatedTotal . ', unallocatable: ' . $unallocatableTotal . ', uncertain: '
                . $uncertainTotal . ', incomplete: ' . $incompleteTotal
                . ', incomplete/impassable: ' . $incompleteImpassTotal . ', incomplete/mystery: '
                . $incompleteMysteryTotal . ')',
            );

        # ;mrp -h
        # ;mrp -h -r <region>
        } elsif ($checkedFlag) {

            my (
                $exitTotal, $checkedTotal, $checkableTotal,
                %dirHash,
            );

            # The number of checkable directions depends on GA::Obj::WorldModel->checkableDirMode,
            #   and specifies directions ranging from NSEW to all primary directions
            # Compile a hash of checkable directions, in the form
            #   %hash{custom_primary_direction} = undef
            %dirHash = $session->worldModelObj->getCheckableDirs($session);

            # Display header
            $session->writeText('World model checked direction report');
            $session->writeText(' ');

            # Initialise counts
            $exitTotal = 0;
            $checkedTotal = 0;
            $checkableTotal = 0;

            # Display list
            foreach my $regionObj (@regionObjList) {

                my ($exitCount, $checkedCount, $checkableCount)
                    = $regionObj->countCheckedDirs($session, %dirHash);

                $exitTotal += $exitCount;
                $checkedTotal += $checkedCount;
                $checkableTotal += $checkableCount;

                $session->writeText('   Region \'' . $regionObj->name . '\'');

                $session->writeText('      World model exits       : ' . $exitCount);
                $session->writeText('      Checked directions      : ' . $checkedCount);
                $session->writeText('      Checkable directions    : ' . $checkableCount);
            }

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of checked direction report (total world model exits: ' . $exitTotal
                . ', checked directions: ' . $checkedTotal . ', checkable directions: '
                . $checkableTotal . ')',
            );

        # ;mrp -q
        } elsif ($quickFlag) {

            # Display header
            $session->writeText('World model exit report');
            $session->writeText(' ');


            my (
                $regionmap, $visitCount, $titleCount, $verbCount,
                @roomList, @argList,
                %selectHash,
            );

            # Initialise counts
            $visitCount = 0;
            $titleCount = 0;
            $verbCount = 0;

            # Display header
            $regionmap = $session->mapWin->currentRegionmap;
            $session->writeText('World model quick report, region \'' . $regionmap->name . '\'');
            $session->writeText(' ');

            # Unselect everything in the Automapper window
            $session->mapWin->setSelectedObj();

            # Get a sorted list of room numbers in the current region
            @roomList = $regionmap->ivValues('gridRoomHash');
            @roomList = sort {$a <=> $b} (@roomList);

            # For each room, check whether there's a title, a verbose description, and whether the
            #   current character has visited it
            foreach my $roomNum (@roomList) {

                my $obj = $session->worldModelObj->ivShow('modelHash', $roomNum);

                if ($obj) {

                    if (! $obj->ivShow('visitHash', $session->currentChar->name)) {

                        # Unvisited room
                        $visitCount++;
                        $session->writeText('   Room never visited             #' . $obj->number);
                        # Mark the room to be selected
                        $selectHash{$roomNum} = $obj;
                    }

                    if (! $obj->titleList && ! $obj->descripHash) {

                        # Room with no title or verbose descrip
                        $titleCount++;
                        $verbCount++;
                        $session->writeText('   Room without title/descrip     #' . $obj->number);
                        # Mark the room to be selected
                        $selectHash{$roomNum} = $obj;

                    } elsif (! $obj->titleList) {

                        # Room with no title
                        $titleCount++;
                        $session->writeText('   Room without title             #' . $obj->number);
                        # Mark the room to be selected
                        $selectHash{$roomNum} = $obj;

                    } elsif (! $obj->descripHash) {

                        # Room with no verbose descrip
                        $verbCount++;
                        $session->writeText('   Room without verbose descrip    #' . $obj->number);
                        # Mark the room to be selected
                        $selectHash{$roomNum} = $obj;
                    }
                }
            }

            # Select any rooms that were marked for selection
            foreach my $obj (values %selectHash) {

                push (@argList, $obj, 'room');
            }

            $session->mapWin->setSelectedObj(\@argList, TRUE);

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'End of quick report (unvisited rooms: ' . $visitCount . ', rooms without titles: '
                . $titleCount . ', rooms without verbose descriptions: ' . $verbCount . ')',
            );
        }
    }

    sub getPercentage {

        # Called by $self->do for ';mrp -v' (etc)
        # Returns a string showing the number of rooms visited, as a (rounded) percentage
        #
        # Expected arguments
        #   $visits     - The number of rooms visited by a character in a region
        #   $total      - The number of rooms in a region
        #
        # Return values
        #   'undef' on improper arguments
        #   Otherwise returns a string to display, e.g. '60%'

        my ($self, $visits, $total, $check) = @_;

        # Local variables
        my $percentage;

        # Check for improper arguments
        if (! defined $visits || ! defined $total || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->getPercentage', @_);
        }

        if (! $visits) {

            return '0%';

        } elsif ($visits == $total) {

            return '100%';

        } else {

            $percentage = int(($visits/$total) * 100);

            # If at least one room has been visited, but enough rooms to comprise 1%...
            if (! $percentage) {
                return '< 1%';
            } else {
                return $percentage . '%';
            }
        }
    }
}

{ package Games::Axmud::Cmd::ListSourceCode;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listsourcecode', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lso', 'listsource', 'listsourcecode'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Displays mudlib file paths for world model objects';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $sortFlag, $allObjFlag, $thisRegionObj,
            @regionList, @childList, @sortedList,
        );

        # Extract switches
        ($switch, @args) = $self->extract('-n', 0, @args);
        if (defined $switch) {

            $sortFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-a', 0, @args);
        if (defined $switch) {

            $allObjFlag = TRUE;
        }

        # Everything else in @args is a region name. If no region has been specified, use the
        #   Automapper window's current region (if open)
        if (! @args) {

            if ($session->mapWin && $session->mapWin->currentRegionmap) {

                push (@args, $session->mapWin->currentRegionmap->name);

            } else {

                return $self->error(
                    $session, $inputString,
                    'Please specify a region name',
                );
            }
        }

        # Check that every region in @args actually exists, and compile a list of region objects
        foreach my $regionName (@args) {

            my $regionmapObj;

            if (! $session->worldModelObj->ivExists('regionmapHash', $regionName)) {

                return $self->error(
                    $session, $inputString,
                    'Unrecognised region name \'' . $regionName . '\'',
                );

            } else {

                $regionmapObj = $session->worldModelObj->ivShow('regionmapHash', $regionName);
                push (
                    @regionList,
                    $session->worldModelObj->ivShow('modelHash', $regionmapObj->number),
                );
            }
        }

        # Get a list of all the regions' child objects ->sourceCodePath set. If $allObjFlag is TRUE,
        #   include all of the regions' children (except other regions); otherwise, include only
        #   rooms
        foreach my $regionObj (@regionList) {

            foreach my $childNum ($regionObj->ivKeys('childHash')) {

                my $childObj = $session->worldModelObj->ivShow('modelHash', $childNum);

                if (
                    $childObj->sourceCodePath
                    && (
                        ($allObjFlag && $childObj->category ne 'region')
                        || $childObj->category eq 'room'
                    )
                ) {
                    push (@childList, $childObj);
                }
            }
        }

        if (! @childList) {

            return $self->error(
                $session, $inputString,
                'None of the objects in the region(s) have their source code file path set',
            );
        }

        # Sort the list, numerically or by source code path
        if ($sortFlag) {
            @sortedList = sort {$a->number <=> $b->number} (@childList);
        } else {
            @sortedList = sort {lc($a->sourceCodePath) cmp lc($b->sourceCodePath)} (@childList);
        }

        if (! @sortedList) {

            return $self->complete($session, $standardCmd, 'The specified list is empty');
        }

        # Display header
        if (@regionList == 1) {

            $thisRegionObj = $regionList[0];
            $session->writeText(
                'List of source code paths for region \'' . $thisRegionObj->name . '\'',
            );

        } else {

            $session->writeText(
                'List of source code paths for \'' . scalar @sortedList . '\' regions',
            );
        }

        $session->writeText('   Number   Type         Source code path');

        # Display list
        foreach my $childObj (@sortedList) {

            $session->writeText(
                sprintf('   %-8.8s %-12.12s ', $childObj->number, $childObj->category)
                . $childObj->sourceCodePath,
            );
        }

        # Display footer
        if (@sortedList == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 child object found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @sortedList . ' child objects found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddRegionScheme;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addregionscheme', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ars', 'addrs', 'addregionscheme'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a region colour scheme';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check $name is not too long
        if (length $name > 16) {

            return $self->error(
                $session, $inputString,
                'Invalid region colour scheme name (can contain any characters, but maximum length'
                . ' is 16)',
            );

        # Check a style with that name doesn't already exist
        } elsif ($session->worldModelObj->ivExists('regionSchemeHash', $name)) {

            return $self->error(
                $session, $inputString,
                'There is already a region colour scheme called \'' . $name
                . '\' in the world model',
            );
        }

        # Create the region scheme object
        $obj = $session->worldModelObj->addRegionScheme($session, $name);

        # Operation complete
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Could not add region colour scheme \'' . $name . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Added region colour scheme \'' . $name . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditRegionScheme;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editregionscheme', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ers', 'editrs', 'editregionscheme'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a region colour scheme';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the region scheme exists
        $obj = $session->worldModelObj->ivShow('regionSchemeHash', $name);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'The world model doesn\'t contain a region colour scheme called \'' . $name . '\'',
            );
        }

        # Open an \'edit\' window for the region scheme object
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::RegionScheme',
                $session->mainWin,
                $session,
                'Edit region colour scheme \'' . $name . '\'',
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the region colour scheme \'' . $name . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the region colour scheme \'' . $name . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::RenameRegionScheme;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('renameregionscheme', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rrs', 'renamers', 'renameregionscheme'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Renames a region colour scheme';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $oldName, $newName,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $oldName || ! defined $newName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that the old name exists (no reason why it shouldn't, but we'll check anyway)
        if (! $session->worldModelObj->ivExists('regionSchemeHash', $oldName)) {

            return $self->error(
                $session, $inputString,
                'A region colour scheme called \'' . $oldName . '\' doesn\'t exist',
            );

        # ...and that the new name doesn't already exist
        } elsif ($session->worldModelObj->ivExists('regionSchemeHash', $newName)) {

            return $self->error(
                $session, $inputString,
                'A region colour scheme called \'' . $newName . '\' already exists',
            );

        # ...and is not too long
        } elsif (length $newName > 16) {

            return $self->error(
                $session, $inputString,
                'Invalid region colour scheme name (can contain any characters, but maximum length'
                . ' is 16)',
            );

        } elsif ($oldName eq 'default') {

            return $self->error(
                $session, $inputString,
                'The default region colour scheme cannot be renamed',
            );

        } elsif (! $session->worldModelObj->renameRegionScheme($session, $oldName, $newName)) {

            return $self->error(
                $session, $inputString,
                'Failed to rename the region colour scheme \'' . $oldName . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Region colour scheme \'' . $oldName . '\' renamed as \'' . $newName . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteRegionScheme;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteregionscheme', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['drs', 'delrs', 'deleters', 'deleteregionscheme'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a region colour scheme';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the region colour scheme exists, and is deleteable
        $obj = $session->worldModelObj->ivShow('regionSchemeHash', $name);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'The world model doesn\'t contain a region colour scheme called \'' . $name . '\'',
            );

        } elsif ($name eq 'default') {

            return $self->error(
                $session, $inputString,
                'The default region colour scheme cannot be deleted',
            );

        } else {

            # Delete the scheme
            $session->worldModelObj->deleteRegionScheme(
                TRUE,           # Update automapper windows now
                $name,
            );

            return $self->complete(
                $session, $standardCmd,
                'Region colour scheme \'' . $name . '\' deleted',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListRegionScheme;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listregionscheme', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lrs', 'listrs', 'listregionscheme'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists region colour schemes';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            @schemeList,
            %schemeHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # (Very unlikely, because the 'default' scheme should be undeletable)
        if (! $session->worldModelObj->regionSchemeHash) {

            return $self->complete(
                $session, $standardCmd,
                'The world model contains no region colour schemes',
            );
        }

        # Compile a hash of region schemes and the regions that use them, in the form
        #   $schemeHash{scheme_name} = reference_to_list_of_region_names
        foreach my $schemeObj ($session->worldModelObj->ivValues('regionSchemeHash')) {

            $schemeHash{$schemeObj->name} = [];
        }

        foreach my $regionmapObj ($session->worldModelObj->ivValues('regionmapHash')) {

            my ($schemeName, $listRef);

            if (! defined $schemeName) {
                $schemeName = 'default';
            } else {
                $schemeName = $regionmapObj->regionScheme;
            }

            if (! exists $schemeHash{$schemeName}) {

                $schemeHash{$schemeName} = [ $regionmapObj->name ];

            } else {

                $listRef = $schemeHash{$schemeName};
                push (@$listRef, $regionmapObj->name);
            }
        }

        # Display header
        $session->writeText(
            'List of region colour scheme (* - default scheme, cannot be deleted)',
        );
        $session->writeText('   Name             Used by');

        # Display list
        @schemeList = sort {lc($a) cmp lc($b)} (keys %schemeHash);
        foreach my $schemeName (@schemeList) {

            my (
                $column, $listRef, $region,
                @regionList,
            );

            # (The default region scheme should be named 'default', but we'll check anyway)
            if (
                defined $session->worldModelObj->defaultSchemeObj
                && $session->worldModelObj->defaultSchemeObj->name eq $schemeName
            ) {
                $column = ' * ';
            } else {
                $column = '   ';
            }

            $listRef = $schemeHash{$schemeName};
            @regionList = sort {lc($a) cmp lc($b)} (@$listRef);

            # First region (if any) goes on the same line as the scheme name. Subsequent regions (if
            #   any) go on their own lines
            if (! @regionList) {
                $region = '<none>';
            } else {
                $region = shift @regionList;
            }

            $session->writeText(
                $column . sprintf('%-16.16s ', $schemeName) . $region,
            );

            foreach my $otherRegion (@regionList) {

                $session->writeText('                    ' . $otherRegion);
            }
        }

        # Display footer
        if (@schemeList == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 region colour scheme found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . @schemeList . ' region colour schemes found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddLabelStyle;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addlabelstyle', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['als', 'addstyle', 'addlabelstyle'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a map label style';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name, $text, $underlay,
            $check,
        ) = @_;

        # Local variables
        my ($type, $underlayFlag, $obj);

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check $name is not too long
        if (length $name > 16) {

            return $self->error(
                $session, $inputString,
                'Invalid label style name (can contain any characters, but maximum length is 16)',
            );

        # Check a style with that name doesn't already exist
        } elsif ($session->worldModelObj->ivExists('mapLabelStyleHash', $name)) {

            return $self->error(
                $session, $inputString,
                'There is already a label style called \'' . $name . '\' in the world model',
            );
        }

        # If $text and $underlay are specified, check they're valid, and convert to am RGB colour
        #   tag
        if (defined $text) {

            ($type, $underlayFlag) = $axmud::CLIENT->checkColourTags($text);
            if (! $type) {

                return $self->error(
                    $session, $inputString,
                    'Invalid ' . $axmud::SCRIPT . ' colour tag \'' . $text . '\'',
                );

            } else {

                if ($underlayFlag) {

                    # Convert an underlay tag to a text tag (the GooCanvas2::Canvas doesn't care
                    #   about the difference between Axmud text/underlay colour tags)
                    $text = $axmud::CLIENT->swapColours($text);
                }

                # Convert to an RGB colour tag
                $text = $axmud::CLIENT->returnRGBColour($text);
            }
        }

        if (defined $underlay) {

            ($type) = $axmud::CLIENT->checkColourTags($underlay);
            if (! $type) {

                return $self->error(
                    $session, $inputString,
                    'Invalid ' . $axmud::SCRIPT . ' colour tag \'' . $underlay . '\'',
                );

            } else {

                if ($underlayFlag) {

                    $underlay = $axmud::CLIENT->swapColours($underlay);
                }

                $underlay = $axmud::CLIENT->returnRGBColour($underlay);
            }
        }

        # Create the map label style object
        $obj = $session->worldModelObj->addLabelStyle($session, $name, $text, $underlay);

        # Operation complete
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Could not add label style \'' . $name . '\'',
            );

        } else {

            return $self->complete($session, $standardCmd, 'Added label style \'' . $name . '\'');
        }
    }
}

{ package Games::Axmud::Cmd::EditLabelStyle;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editlabelstyle', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['els', 'editstyle', 'editlabelstyle'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for a map label style';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the map label style exists
        $obj = $session->worldModelObj->ivShow('mapLabelStyleHash', $name);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'The world model doesn\'t contain a map label style called \'' . $name . '\'',
            );
        }

        # Open an \'edit\' window for the style object
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::MapLabelStyle',
                $session->mainWin,
                $session,
                'Edit map label style \'' . $name . '\'',
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the map label style \'' . $name . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the map label style \'' . $name . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::RenameLabelStyle;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('renamelabelstyle', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rls', 'renamestyle', 'renamelabelstyle'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Renames a map label style';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $oldName, $newName,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $oldName || ! defined $newName || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that the old name exists (no reason why it shouldn't, but we'll check anyway)
        if (! $session->worldModelObj->ivExists('mapLabelStyleHash', $oldName)) {

            return $self->error(
                $session, $inputString,
                'A map label style called \'' . $oldName . '\' doesn\'t exist',
            );

        # ...and that the new name doesn't already exist
        } elsif ($session->worldModelObj->ivExists('mapLabelStyleHash', $newName)) {

            return $self->error(
                $session, $inputString,
                'A map label style called \'' . $newName . '\' already exists',
            );

        # ...and is not too long
        } elsif (length $newName > 16) {

            return $self->error(
                $session, $inputString,
                'Invalid label style name (can contain any characters, but maximum length is 16)',
            );

        } elsif (! $session->worldModelObj->renameLabelStyle($session, $oldName, $newName)) {

            return $self->error(
                $session, $inputString,
                'Failed to rename the map label style \'' . $oldName . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Map label style \'' . $oldName . '\' renamed as \'' . $newName . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteLabelStyle;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deletelabelstyle', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dls', 'delstyle', 'deletestyle', 'deletelabelstyle'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a map label style';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the map label style exists
        $obj = $session->worldModelObj->ivShow('mapLabelStyleHash', $name);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'The world model doesn\'t contain a map label style called \'' . $name . '\'',
            );

        } else {

            # Delete the style
            $session->worldModelObj->deleteLabelStyle(
                TRUE,           # Update automapper windows now
                $name,
            );

            return $self->complete(
                $session, $standardCmd,
                'Map label style \'' . $name . '\' deleted',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListLabelStyle;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listlabelstyle', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lls', 'liststyle', 'listlabelstyle'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists map label styles';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        if (! $session->worldModelObj->mapLabelStyleHash) {

            return $self->complete(
                $session, $standardCmd,
                'The world model contains no map label styles',
            );
        }

        # Display header
        $session->writeText(
            'List of map label styles (* - current style for new labels)',
        );
        $session->writeText(
            '   Name             Text     Underlay Size     Ital Bold Undl Strk Box  Orientation',
        );

        # Display list
        @list = sort {lc($a->name) cmp lc($b->name)}
                    ($session->worldModelObj->ivValues('mapLabelStyleHash'));

        foreach my $obj (@list) {

            my ($column, $text, $underlay, $italics, $bold, $under, $strike, $box);

            if (
                defined $session->worldModelObj->mapLabelStyle
                && $session->worldModelObj->mapLabelStyle eq $obj->name
            ) {
                $column = ' * ';
            } else {
                $column = '   ';
            }

            if (defined $obj->textColour) {
                $text = $obj->textColour;
            } else {
                $text = 'not set';
            }

            if (defined $obj->underlayColour) {
                $underlay = $obj->underlayColour;
            } else {
                $underlay = 'not set';
            }

            if (! $obj->italicsFlag) {
                $italics = 'NO   ';
            } else {
                $italics = 'YES  ';
            }

            if (! $obj->boldFlag) {
                $bold = 'NO   ';
            } else {
                $bold = 'YES  ';
            }

            if (! $obj->underlineFlag) {
                $under = 'NO   ';
            } else {
                $under = 'YES  ';
            }

            if (! $obj->strikeFlag) {
                $strike = 'NO   ';
            } else {
                $strike = 'YES  ';
            }

            if (! $obj->boxFlag) {
                $box = 'NO   ';
            } else {
                $box = 'YES  ';
            }

            $session->writeText(
                $column
                .  sprintf(
                    '%-16.16s %-8.8s %-8.8s %-8.8s ', $obj->name, $text, $underlay, $obj->relSize,
                )
                . $italics . $bold . $under . $strike . $box . $obj->rotateAngle,
            );
        }

        # Display footer
        if (@list == 1) {

            return $self->complete($session, $standardCmd, 'End of list (1 map label style found)');

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . @list . ' map label styles found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::QuickLabelDelete;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('quicklabeldelete', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['qld', 'quicklabel', 'quicklabeldelete'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens a window to quickly delete map labels';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Open an 'other' window
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::OtherWin::LabelDelete',
                $session->mainWin,
                $session,
                'Quick word adder',
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not open the label deletion window',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened the label deletion window',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddPlayerCharacter;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addplayercharacter', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['apc', 'addpc', 'addplayerchar', 'addplayercharacter'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a player character (PC) to the world model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name, $parent,
            $check,
        ) = @_;

        # Local variables
        my ($obj, $parentObj);

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # If $name is longer than 32 characters, it must be shortened
        if (length $name > 32) {

            $name = substr($name, 0, 32);
        }

        # If parent is specified, check it's a valid model object
        if (defined $parent && ! $session->worldModelObj->ivExists('modelHash', $parent)) {

            return $self->error(
                $session, $inputString,
                'The parent #' . $parent . ' isn\'t the number of an existing world model object',
            );
        }

        # Check that $name isn't already a known character; multiple character objects with the same
        #   name aren't allowed
        if ($session->worldModelObj->ivExists('knownCharHash', $name)) {

            return $self->error(
                $session, $inputString,
                'There is already a player character called \'' . $name . '\' in the world model'
                . ' (duplicate player characters are not allowed)',
            );
        }

        # Create the character model object
        $obj = $session->worldModelObj->addChar(
            $session,
            TRUE,       # Update Automapper windows
            $name,
            $parent,
        );

        # Operation complete
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'Could not add player character \'' . $name . '\'',
            );

        } elsif ($parent) {

            $parentObj = $session->worldModelObj->ivShow('modelHash', $parent);

            return $self->complete(
                $session, $standardCmd,
                'Added player character, model #' . $obj->number . ' \'' . $name
                . '\' (parent #' . $parent . ' \'' . $parentObj->name . '\')',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Added player character, model #' . $obj->number . ' \'' . $name . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeletePlayerCharacter;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteplayercharacter', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dpc', 'delpc', 'deletepc', 'deleteplayercharacter'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes a player character (PC) from the model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my ($obj, $result);

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that $name is a known player character, and gets its model object
        if (! $session->worldModelObj->ivExists('knownCharHash', $name)) {

            return $self->error(
                $session, $inputString,
                'There is no player character called \'' . $name . '\' in the world model',
            );

        } else {

            $obj = $session->worldModelObj->ivShow('knownCharHash', $name);
        }

        # Delete the room
        $result = $session->worldModelObj->deleteObj(
            $session,
            TRUE,              # Update Automapper windows now
            $obj,
        );

        if (! $result) {

            return $self->error(
                $session, $inputString,
                'Delete model object operation failed',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'The model object #' . $obj->number . ' \'' . $obj->name . '\' has'
                . ' been deleted',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListPlayerCharacter;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listplayercharacter', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lpc', 'listpc', 'listplayerchar', 'listplayercharacter'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists player characters (PCs) in the world model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $string,
            @list,
        );

        # Check for improper arguments
        if ((defined $switch && $switch ne '-a') || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Get a list of characters and sort them numerically or alphabetically
        @list = $session->worldModelObj->ivValues('knownCharHash');
        if ($switch) {
            @list = sort {lc($a->name) cmp lc($b->name)} @list;
        } else {
            @list = sort {$a->number <=> $b->number} @list;
        }

        if (! @list) {

            return $self->complete(
                $session, $standardCmd,
                'The world model\'s player character list is empty',
            );
        }

        # Display header
        $session->writeText('World model character list (* = own character)');
        $session->writeText('   Number    Character name                   Parent');

        # Display list
        foreach my $obj (@list) {

            if ($obj->ownCharFlag) {
                $string = ' * #';
            } else {
                $string = '   #';
            }

            if ($obj->parent) {

                $session->writeText(
                    $string . sprintf(
                        '%-8.8s %-32.32s #%-8.8s',
                        $obj->number,
                        $obj->name,
                        $obj->parent,
                    )
                );

            } else {

                $session->writeText(
                    $string . sprintf(
                        '%-8.8s %-32.32s (none)',
                        $obj->number,
                        $obj->name,
                    )
                );
            }
        }

        # Display footer
        if (@list == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of list (1 player character found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of list (' . scalar @list . ' player characters found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AddMinionString;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addminionstring', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ams', 'addminionstring'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds a minion string to the world model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $number, $modelFlag, $otherFlag, $addFlag, $string, $obj,
            @objList,
        );

        # Extract the optional switches
        ($switch, $number, @args) = $self->extract('-m', 1, @args);
        if (defined $switch) {

            $modelFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-o', 0, @args);
        if (defined $switch) {

            $otherFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-a', 0, @args);
        if (defined $switch) {

            $addFlag = TRUE;
        }

        # Extract the remaining arguments
        $string = shift @args;
        # There should be nothing left in @args
        if (! defined $string || @args) {

            return $self->improper($session, $inputString);
        }

        # The switches -m and -a can't be combined
        if ($modelFlag && $addFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -m and -a can\'t be combined',
            );
        }

        # ;ams <string>
        # ;ams <string> -o
        # ;ams <string> -a
        # ;ams <string> -a -o
        if (! $modelFlag) {

            # Parse the string into a non-model object
            @objList = $session->worldModelObj->parseObj(
                $session,
                FALSE,      # Treat 5 coins as 5 separate objects
                $string,
            );

            if (! @objList) {

                return $self->error(
                    $session, $inputString,
                    'Unable to parse the minion string \'' . $string . '\'',
                );

            } elsif (@objList > 1) {

                return $self->error(
                    $session, $inputString,
                    'After parsing, the minion string \'' . $string . '\' seems to represent'
                    . scalar @objList . ' objects; try using a shorter string',
                );
            }

            # Got a non-model object
            $obj = shift @objList;
            # $obj could be a decoration, sentient or a creature. Convert it into a minion
            $obj = $session->worldModelObj->convertCategory(
                $session,
                FALSE,      # Don't update Automapper windows
                $obj,
                'minion',
            );

            if (! $obj) {

                return $self->error(
                    $session, $inputString,
                    'General error parsing the string',
                );
            }

            # Set its owner
            if (! $otherFlag) {

                $obj->ivPoke('ownMinionFlag', TRUE);
            }

            if ($addFlag) {

                # Add the non-model object to the model
                $session->worldModelObj->importOther(
                    $session,
                    FALSE,      # Don't update Automapper windows
                    $obj,
                );
            }

            # Add the minion string to the world model
            $session->worldModelObj->ivAdd('minionStringHash', $string, $obj);

            if (! $addFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Added the minion string \'' . $string . '\' corresponding to a non-model'
                    . ' minion object',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Added the minion string \'' . $string . '\' corresponding to the new minion'
                    . ' object #' . $obj->number . ' \'' . $obj->name . '\'',
                );
            }

        # ;ams <string> -m <number>
        # ;ams <string> -m <number> -o
        } else {

            # Check that the model object exists and that it's a minion object
            if (! $session->worldModelObj->ivExists('modelHash', $number)) {

                return $self->error(
                    $session, $inputString,
                    'The world model object #' . $number . ' does not exist',
                );

            } else {

                $obj = $session->worldModelObj->ivShow('modelHash', $number);
            }

            if ($obj->category ne 'minion') {

                return $self->error(
                    $session, $inputString,
                    'The existing model object #' . $number . ' is not a minion',
                );
            }

            # Set the object's owner, replacing the IV's existing value
            if ($otherFlag) {
                $obj->ivPoke('ownMinionFlag', FALSE);
            } else {
                $obj->ivPoke('ownMinionFlag', TRUE);
            }

            # Add the minion string to the world model
            $session->worldModelObj->ivAdd('minionStringHash', $string, $obj);

            return $self->complete(
                $session, $standardCmd,
                'Added the minion string \'' . $string . '\' corresponding to the minion object'
                . ' #' . $obj->number . ' \'' . $obj->name . '\'',
            );

        }
    }
}

{ package Games::Axmud::Cmd::DeleteMinionString;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteminionstring', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dms', 'delms', 'deleteminionstring'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes minion string(s) from the world model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $pattern,
            $check,
        ) = @_;

        # Local variables
        my (
            $count,
            %hash,
        );

        # Check for improper arguments
        if (! defined $pattern || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check $pattern is valid
        if ($axmud::CLIENT->regexCheck($pattern)) {

            return $self->error(
                $session, $inputString,
                'The pattern \'' . $pattern . '\' isn\'t a valid regular expression',
            );
        }

        # Import the hash of minion strings
        %hash = $session->worldModelObj->minionStringHash;
        if (! %hash) {

            return $self->error(
                $session, $inputString,
                'The world model does not contain any minion strings',
            );
        }

        # Check every minion string in the hash. Any minion strings which don't match $pattern
        #   should be preserved
        $count = 0;
        foreach my $key (keys %hash) {

            if ($key =~ m/$pattern/i) {

                delete $hash{$key};
                $count++;
            }
        }

        if (! $count) {

            return $self->error(
                $session, $inputString,
                'The pattern \'' . $pattern . '\' does not match any existing minion strings',
            );
        }

        # Restore the IV
        $session->worldModelObj->set_minionStringHash(%hash);

        if ($count == 1) {

            return $self->complete(
                $session, $standardCmd,
                'Removed 1 minion string matching the pattern \'' . $pattern . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Removed ' . $count . ' minion strings matching the pattern \'' . $pattern . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListMinionString;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listminionstring', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lms', 'listms', 'listminionstring'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists minion strings stored in the world model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            @list,
            %hash, %checkHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the hash of minion strings
        %hash = $session->worldModelObj->minionStringHash;
        if (! %hash) {

            return $self->error(
                $session, $inputString,
                'The world model does not contain any minion strings',
            );
        }

        # Display header
        $session->writeText('List of minion strings (* - controlled by you)');

        # Display list
        @list = sort {lc($a) cmp lc($b)} (keys %hash);
        foreach my $string (@list) {

            my $minionObj = $hash{$string};

            if (! $minionObj->number) {

                # Non-model minion
                $session->writeText(sprintf('   %-40.40s', $string));

            } elsif (! $minionObj->ownMinionFlag) {

                $session->writeText(sprintf('   %-40.40s', $string) . ' #' . $minionObj->number);

            } else {

                $session->writeText(sprintf(' * %-40.40s', $string) . ' #' . $minionObj->number);
            }

            # Keep track of how many minion objects are here
            if ($minionObj->number) {

                $checkHash{$minionObj->number} = undef;
            }
        }

        # Display footer
        return $self->complete(
            $session, $standardCmd,
            'End of list (strings: ' . scalar @list . ', unique minion objects: '
            . scalar (keys %checkHash) . ')',
        );
    }
}

{ package Games::Axmud::Cmd::SetLightList;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setlightlist', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sll', 'setlightlist'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the list of recognised light status values';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @list,
        ) = @_;

        # Local variables
        my (
            @finalList,
            %standardHash, %checkHash,
        );

        # (No improper arguments to check)

        # ;sll
        if (! @list) {

            $session->writeText('Current light status list:');
            $session->writeText('   ' . join(' ', $session->worldModelObj->lightStatusList));
            $session->writeText('Default light status list:');
            $session->writeText('   ' . join(' ', $session->worldModelObj->constLightStatusList));

            return $self->complete($session, $standardCmd, 'Light status lists displayed');

        # ;sll <list>
        } else {

            # Build a hash of standard light statuses, for quick comparison
            foreach my $status ($session->worldModelObj->constLightStatusList) {

                $standardHash{$status} = undef;
            }

            # Go through @list. If there are any multiple entries, or any values that are standard
            #   ('day', 'night', 'darkness'), remove them. For quick checking, build a hash as we go
            foreach my $item (@list) {

                if (! exists $checkHash{$item} && ! exists $standardHash{$item}) {

                    push (@finalList, $item);
                }
            }

            # Add the standard light status values to the list (at the beginning)
            unshift (@finalList, $session->worldModelObj->constLightStatusList);

            # Update the IV
            $session->worldModelObj->set_lightStatusList(@finalList);

            return $self->complete(
                $session, $standardCmd,
                'Light status list set to \'' . join(' ', @finalList) . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ResetLightList;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resetlightlist', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rll', 'resetlightlist'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets the list of recognised light status values';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Reset the list
        @list = $session->worldModelObj->constLightStatusList;
        $session->worldModelObj->set_lightStatusList(@list);

        return $self->complete(
            $session, $standardCmd,
            'Light status list reset to \'' . join(' ', @list) . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::SetLightStatus;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setlightstatus', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sls', 'setlight', 'setlightstatus'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the current light status';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $status,
            $check,
        ) = @_;

        # Local variables
        my $matchFlag;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;sls
        if (! defined $status) {

            return $self->complete(
                $session, $standardCmd,
                'Light status currently set to \''. $session->worldModelObj->lightStatus . '\'',
            );

        # ;sls <status>
        } else {

            # Check that $status is a recognised light status
            OUTER: foreach my $item ($session->worldModelObj->lightStatusList) {

                if ($status eq $item) {

                    $matchFlag = TRUE;
                    last OUTER;
                }
            }

            if (! $matchFlag) {

                return $self->error(
                    $session, $inputString,
                    'Light status \'' . $status . '\' isn\'t one of the world model\'s recognised'
                    . ' light status values (try \';setlightlist\' first)',
                );

            } else {

                # Set the light status
                $session->worldModelObj->set_lightStatus($status);

                return $self->complete(
                    $session, $standardCmd,
                    'Current light status set to \''. $status . '\'',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::SetRoom;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setroom', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['srm', 'setroom'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the automapper\'s current room';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $roomNum, $numberFlag, $lostFlag, $waitFlag, $followFlag, $updateFlag,
            $flagCount, $roomTag, $roomObj, $result, $msg,
        );

        # Extract switches
        $flagCount = 0;

        ($switch, $roomNum, @args) = $self->extract('-n', 1, @args);
        if (defined $switch) {

            $numberFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-l', 0, @args);
        if (defined $switch) {

            $lostFlag = TRUE;
        }

        ($switch, @args) = $self->extract('-w', 0, @args);
        if (defined $switch) {

            $waitFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-f', 0, @args);
        if (defined $switch) {

            $followFlag = TRUE;
            $flagCount++;
        }

        ($switch, @args) = $self->extract('-u', 0, @args);
        if (defined $switch) {

            $updateFlag = TRUE;
            $flagCount++;
        }

        # There should be 0 or 1 arguments left
        $roomTag = shift @args;
        if (defined $roomTag && $numberFlag) {

            return $self->error(
                $session, $inputString,
                'The \'-n\' switch must be used with a room number and can\'t be combined with a'
                . ' room tag',
            );
        }

        # Some arguments can't be combined
        if ($flagCount > 1) {

            return $self->error(
                $session, $inputString,
                'The switches -w, -f and -u can\'t be combined',
            );

        } elsif ($numberFlag && $lostFlag) {

            return $self->error(
                $session, $inputString,
                'The switches -n and -l can\'t be combined',
            );

        } elsif ($lostFlag && defined $roomTag) {

            return $self->error(
                $session, $inputString,
                'The -l switch can\'t be combined with a room tag/room number argument',
            );
        }

        # Check for improper arguments
        if (@args || (! defined $roomTag && ! defined $roomNum && ! $lostFlag)) {

            return $self->improper($session, $inputString);
        }

        # Can't do anything if the Locator is not running, if it doesn't know anything about the
        #   current location (exception: when the session is in 'connect offline' mode, the call to
        #   GA::Obj::Map->setCurrentRoom creates a pseudo-room statement for the Locator task), if
        #   the Locator is expecting more room statements or if the -l switch was specified and the
        #   automapper hasn't set the last known room (in which the character got lost)
        if (! $session->locatorTask) {

            return $self->error(
                $session, $inputString,
                'Can\'t set the automapper\'s current room because the Locator task is not running',
            );

        } elsif (! $session->locatorTask->roomObj && $session->status ne 'offline') {

            return $self->error(
                $session, $inputString,
                'Can\'t set the automapper\'s current room because the Locator task doesn\'t know'
                . ' anything about the current location',
            );

        } elsif ($session->locatorTask->moveList) {

            return $self->error(
                $session, $inputString,
                'Can\'t set the automapper\'s current room because the Locator task is expecting'
                . ' more room statements (use \';resetlocator\' to reset it)',
            );

        } elsif ($lostFlag && ! $session->mapObj->lastKnownRoom) {

            return $self->error(
                $session, $inputString,
                'Can\'t set the automapper\'s current room because the character\'s location before'
                . ' getting lost is not known',
            );
        }

        # ;srm -l
        # ;srm -l <mode>
        if ($lostFlag) {

            $roomNum = $session->mapObj->lastKnownRoom->number;

        # ;srm <tag>
        # ;srm <tag> <mode>
        # ;srm <number>
        # ;srm <number> <mode>
        } elsif (! $numberFlag) {

            # Check that the world model is using this room tag (or one with that's the same, after
            #   we convert all capital letters to lower-case). The call to ->checkRoomTag converts
            #   a room tag to a model number
            $roomNum = $session->worldModelObj->checkRoomTag($roomTag);
            if (! $roomNum) {

                if (! $axmud::CLIENT->intCheck($roomTag)) {

                    return $self->error(
                        $session, $inputString,
                        'The world model doesn\'t have a room with the room tag \'' . $roomTag
                        . '\' (did you mean to type \';setroom -n ' . $roomTag . '\'?)',
                    );

                } else {

                    # $roomTag is a model number
                    $roomNum = $roomTag;
                    $roomTag = undef;
                }
            }
        }

        # ;srm <tag>
        # ;srm <tag> <mode>
        # ;srm <number>
        # ;srm <number> <mode>
        # ;srm -n <number>
        # ;srm -n <number> <mode>
        $roomObj = $session->worldModelObj->ivShow('modelHash', $roomNum);
        if (! $roomObj) {

            return $self->error(
                $session, $inputString,
                'The world model doesn\'t have a room #' . $roomNum,
            );

        } elsif ($roomObj->category ne 'room') {

            return $self->error(
                $session, $inputString,
                'The world model object #' . $roomObj->number . ' is a \'' . $roomObj->category
                . '\', not a room',
            );
        }

        # Set the automapper's current room
        if (! $session->mapObj->setCurrentRoom($roomObj)) {

            return $self->error(
                $session, $inputString,
                'General error setting the automapper\'s current room',
            );
        }

        # Compose a confirmation message
        if ($roomTag) {

            $msg = 'Automapper\'s current room set to \'' . $roomTag . '\' (#' . $roomNum . ')';

        } else {

            $msg = 'Automapper\'s current room set to #' . $roomNum;
        }

        # If one of the <mode> switches was specified, set the automapper's mode
        if ($flagCount) {

            # Set the Automapper window's mode directly (if the window isn't open, the automapper
            #   object's ->trackAloneFlag has already been set to TRUE by the call to
            #   ->setCurrentRoom)
            if ($session->mapWin) {

                if ($waitFlag) {

                    $result = $session->mapWin->setMode('wait');

                } else {

                    if ($followFlag) {
                        $result = $session->mapWin->setMode('follow');
                    } elsif ($updateFlag) {
                        $result = $session->mapWin->setMode('update');
                    }

                    # The call to ->setMode fails if no regionmap is visible, and when 'follow' mode
                    #   is on, the previous call to ->setCurrentRoom didn't set the automapper's
                    #   ghost room
                    # Solution to both problems is to set the current room a second time
                    $session->mapObj->setCurrentRoom($roomObj);
                }

                if (! $result) {

                    return $self->complete(
                        $session, $standardCmd,
                        $msg . ', but the Automapper window mode couldn\'t be set',
                    );
                }
            }
        }

        # Operation complete
        return $self->complete($session, $standardCmd, $msg);
    }
}

{ package Games::Axmud::Cmd::ResetRoom;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('resetroom', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['rrm', 'resetroom'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Resets the automapper\'s current room';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Can't do anything if the Locator is not running or if it doesn't know anything about the
        #   current location
        if (! $session->locatorTask) {

            return $self->error(
                $session, $inputString,
                'Can\'t reset the automapper\'s current room because the Locator task is not'
                . ' running',
            );

        } elsif (! $session->locatorTask->roomObj) {

            return $self->error(
                $session, $inputString,
                'Can\'t reset the automapper\'s current room because the Locator task doesn\'t know'
                . ' anything about the current location',
            );
        }

        # Check that the automapper object has a current room set
        if (! $session->mapObj->currentRoom) {

            return $self->complete(
                $session, $standardCmd,
                'Automapper\'s current room reset (actually, it hadn\'t actually been set)',
            );

        } else {

            # Reset the room
            $session->mapObj->setCurrentRoom();

            return $self->complete(
                $session, $standardCmd,
                'Automapper\'s current room reset',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetOfflineRoom;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setofflineroom', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sor', 'setoffline', 'setofflineroom'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets the type of room statement in \'offline\' mode';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Don't do anything while the session's status is not 'offline'
        if ($session->status ne 'offline') {

            return $self->error(
                $session, $inputString,
                'This command can only be used while the session is in \'connect offline\' mode',
            );
        }

        # ;sor
        if (! $switch) {

            if ($session->mapObj->pseudoStatementMode eq 'short') {

                return $self->complete(
                    $session, $standardCmd,
                    'The automapper is currently showing short verbose room statements',
                );

            } else {

                # ->pseudoStatementMode is set to 'verbose' or 'brief'
                return $self->complete(
                    $session, $standardCmd,
                    'The automapper is currently showing ' . $session->mapObj->pseudoStatementMode
                    . ' room statements',
                );
            }

        # ;sor -v
        # ;sor -s
        # ;sor -b
        } else {

            if ($switch eq '-v' || $switch eq 'verbose') {

                if (! $session->currentWorld->verboseComponentList) {

                    return $self->error(
                        $session, $inputString,
                        'The current world profile doesn\'t specify how to display verbose room'
                        . ' statements',
                    );

                } else {

                    $session->mapObj->set_pseudoStatementMode('verbose');
                }

            } elsif ($switch eq '-s' || $switch eq 'short') {

                if (! $session->currentWorld->shortComponentList) {

                    return $self->error(
                        $session, $inputString,
                        'The current world profile doesn\'t specify how to display short verbose'
                        . ' room statements',
                    );

                } else {

                    $session->mapObj->set_pseudoStatementMode('short');
                }

            } elsif ($switch eq '-b' || $switch eq 'brief') {

                if (! $session->currentWorld->briefComponentList) {

                    return $self->error(
                        $session, $inputString,
                        'The current world profile doesn\'t specify how to display brief room'
                        . ' statements',
                    );

                } else {

                    $session->mapObj->set_pseudoStatementMode('brief');
                }

            } else {

                return $self->error(
                    $session, $inputString,
                    'Invalid switch \'' . $switch . '\'',
                );
            }

            if ($session->mapObj->pseudoStatementMode eq 'short') {

                return $self->complete(
                    $session, $standardCmd,
                    'The automapper is now showing short verbose room statements',
                );

            } else {

                # ->pseudoStatementMode is set to 'verbose' or 'brief'
                return $self->complete(
                    $session, $standardCmd,
                    'The automapper is now showing ' . $session->mapObj->pseudoStatementMode
                    . ' room statements',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::LocateRoom;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('locateroom', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lrm', 'locateroom'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Locates the current room in the world model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $switch,
            $check,
        ) = @_;

        # Local variables
        my (
            $waitFlag, $followFlag, $updateFlag, $taskObj, $newRoomObj, $result, $count, $largest,
            @roomList, @sortedList, @selectList,
            %checkHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # If a switch was specified, check it's valid
        if ($switch) {

            if ($switch ne '-w' && $switch ne '-f' && $switch ne '-u') {

                return $self->error(
                    $session, $inputString,
                    'Invalid switch - try \'-w\', \'-f\' or \'-u\'',
                );
            }
        }

        # Import Locator task (for convenience), and check that it's ready
        $taskObj = $session->locatorTask;
        if (! $taskObj) {

            return $self->error(
                $session, $inputString,
                'Can\'t locate the current room because the Locator task is not running',
            );

        } elsif ($session->status eq 'offline') {

            return $self->error(
                $session, $inputString,
                'Can\'t locate the current room in \'offline\' mode',
            );

        } elsif (! $taskObj->roomObj) {

            return $self->error(
                $session, $inputString,
                'Can\'t locate the current room because the Locator task doesn\'t know anything'
                . ' about the current location',
            );

        } elsif ($taskObj->roomObj->currentlyDarkFlag) {

            return $self->error(
                $session, $inputString,
                'Can\'t locate the current room because it is dark',
            );

        } elsif ($taskObj->roomObj->unspecifiedFlag) {

            return $self->error(
                $session, $inputString,
                'Can\'t locate the current room because it is unspecified',
            );

        } elsif ($taskObj->moveList) {

            return $self->error(
                $session, $inputString,
                'Can\'t locate the current room because the Locator task is expecting more room'
                . ' statements (use \';resetlocator\' to reset it)',
            );
        }

        # Compare the Locator task's current room with every room in the model
        foreach my $roomObj ($session->worldModelObj->ivValues('roomModelHash')) {

            my ($result) = $session->worldModelObj->compareRooms(
                $session,
                $roomObj,
                undef,              # Compare against the Locator task's current room
                FALSE,              # Don't match dark rooms
                TRUE,               # Don't match rooms if one has a title/descrip & other doesn't
            );

            if ($result) {

                push (@roomList, $roomObj);
            }
        }

        # No matching rooms found
        if (! @roomList) {

            return $self->error(
                $session, $inputString,
                'No matching rooms found in any region',
            );
        }

        # At least one matching room found, so list them (but don't display more than 100 rooms)
        if ((scalar @roomList) > 100) {

            # We'll list the first 100 rooms, sorted by room number
            $count = 0;

            foreach my $roomObj (@roomList) {

                $count++;

                if ($count <= 100) {

                    $checkHash{$roomObj->number} = $roomObj;
                    if (! defined $largest || $count > $largest) {

                        $largest = $count;
                    }

                } elsif ($count < $largest) {

                    delete $checkHash{$largest};
                    $checkHash{$roomObj->number} = $roomObj;

                    $largest = undef;
                    foreach my $number (keys %checkHash) {

                        if (! defined $largest || $number > $largest) {

                            $largest = $number;
                        }
                    }
                }
            }

            @sortedList = sort {$a->number <=> $b->number} (values %checkHash);

        } else {

            @sortedList = sort {$a->number <=> $b->number} (@roomList);
        }

        # Display header
        if ((scalar @roomList) <= 100) {
            $session->writeText('Locate room list');
        } else {
            $session->writeText('Locate room list (showing first 100 rooms)');
        }

        $session->writeText('   Room num      Room tag     Region name');

        # Display list
        OUTER: foreach my $roomObj (@sortedList) {

            my ($regionObj, $tag);

            $regionObj = $session->worldModelObj->ivShow('modelHash', $roomObj->parent);
            if ($roomObj->roomTag) {
                $tag = $roomObj->roomTag;
            } else {
                $tag = '<none>';
            }

            $session->writeText(
                '   #' . sprintf('%-12.12s %-12.12s', $roomObj->number, $tag)
                . ' ' . $regionObj->name,
            );
        }

        if ((scalar @roomList) > 100) {

            $session->writeText('   ...');
        }

        # If exactly one matching room was found, set the automapper's current room
        if (@roomList == 1) {

            $newRoomObj = $roomList[0];

            if (! $session->mapObj->setCurrentRoom($newRoomObj)) {

                return $self->error(
                    $session, $inputString,
                    'General error setting the automapper\'s current room',
                );
            }

            # If one of the <mode> switches was specified, set the automapper's mode
            if ($switch) {

                # Set the Automapper window's mode directly (if the window isn't open, the
                #   automapper object's ->trackAloneFlag has already been set to TRUE by the call to
                #   ->setCurrentRoom)
                if ($session->mapWin) {

                    if ($switch eq '-w') {
                        $result = $session->mapWin->setMode('wait');
                    } elsif ($switch eq '-f') {
                        $result = $session->mapWin->setMode('follow');
                    } elsif ($switch eq '-u') {
                        $result = $session->mapWin->setMode('update');
                    }
                }
            }

        # If more than one match was found and the automapper window is open, select the matching
        #   rooms
        } elsif (@roomList && $session->mapWin) {

            foreach my $roomObj (@roomList) {

                push (@selectList, $roomObj, 'room');
            }

            $session->mapWin->setSelectedObj();
            $session->mapWin->setSelectedObj(\@selectList, TRUE);
        }

        # Display footer
        if (@roomList == 1) {

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'Automapper current room set to #' . $newRoomObj->number,
            );

        } else {

            # Display footer
            return $self->complete(
                $session, $standardCmd,
                'Found ' . scalar @roomList . ' matching rooms (use \'setroom <number>\' to set'
                . ' the automapper\'s current room',
            );
        }
    }
}

{ package Games::Axmud::Cmd::EditRoomComponent;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editroomcomponent', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ecm', 'editcomponent', 'editroomcomponent'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for the room component';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the component exists
        $obj = $session->currentWorld->ivShow('componentHash', $name);
        if (! $obj) {

            return $self->error(
                $session, $inputString,
                'The \'' . $session->currentWorld->name . '\' world profile doesn\'t have a room'
                . ' component called \'' . $name . '\'',
            );
        }

        # Open an 'edit' window for the world model painter
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Component',
                $session->mainWin,
                $session,
                'Edit room component \'' . $name . '\'',
                $obj,
                FALSE,                          # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the room component \'' . $name . '\'',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the room component \'' . $name . '\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListRoomComponent;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listroomcomponent', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lcm', 'listcomponent', 'listroomcomponent'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists room components for the current world';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $name,
            $check,
        ) = @_;

        # Local variables
        my (
            $count, $obj, $string,
            @list,
            %hash, %hash2,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;lcm
        if (! defined $name) {

            # Display header
            $session->writeText(
                'List of room statement components for \'' . $session->currentWorld->name . '\'',
            );

            $session->writeText('   Name                     Type             Size Min  Max');

            # Display list
            foreach my $obj (
                sort {lc($a->name) cmp lc($b->name)}
                ($session->currentWorld->ivValues('componentHash'))
            ) {
                $session->writeText(
                    sprintf(
                        '   %-24.24s %-16.16s %-4.4s %-4.4s %-4.4s', #
                        $obj->name,
                        $obj->type,
                        $obj->size,
                        $obj->minSize,
                        $obj->maxSize,
                    ),
                );
            }

            # Display supplementary lists
            $session->writeText('Components used for verbose room statements');
            if (! $session->currentWorld->verboseComponentList) {

                $session->writeText('   (none used)');

            } else {

                $session->writeText(
                    '   ' . join(' ', $session->currentWorld->verboseComponentList),
                );
            }

            $session->writeText('Components used for short verbose room statements');
            if (! $session->currentWorld->shortComponentList) {

                $session->writeText('   (none used)');

            } else {

                $session->writeText('   ' . join(' ', $session->currentWorld->shortComponentList));
            }


            $session->writeText('Components used for brief room statements');
            if (! $session->currentWorld->briefComponentList) {

                $session->writeText('   (none used)');

            } else {

                $session->writeText('   ' . join(' ', $session->currentWorld->briefComponentList));
            }

            # Display footer
            $count = $session->currentWorld->ivPairs('componentHash');
            if ($count == 1) {

                return $self->complete($session, $standardCmd, 'End of list (1 component found)');

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'End of list (' . $count . ' components found)',
                );
            }

        # ;lcm <name>
        } else {

            # Check the component exists
            $obj = $session->currentWorld->ivShow('componentHash', $name);
            if (! $obj) {

                return $self->error(
                    $session, $inputString,
                    'The \'' . $session->currentWorld->name . '\' world profile doesn\'t have a'
                    . ' room component called \'' . $name . '\'',
                );
            }

            # Display header
            $session->writeText(
                'Room component \'' . $name . '\' (world: \'' . $session->currentWorld->name
                . '\')',
            );

            # Display list
            $session->writeText('   Type:                      ' . $obj->type);
            $session->writeText('   Fixed size (0- not fixed): ' . $obj->size);
            $session->writeText('   (or) minimum size:         ' . $obj->minSize);
            $session->writeText('   maximum size:              ' . $obj->maxSize);
            $session->writeText(' ');

            if ($obj->analyseMode eq 'check_pattern') {

                $session->writeText(
                    '   Analyse mode \'check_pattern\' - Check each line, one at a time, against'
                    . ' patterns/tags',
                );

            } else {

                $session->writeText(
                    '   Analyse mode \'check_pattern_tag\' - Check each pattern/tag, one at a time,'
                    . ' against lines',
                );
            }

            if (! $obj->boldSensitiveFlag) {
                $string = 'no';
            } else {
                $string = 'yes';
            }

            $session->writeText('   Use normal/bold tags interchangeably: ' . $string);

            if (! $obj->useInitialTagsFlag) {
                $string = 'no';
            } else {
                $string = 'yes';
            }

            $session->writeText('   Only use tags at beginning of line: ' . $string);

            if (! $obj->useExplicitTagsFlag) {
                $string = 'no';
            } else {
                $string = 'yes';
            }

            $session->writeText('   Only use tags explicitly appearing in line: ' . $string);

            if (! $obj->combineLinesFlag) {
                $string = 'no';
            } else {
                $string = 'yes';
            }

            $session->writeText('   Treat lines of component as a single line: ' . $string);

            if (! defined $obj->useTextColour) {
                $string = '(not set)';
            } else {
                $string = $obj->useTextColour;
            }

            $session->writeText('   Only use text in this colour: ' . $string);

            $session->writeText('   Ignore first n characters: ' . $obj->ignoreFirstChars);
            $session->writeText('   Use first n characters: ' . $obj->useFirstChars);

            if (! defined $obj->usePatternGroups) {
                $string = '(not set)';
            } else {
                $string = $obj->usePatternGroups;
            }

            $session->writeText(
                '   If pattern (regex) matches, use only group substrings: ' . $string,
            );

            if (! $obj->noExtractList) {
                $string = '(not set)';
            } else {
                $string = join(' ', $obj->noExtractList);
            }

            $session->writeText(
                '   If this component is extracted, don\'t extract these components: ' . $string,
            );

            if (! $obj->size) {

                $session->writeText(' ');
                $session->writeText(
                    '   Patterns/tags used to extract the component (tested in this order)',
                );

                # IVs to display are 'xxxPatternList, 'xxxTagList', 'xxxAllFlag', 'xxxTagMode'
                @list = (
                    'start', 'startNo', 'skip', 'stopBefore', 'stopBeforeNo', 'stopAt', 'stopAtNo',
                );

                %hash = (
                    'start'
                        => 'Start at first line which does match these patterns/tags:',
                    'startNo'
                        => 'Start at first line which does not match these patterns/tags:',
                    'skip'
                        => 'Skip the first line which does match these patterns/tags:',
                    'stopBefore'
                        => 'Stop before the first line which does match these patterns/tags:',
                    'stopBeforeNo'
                        => 'Stop before the first line which does not match these patterns/tags:',
                    'stopAt'
                        => 'Stop at the first line which does match these patterns/tags:',
                    'stopAtNo'
                        => 'Stop at the first line which does not match these patterns/tags:',
                );

                foreach my $item (@list) {

                    my (
                        $iv, $iv2, $iv3, $iv4, $allFlag, $tagMode,
                        @patternList, @tagList,
                    );

                    $iv = $item . 'PatternList';
                    @patternList = $obj->$iv;

                    $iv2 = $item . 'TagList';
                    @tagList = $obj->$iv2;

                    $iv3 = $item . 'AllFlag';
                    $allFlag = $obj->$iv3;

                    $iv4 = $item . 'TagMode';
                    $tagMode = $obj->$iv4;

                    # For convenience, only display the pattern groups which have been set
                    if (@patternList || @tagList || $allFlag || $tagMode) {

                        $session->writeText('      ' . $hash{$item});

                        if (! @patternList) {

                            $session->writeText('         Patterns: (none)');

                        } else {

                            $session->writeText('         Patterns:');

                            foreach my $pattern (@patternList) {
                                $session->writeText('            ' . $pattern);
                            }
                        }

                        if (! @tagList) {

                            $session->writeText('         Tags:     (none)');

                        } else {

                            $session->writeText('         Tags:');

                            foreach my $tag (@tagList) {
                                $session->writeText('            ' . $tag);
                            }
                        }

                        if (! $allFlag) {
                            $string = 'no';
                        } else {
                            $string = 'yes';
                        }

                        $session->writeText('         All patterns/tags must be found: ' . $string);

                        if ($tagMode eq 'default') {

                            $session->writeText(
                                '         Tag mode \'default\' - line may contain tags',
                            );

                        } elsif ($tagMode eq 'no_colour') {

                            $session->writeText(
                                '         Tag mode \'no_colour\' - line contains no colour tags',
                            );

                        } elsif ($tagMode eq 'no_style') {

                            $session->writeText(
                                '         Tag mode \'no_style\' - line contains no style tags',
                            );

                        } elsif ($tagMode eq 'no_colour_style') {

                            $session->writeText(
                                '         Tag mode \'no_colour_style\' - line contains no'
                                . ' colour/style tags',
                            );
                        }

                    }
                }

                $session->writeText(
                    '      Stop at nth line starting with Capital Letter: ' .  $obj->upperCount,
                );

                $session->writeText(
                    '      Stop at nth line starting with other alphanumeric character: '
                    .  $obj->otherCount,
                );

                # 2 IVs to display here
                %hash2 = (
                    'default'       => '(not used)',
                    'no_char'       => 'the first line containing no characters at all',
                    'no_letter_num' => 'the first line containing no alphanumeric characters',
                    'no_start_letter_num'
                                    => 'the first line which doesn\'t start with an alphanumeric'
                                            . ' character',
                    'no_tag'        => 'the first line containing no ' . $axmud::SCRIPT
                                            . ' colour/style tags at all',
                    'has_letter_num'
                                    => 'the first line which DOES contain alphanumeric characters',
                    'has_start_letter_num'
                                    => 'the first line which DOES start with an alphanumeric'
                                            . 'character',
                    'has_tag'       => 'the first line which DOES contain ' . $amux::NAME_ARTICLE
                                            . ' colour/style tag',
                );

                $session->writeText(
                    '      Stop before mode: ' . $obj->stopBeforeMode . ' - stop before '
                    . $hash2{$obj->stopBeforeMode},
                );

                $session->writeText(
                    '      Stop at mode: ' . $obj->stopAtMode . ' - stop at '
                    . $hash2{$obj->stopAtMode},
                );
            }

            # Display footer
            return $self->complete($session, $standardCmd, 'End of list');
        }
    }
}

{ package Games::Axmud::Cmd::EditPainter;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editpainter', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['edp', 'editpaint', 'editpainter'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for the world model painter';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Open an 'edit' window for the world model painter
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Painter',
                $session->mainWin,
                $session,
                'Edit world model painter',
                $session->worldModelObj->painterObj,
                FALSE,                          # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the world model painter',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the world model painter',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetAssistedMoves;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setassistedmoves', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['sam', 'setam', 'setassistedmove', 'setassistedmoves'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Turns assisted moves on/off';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;sam
        if (! defined $arg) {

            if ($session->worldModelObj->assistedMovesFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Assisted moves are currently turned on',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Assisted moves are currently turned off',
                );
            }

        # ;sam on
        } elsif ($arg eq 'on') {

            if ($session->worldModelObj->assistedMovesFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Assisted moves are already turned on',
                );

            } else {

                $session->worldModelObj->toggleFlag(
                    'assistedMovesFlag',
                    TRUE,       # Turn on
                    FALSE,      # If Automapper window is open, don't call $self->drawRegion
                    'allow_assisted_moves',
                );

                return $self->complete(
                    $session, $standardCmd,
                    'Assisted moves have been turned on',
                );
            }

        } elsif ($arg eq 'off') {

            if (! $session->worldModelObj->assistedMovesFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Assisted moves are already turned off',
                );

            } else {

                $session->worldModelObj->toggleFlag(
                    'assistedMovesFlag',
                    FALSE,      # Turn off
                    FALSE,      # If Automapper window is open, don't call $self->drawRegion
                    'allow_assisted_moves',
                );

                return $self->complete(
                    $session, $standardCmd,
                    'Assisted moves have been turned off',
                );
            }

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid setting - try \';setassistedmoves on\' or \';setassistedmoves off\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::SetProtectedMoves;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('setprotectedmoves', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['spm', 'setpm', 'setprotectedmove', 'setprotectedmoves'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Turns protected moves on/off';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $arg,
            $check,
        ) = @_;

        # Local variables
        my @list;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # ;spm
        if (! defined $arg) {

            if ($session->worldModelObj->protectedMovesFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Protected moves are currently turned on',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Protected moves are currently turned off',
                );
            }

        # ;spm on
        } elsif ($arg eq 'on') {

            if ($session->worldModelObj->protectedMovesFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Protected moves are already turned on',
                );

            } else {

                $session->worldModelObj->toggleFlag(
                    'protectedMovesFlag',
                    TRUE,       # Turn on
                    FALSE,      # If Automapper window is open, don't call $self->drawRegion
                    'allow_protected_moves',
                );

                return $self->complete(
                    $session, $standardCmd,
                    'Protected moves have been turned on',
                );
            }

        } elsif ($arg eq 'off') {

            if (! $session->worldModelObj->protectedMovesFlag) {

                return $self->complete(
                    $session, $standardCmd,
                    'Protected moves are already turned off',
                );

            } else {

                $session->worldModelObj->toggleFlag(
                    'protectedMovesFlag',
                    FALSE,      # Turn off
                    FALSE,      # If Automapper window is open, don't call $self->drawRegion
                    'allow_protected_moves',
                );

                return $self->complete(
                    $session, $standardCmd,
                    'Protected moves have been turned off',
                );
            }

        } else {

            return $self->error(
                $session, $inputString,
                'Invalid setting - try \';setprotectedmoves on\' or \';setprotectedmoves off\'',
            );
        }
    }
}

{ package Games::Axmud::Cmd::RoomCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('roomcommand', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['room', 'roomcmd', 'roomcommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Executes a room command';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my $cmd;

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the Locator task knows the current location
        if (! $session->locatorTask) {

            return $self->error(
                $session, $inputString,
                'The Locator task is not running',
            );

        } elsif (! $session->locatorTask->roomObj) {

            return $self->error(
                $session, $inputString,
                'The Locator task doesn\'t know anything about the current location',
            );

        } elsif (! $session->locatorTask->roomObj->tempRoomCmdList) {

            return $self->error(
                $session, $inputString,
                'There are no room commands available in the current room',
            );

        } else {

            $cmd = $session->locatorTask->useRoomCmd();

            if (! $cmd) {

                return $self->error(
                    $session, $inputString,
                    'Internal error while trying to execute a room command',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'Executed room command \'' . $cmd . '\'',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::IgnoreRoomCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('ignoreroomcommand', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['iro', 'ignoreroom', 'ignoreroomcmd', 'ignoreroomcommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Marks a room command as ignorable';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $cmd,
            $check,
        ) = @_;

        # Check for improper arguments
        if (! defined $cmd || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the room command isn't already ignored
        if (defined $session->currentWorld->ivFind('roomCmdIgnoreList', $cmd)) {

            return $self->error(
                $session, $inputString,
                'The room command \'' . $cmd . '\' is already marked as ignorable by the current'
                . ' world',
            );

        } else {

            $session->currentWorld->ivPush('roomCmdIgnoreList', $cmd);

            return $self->complete(
                $session, $standardCmd,
                'The room command \'' . $cmd . '\' has been marked as ignorable by the current'
                . ' world',
            );
        }
    }
}

{ package Games::Axmud::Cmd::NoticeRoomCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('noticeroomcommand', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['nro', 'noticeroom', 'noticeroomcmd', 'noticeroomcommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Marks a room command as usable';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $cmd,
            $check,
        ) = @_;

        # Local variables
        my $index;

        # Check for improper arguments
        if (! defined $cmd || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the room command isn't already usable
        $index = $session->currentWorld->ivFind('roomCmdIgnoreList', $cmd);
        if (! defined $index) {

            return $self->error(
                $session, $inputString,
                'The room command \'' . $cmd . '\' is already marked as usable by the current'
                . ' world',
            );

        } else {

            $session->currentWorld->ivSplice('roomCmdIgnoreList', $index, 1);

            return $self->complete(
                $session, $standardCmd,
                'The room command \'' . $cmd . '\' has been marked as usable by the current world',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListRoomCommand;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listroomcommand', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lro', 'listroom', 'listroomcmd', 'listroomcommand'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Displays room command settings';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $taskObj, $string,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the Locator task (for convenience)
        $taskObj = $session->locatorTask;

        # Display header
        $session->writeText('Room command settings');

        # Display list
        if (! $taskObj || ! $taskObj->roomObj) {
            $string = 'NO';
        } else {
            $string = 'YES';
        }

        $session->writeText('   Locator task running and knows current location : ' . $string);

        if ($string eq 'YES') {

            $session->writeText('   Current location\'s room commands (in original order)');

            if (! $taskObj->roomObj->roomCmdList) {
                $session->writeText('      <empty>');
            } else {
                $session->writeText('      ' . join(', ', $taskObj->roomObj->roomCmdList));
            }
        }

        $session->writeText('   Current world\'s ignorable room command list');

        if (! $session->currentWorld->roomCmdIgnoreList) {

            $session->writeText('      <empty>');

        } else {

            $session->writeText(
                '      ' . join(', ', $session->currentWorld->roomCmdIgnoreList),
            );
        }

        if ($string eq 'YES') {

            $session->writeText('   Next world commands to send with \';roomcommand\'');
            if (! $taskObj->roomObj->tempRoomCmdList) {

                $session->writeText('      <none>');

            } else {

                # Filter out ignorable commands
                foreach my $cmd ($taskObj->roomObj->tempRoomCmdList) {

                    if (! defined $session->currentWorld->ivMatch('roomCmdIgnoreList', $cmd)) {

                        push (@list, $cmd);
                    }
                }

                if (! @list) {
                    $session->writeText('      <empty, after removing ignorable commands>');
                } else {
                    $session->writeText('      ' . join(', ', @list));
                }
            }
        }

        # Display footer
        return $self->complete($session, $standardCmd, 'End of room command list');
    }
}

# Exit model commands

{ package Games::Axmud::Cmd::AddExit;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('addexit', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['aex', 'addexit'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Adds an exit to the exit model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $dir, $roomNum,
            $check,
        ) = @_;

        # Local variables
        my ($roomObj, $exitObj, $result, $mapDir);

        # Check for improper arguments
        if (! defined $dir || ! defined $roomNum || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check the parent room is a valid model object
        if (! $session->worldModelObj->ivExists('modelHash', $roomNum)) {

            return $self->error(
                $session, $inputString,
                'The room #' . $roomNum . ' doesn\'t exist in the world model',
            );

        } else {

            $roomObj = $session->worldModelObj->ivShow('modelHash', $roomNum);
        }

        if ($roomObj->category ne 'room') {

            return $self->error(
                $session, $inputString,
                'The world model object #' . $roomNum . ' is not a room',
            );
        }

        # Check that $dir isn't longer than 64 characters (this is not allowed)
        # N.B. The string can contain spaces and other non-standard chars, to allow for exits like
        #   'enter well'
        if (length ($dir) > 64) {

            return $self->error(
                $session, $inputString,
                'Illegal direction \'' . $dir . '\' (max length: 32 characters)',
            );
        }

        # If $dir is an abbreviated primary direction, unabbreviate it
        $result = $session->currentDict->checkPrimaryDir($dir);
        if ($result) {

            $dir = $result;
            # Get the standard form of the primary direction, used to set the exit object's
            #   ->mapDir
            $mapDir = $session->currentDict->ivShow('combRevDirHash', $dir);

        } else {

            # If $dir is an abbreviated secondary direction, unabbreviate it
            $result = $session->currentDict->checkSecondaryDir($dir);
            if ($result) {

                $dir = $result;

            } else {

                # There's no point specifying relative directions in this command but, nevertheless,
                #   unabbreviate any relation direction the user specifies
                $result = $session->currentDict->checkRelativeDir($dir);
                if ($result) {

                    $dir = $result;
                }
            }
        }

        # Add a new exit to the world model
        $exitObj = $session->worldModelObj->addExit(
            $session,
            FALSE,              # Don't redraw the map yet...
            $roomObj,
            $dir,
            $mapDir,            # May be 'undef'
        );

        if (! $exitObj) {

            return $self->error(
                $session, $inputString,
                'Could not create an exit \'' . $dir . '\' for the room #' . $roomNum,
            );
        }

        if (! $mapDir) {

            # Allocate the exit a map direction
            $session->worldModelObj->allocateCardinalDir($session, $roomObj, $exitObj);
        }

        # (The rest of the code is adapted from GA::Win::Map->addExitCallback)

        # Now, we need to check if the room has any more unallocated exits. If they've temporarily
        #   been assigned the map direction 'undef', we must reallocate them
        OUTER: foreach my $number ($roomObj->ivValues('exitNumHash')) {

            my $thisExitObj = $session->worldModelObj->ivShow('exitModelHash', $number);

            if (! defined $thisExitObj->mapDir && $thisExitObj->drawMode eq 'primary') {

                # Assign the exit object a new map direction (using one of the sixteen cardinal
                #   directions, but not 'up' and 'down'), if any are available
                $session->worldModelObj->allocateCardinalDir(
                    $session,
                    $roomObj,
                    $thisExitObj,
                );
            }
        }

        # Now, if there are any incoming 1-way exits whose ->mapDir is the opposite of the exit
        #   we've just added, the incoming exit should be marked as an uncertain exit
        $session->worldModelObj->modifyIncomingExits(
            $session,
            TRUE,              # Redraw any modified incoming exit
            $roomObj,
            $exitObj,
        );

        # Redraw the new exit's room in every window
        $session->worldModelObj->updateMaps('room', $roomObj);

        return $self->complete(
            $session, $standardCmd,
            'Created exit model object #' . $exitObj->number . ' in room #' . $roomNum . ' \''
            . $roomObj->name . '\'',
        );
    }
}

{ package Games::Axmud::Cmd::EditExit;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('editexit', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['eex', 'editexit'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Opens an \'edit\' window for an exit model object';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $number,
            $check,
        ) = @_;

        # Local variables
        my $obj;

        # Check for improper arguments
        if (! defined $number || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that $number is a valid exit model number
        if (! $session->worldModelObj->ivExists('exitModelHash', $number)) {

            return $self->error(
                $session, $inputString,
                'Could not edit the exit object #' . $number . ' - object does not exist',
            );

        } else {

            $obj = $session->worldModelObj->ivShow('exitModelHash', $number);
        }

        # Open an 'edit' window for the model object
        if (
            ! $session->mainWin->createFreeWin(
                'Games::Axmud::EditWin::Exit',
                $session->mainWin,
                $session,
                'Edit exit model object #' . $obj->number,
                $obj,
                FALSE,                  # Not temporary
            )
        ) {
            return $self->error(
                $session, $inputString,
                'Could not edit the exit model object #' . $number,
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Opened \'edit\' window for the exit model object #' . $number,
            );
        }
    }
}

{ package Games::Axmud::Cmd::DeleteExit;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('deleteexit', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dex', 'delexit', 'deleteexit'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Deletes an exit from the exit model';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my (
            $switch, $twinFlag, $exitNum, $exitObj, $twinExitObj, $result,
            @exitList,
        );

        # Extract optional switch
        ($switch, @args) = $self->extract('-t', 0, @args);
        if (defined $switch) {

            $twinFlag = TRUE;
        }

        # Extract the remaining arguments
        $exitNum = shift @args;
        # There should be nothing left in @args
        if (! defined $exitNum || @args) {

            return $self->improper($session, $inputString);
        }

        # Check the exit is a valid exit model object
        if (! $session->worldModelObj->ivExists('exitModelHash', $exitNum)) {

            return $self->error(
                $session, $inputString,
                'The exit #' . $exitNum . ' doesn\'t exist in the exit model',
            );

        } else {

            $exitObj = $session->worldModelObj->ivShow('exitModelHash', $exitNum);
            push (@exitList, $exitObj);
        }

        # If the -t switch was specified, we also delete the twin exit
        if ($twinFlag && $exitObj->twinExit) {

            $twinExitObj = $session->worldModelObj->ivShow('exitModelHash', $exitObj->twinExit);
            if ($twinExitObj) {

                push (@exitList, $twinExitObj);
            }
        }

        # Delete the exit object(s) and instruct the world model to update its Automapper windows
        $result = $session->worldModelObj->deleteExits(
            $session,
            TRUE,           # Update Automapper windows now
            @exitList,
        );

        # Operation complete
        if (! $result) {

            return $self->error($session, $inputString, 'Could not delete the exit #' . $exitNum);

        } elsif (! $twinExitObj) {

            return $self->complete(
                $session, $standardCmd,
                'Deleted the exit #' . $exitObj->number . ' from the exit model',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Deleted the exit #' . $exitObj->number . ' and its twin exit #'
                . $twinExitObj->number . ' from the exit model',
            );
        }
    }
}

{ package Games::Axmud::Cmd::ListExitModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('listexitmodel', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['lem', 'listexit', 'listexitmodel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists exit model objects';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $start, $stop,
            $check,
        ) = @_;

        # Local variables
        my (
            $recentNum, $recentObj,
            @list,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Check that <start> and <stop> are valid exit model numbers, if specified
        if ((defined $start || defined $stop) && ! $session->worldModelObj->exitObjCount) {

            return $self->complete($session, $standardCmd, 'The exit model is empty');

        } elsif (
            defined $start
            && ! $axmud::CLIENT->intCheck($start, 1, $session->worldModelObj->exitObjCount)
        ) {
            return $self->error(
                $session, $inputString,
                'Can\'t process exit model #' . $start . ': must be a number between 1 and '
                . $session->worldModelObj->exitObjCount,
            );

        } elsif (
            defined $stop
            && ! $axmud::CLIENT->intCheck($stop, 1, $session->worldModelObj->exitObjCount)
        ) {
            return $self->error(
                $session, $inputString,
                'Can\'t process exit model #' . $stop . ': must be a number between 1 and '
                . $session->worldModelObj->exitObjCount,
            );

        } elsif (defined $start && defined $stop && $start > $stop) {

            return $self->error(
                $session, $inputString,
                'Format: \';listexitmodel <start> <stop>\'',
            );
        }

        # ;lem
        if (! defined $start) {

            $session->writeText('Exit model status');
            $session->writeText(
                '   No. objects    : ' . $session->worldModelObj->exitActualCount,
            );
            $session->writeText(
                '   Apparent size  : ' . $session->worldModelObj->exitObjCount,
            );

            # (Import the most-recently created model object)
            $recentNum = $session->worldModelObj->mostRecentExitNum;
            if ($recentNum) {

                $recentObj = $session->worldModelObj->ivShow('exitModelHash', $recentNum);
                if ($recentObj) {

                    $session->writeText('   Newest object  : #' . $recentNum);
                    $session->writeText('      Direction   : ' . $recentObj->dir);
                    if ($recentObj->mapDir) {
                        $session->writeText('      Map dir     : ' . $recentObj->mapDir);
                    } else {
                        $session->writeText('      Map dir     : (unallocatable)');
                    }

                    if ($recentObj->parent) {

                        $session->writeText('      Parent room : ' . $recentObj->parent);

                    } else {

                        # (All exits should have a parent room, but just in case...)
                        $session->writeText('      Parent room : (none)');
                    }

                    if ($recentObj->twinExit) {
                        $session->writeText('      Twin exit   : ' . $recentObj->twinExit);
                    } else {
                        $session->writeText('      Twin exit   : (none)');
                    }

                } else {

                    $session->writeText(
                        '   Newest object  : #' . $recentNum . ' (deleted)',
                    );
                }

            } else {

                $session->writeText('   Newest object  : (none)');
            }

            return $self->complete($session, $standardCmd, 'Exit model status displayed');

        # ;lem <number>
        # ;lem <start> <stop>
        } else {

            # Compile a list of blessed references to world model objects

            # ;lem <number>
            if (defined $start && ! defined $stop) {

                if (! $session->worldModelObj->ivExists('exitModelHash', $start)) {

                    if ($start > $session->worldModelObj->exitObjCount) {

                        return $self->error(
                            $session, $inputString,
                            'Exit model object #' . $start . ' hasn\'t been created yet',
                        );

                    } else {

                        return $self->error(
                            $session, $inputString,
                            'Exit model object #' . $start . ' has been deleted',
                        );
                    }

                } else {

                    # Only one object in the list
                    push (@list, $session->worldModelObj->ivShow('exitModelHash', $start));
                }

            # ;lem <start stop>
            } else {

                # Add to the list every existing object between $start and $stop
                for (my $count = $start; $count <= $stop; $count++) {

                    my $obj;

                    if ($session->worldModelObj->ivExists('exitModelHash', $count)) {

                        push (@list, $session->worldModelObj->ivShow('exitModelHash', $count));
                    }
                }
            }

            # Display header
            $session->writeText('Exit model object list');
            $session->writeText(
                '   Number    Twin #   Direction        Map direction    Room #    Room name'
            );

            # Display list
            foreach my $obj (@list) {

                my ($twinString, $roomObj, $mapDir);

                if ($obj->twinExit) {
                    $twinString = $obj->twinExit;
                } else {
                    $twinString = '(none)';
                }

                if ($obj->mapDir) {
                    $mapDir = $obj->mapDir;
                } else {
                    $mapDir = 'unallocatable';
                }

                if ($obj->parent) {

                    $roomObj = $session->worldModelObj->ivShow('modelHash', $obj->parent);

                    $session->writeText(
                        '   #' . sprintf(
                            '%-8.8s %-8.8s %-16.16s %-16.16s #%-8.8s %-32.32s',
                            $obj->number,
                            $twinString,
                            $obj->dir,
                            $mapDir,
                            $obj->parent,
                            $roomObj->name,
                        )
                    );

                } else {

                    # (All exits should have a parent room, but just in case...)
                    $session->writeText(
                        '   #' . sprintf(
                            '%-8.8s %-8.8s %-16.16s %-16.16s (none)',
                            $obj->number,
                            $twinString,
                            $obj->dir,
                            $mapDir,
                        )
                    );
                }
            }

            # Display footer
            if (@list == 1) {

                return $self->complete(
                    $session, $standardCmd,
                    'End of exit model list (1 object found)',
                );

            } else {

                return $self->complete(
                    $session, $standardCmd,
                    'End of exit model list (' . @list . ' objects found)',
                );
            }
        }
    }
}

{ package Games::Axmud::Cmd::DumpExitModel;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('dumpexitmodel', TRUE, TRUE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['dem', 'dumpexit', 'dumpexitmodel'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Lists all exit model objects';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $check,
        ) = @_;

        # Local variables
        my (
            $number, $count,
            %modelHash, %exitModelHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import world model IVs (for speed)
        %modelHash = $session->worldModelObj->modelHash;
        %exitModelHash = $session->worldModelObj->exitModelHash;

        # Display header
        $session->writeText('Exit model object dump');
        $session->writeText(
            '   Number    Twin #   Direction        Map direction    Room #    Room name',
        );

        # Display list
        $count = 0;
        for ($number = 0; $number <= $session->worldModelObj->exitObjCount; $number++) {

            my ($obj, $twinString, $roomObj, $mapDir);

            $obj = $exitModelHash{$number};
            if ($obj) {

                $count++;

                if ($obj->twinExit) {
                    $twinString = $obj->twinExit;
                } else {
                    $twinString = '(none)';
                }

                if ($obj->mapDir) {
                    $mapDir = $obj->mapDir;
                } else {
                    $mapDir = 'unallocatable';
                }

                if ($obj->parent) {

                    $roomObj = $modelHash{$obj->parent};

                    $session->writeText(
                        '   #' . sprintf(
                            '%-8.8s %-8.8s %-16.16s %-16.16s #%-8.8s %-32.32s',
                            $obj->number,
                            $twinString,
                            $obj->dir,
                            $mapDir,
                            $obj->parent,
                            $roomObj->name,
                        )
                    );

                } else {

                    # (All exits should have a parent room, but just in case...)
                    $session->writeText(
                        '   #' . sprintf(
                            '%-8.8s %-8.8s %-16.16s %-16.16s (none)',
                            $obj->number,
                            $twinString,
                            $obj->dir,
                            $mapDir,
                        )
                    );
                }
            }
        }

        # Display footer
        if ($count == 1) {

            return $self->complete(
                $session, $standardCmd,
                'End of exit model list (1 object found)',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'End of exit model list (' . $count  . ' objects found)',
            );
        }
    }
}

{ package Games::Axmud::Cmd::AllocateExit;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('allocateexit', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['alx', 'allocexit', 'allocateexit'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Allocates an unallocated exit';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            @args,
        ) = @_;

        # Local variables
        my ($roomObj, $dir, $standardDir, $customDir, $string);

        # Check for improper arguments
        if ((scalar @args) < 2) {

            return $self->improper($session, $inputString);
        }

        # Import the automapper's current room (for convenience)
        $roomObj = $session->mapObj->currentRoom;

        # Check that allocating an exit is feasible
        if (! $roomObj) {

            return $self->error(
                $session, $inputString,
                'Can\'t allocate a primary direction to an exit until a current room is set',
            );
        }

        # The first argument is a primary direction (standard or custom, abbreviated or not)
        $dir = shift @args;
        # Translate it into a standard primary direction
        $standardDir = $session->currentDict->convertStandardDir($dir);
        if (! defined $standardDir) {

            return $self->error(
                $session, $inputString,
                '\'' . $dir . '\' is not a recognised primary direction',
            );

        } else {

            # Get the unabbreviated form
            $customDir = $session->currentDict->ivShow('primaryDirHash', $standardDir);
        }

        # The remaining arguments (once joined together in a simple string, eliminating multiple
        #   whitespace characters) should match an unallocated exit in the automapper's current room
        # Find the matching unallocated exit
        $string = join(' ', @args);
        OUTER: foreach my $dir ($roomObj->ivKeys('exitNumHash')) {

            my ($exitObj, $matchExitObj, $result);

            if (index($dir, $string) != -1) {

                # Check it's an unallocated exit
                $exitObj = $session->worldModelObj->ivShow(
                    'exitModelHash',
                    $roomObj->ivShow('exitNumHash', $dir),
                );

                # If the exit object doesn't exist (for some reason), then obviously we can't
                #   allocate a direction to it (the error message at the end of this function is
                #   seen instead)
                if ($exitObj) {

                    if (
                        $exitObj->drawMode ne 'temp_alloc'
                        && $exitObj->drawMode ne 'temp_unalloc'
                    ) {
                        return $self->error(
                            $session, $inputString,
                            'The exit \'' . $dir . '\' is not an unallocated exit',
                        );
                    }

                    # If any exits already use the same map direction, then it's an 'allocate
                    #   shadow' operation
                    INNER: foreach my $exitNum ($roomObj->ivValues('exitNumHash')) {

                        my $otherExitObj = $session->worldModelObj->ivShow(
                            'exitModelHash',
                            $roomObj->ivShow('exitNumHash', $dir),
                        );

                        if ($otherExitObj ne $exitObj && $otherExitObj->mapDir eq $standardDir) {

                            $matchExitObj = $otherExitObj;
                            last INNER;
                        }
                    }

                    if ($matchExitObj) {

                        $result = $session->worldModelObj->setExitShadow(
                            TRUE,                   # Update Automapper windows now
                            $roomObj,
                            $matchExitObj,
                            $exitObj,
                        );

                    } else {

                        $result = $session->worldModelObj->setExitMapDir(
                            $session,
                            TRUE,                   # Update Automapper windows now
                            $roomObj,
                            $exitObj,
                            $standardDir,
                        );
                    }

                    if (! $result) {

                        return $self->error(
                            $session, $inputString,
                            'Failed to allocate the \'' . $dir . '\' exit in the direction \''
                            . $customDir . '\'',
                        );

                    } else {

                        return $self->complete(
                            $session, $standardCmd,
                            'Allocated the \'' . $dir . '\' exit to the direction \''
                            . $customDir . '\'',
                        );
                    }
                }
            }
        }

        # No matching exit found
        return $self->error(
            $session, $inputString,
            'Couldn\'t find an unallocated exit matching \'' . $string . '\' in the automapper\'s'
            . ' current room',
        );
    }
}

{ package Games::Axmud::Cmd::AlternativeExit;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(Games::Axmud::Generic::Cmd Games::Axmud);

    ##################
    # Constructors

    sub new {

        # Create a new instance of this command object (there should only be one)
        #
        # Expected arguments
        #   (none besides $class)
        #
        # Return values
        #   'undef' if GA::Generic::Cmd->new reports an error
        #   Blessed reference to the new object on success

        my ($class, $check) = @_;

        # Setup
        my $self = Games::Axmud::Generic::Cmd->new('alternativeexit', TRUE, FALSE);
        if (! $self) {return undef}

        $self->{defaultUserCmdList} = ['ale', 'altexit', 'alternativeexit'];
        $self->{userCmdList} = \@{$self->{defaultUserCmdList}};
        $self->{descrip} = 'Sets an exit\'s alternative direction(s)';

        # Bless the object into existence
        bless $self, $class;
        return $self;
    }

    ##################
    # Methods

    sub do {

        my (
            $self, $session, $inputString, $userCmd, $standardCmd,
            $dir, $altDir,
            $check,
        ) = @_;

        # Local variables
        my ($roomObj, $exitNum, $exitObj);

        # Check for improper arguments
        if (! defined $dir || defined $check) {

            return $self->improper($session, $inputString);
        }

        # Import the automapper's current room (for convenience)
        $roomObj = $session->mapObj->currentRoom;

        # Check that allocating an exit is feasible
        if (! $roomObj) {

            return $self->error(
                $session, $inputString,
                'Can\'t set an alternative direction until a current room is set',
            );
        }

        # Get an exit in the current room matching $dir
        $exitNum = $roomObj->ivShow('exitNumHash', $dir);
        if (! defined $exitNum) {

            return $self->error(
                $session, $inputString,
                'The automapper\'s current room doesn\'t have an exit in the direction \''
                . $dir . '\'',
            );

        } else {

            $exitObj = $session->worldModelObj->ivShow('exitModelHash', $exitNum);
        }

        # Set (or reset) the exit's alternative direction(s)
        $session->worldModelObj->set_exitAltDir($exitObj, $altDir);

        if (! defined $altDir) {

            return $self->complete(
                $session, $standardCmd,
                'Reset alternative directions for the \'' . $dir . '\' exit',
            );

        } else {

            return $self->complete(
                $session, $standardCmd,
                'Set alternative directions for the \'' . $dir . '\' exit to \'' . $altDir . '\'',
            );
        }
    }
}

# Package must return a true value
1
