# 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,