#!/usr/bin/perl
#
# Quick and dirty BF1942 remote console script
#
# $Id: bf1942-console,v 1.43 2003/03/01 23:01:59 nemesis Exp $
#
# Copyright (C) 2002 Kees Cook
# kees@outflux.net, http://outflux.net/
# 
# 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 2
# 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, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
# http://www.gnu.org/copyleft/gpl.html
#

# Comments further down discuss the communication protocols...

use strict;
use IO::Socket::INET;
use Term::ReadLine;

# stupid readline breaks things if EDITOR has "vi" in it...
delete $ENV{'EDITOR'};

# place to keep user/pass default instead of in script
my $RCFILE="$ENV{'HOME'}/.bf1942-console";
# format is:
#  HOSTNAME:USER:PASS

# ignore socket read/write errors
$SIG{'PIPE'} = 'IGNORE';

my $term = new Term::ReadLine 'Battlefield 1942 Console';
my $prompt = "> ";
my $OUT = $term->OUT || \*STDOUT;

my($host,$port)=split(/:/,$ARGV[0],2);
$port=4711 if (!defined($port));

# $server is the server offset if you're running multiple servers
# on the same machine.  Normally, this is just "0".
my $server=$ARGV[1]+0;
if (!defined($host))
{
        die "Usage: $0 Host[:Port] [Server Number]\n";
}

my $bf=undef; # battlefield 1942 socket

# Send messages
select($OUT); $|=1;

my $user=undef;
my $pass=undef;
# comment these in and change if you want to auto-login instead of prompting
# $user="admin";
# $pass="admin";

# Or try to read from an rc file...
if (-r $RCFILE && -s _)
{
        open(RC,$RCFILE) || warn "$RCFILE: $!\n";
        my $line;
        while (chomp($line=<RC>))
        {
                my($saved_host,$saved_user,$saved_pass)=split(/:/,$line,3);
                if ($saved_host eq $host)
                {
                        $user=$saved_user;
                        $pass=$saved_pass;
                        last;
                }
        }
        close(RC);
}
else
{
        #warn "No $RCFILE file\n";
}

if (!defined($user))
{
        print "Username: ";
        chomp($user=<STDIN>);
}
if (!defined($pass))
{
        print "Password: ";
        chomp($pass=<STDIN>);
}

my $running=1;

$readline'rl_completion_function = "main'complete";

while ($running)
{
        my $line;
        while ( defined ($line = $term->readline($prompt)) )
        {
                if ($line=~/\S/)
                {
                        $term->addhistory($line);

                        if ($line eq "quit")
                        {
                                $running=0;
                                last;
                        }
                        $line="quit" if ($line eq "admin.quit");
                        
                        my $code;
                        if (($code=sendConsole($line))==1)
                        {
                                print getLenStr($bf),"\n";
                        }
                        else
                        {
                                printf("[Server Error 0x%02X]\n",$code);
                        }
                }
        }
}
        
undef $bf;


# Post-authentication Client/Server communication is done via blocks of
# data.  Each data block starts with the number of strings contained in
# the block, then each string, null terminated, with a 4-byte size sent
# before the string.  Each 4-byte "length" is sent in unsigned long
# little-endian order.
#
# Bytes                  Meaning
# 1 2 3 4                Number of strings in this block
# 5 6 7 8                Size of string #1
# 9 ...                  String #1, with trailing NULL
# ...                    Size of string #2
# ...                    String #2, with trailing NULL
#


# Initial authentication is done via simple XOR'ing.  The server sends 10
# bytes, and the client XORs the authentication information against those
# 10 bytes (and re-uses them again for each of the 10 bytes it sends to
# the server).
#
# Server->Client:  10 bytes: XOR pattern to use
# Client->Server:  4 bytes: length of user name (including the trailing NULL)
# Client->Server:  X bytes: user (index-XOR'd, with trailing NULL)
# 			for example, if the user name was 15 "0x10" values,
# 			and the XOR pattern was the byte values from
# 			0x00-0x09, the resulting "encrypted" user name
# 			would be in hex:
# 			10,11,12,13,14,15,16,17,18,19,10,11,12,13,14,0
# Client->Server:  4 bytes: length of password (including the trailing NULL)
# Client->Server:  X bytes: password, XOR'd like the username, with final NULL
# Server->Client:  1 byte.  (value of "1" means authenticated)
#
# To start console communication, the client seems to send something that
# almost matches the description of "Post-Authentication" communication,
# but the count of strings is wrong.  Where "OFFSET" is the server instance
# to talk to (0 being first, 1 for the next server on the same IP, etc) it
# sends:
#
# 4 byte block length: 2
# NULL-terminated string: ConsoleRun OFFSET
# 4 byte block length: 2
# empty NULL-terminated string
# empty NULL-terminated string
#
# and then it reads back a regular block from the server, and then "normal"
# block-at-a-time communication starts up.

sub Authenticate
{
        my(@xor,$xmitlen,$bytes);

        # Create a new socket
        print "Authenticating: ";
        undef $bf;
        print "connect: ";
        $bf=new IO::Socket::INET->new(
                PeerPort=>$port,
                Proto=>'tcp',
                PeerAddr=>$host);
        if (!defined($bf))
        {
                warn "Cannot connect to $host:$port: $!\n";
                return 0;
        }

        print "xor: ";
        # gank the xor pattern
        if (sysread($bf,$bytes,10)!=10)
        {
                warn "Server did not send authentication challenge\n";
                return 0;
        }
        my @xor=unpack("C10",$bytes);

        # since these strings can have nulls in them, we can't(?)
        # use the regular string-sending functions...
        # encrypt & send the username
        print "user: ";
        my $xmitlen=length($user)+1;
        my $bytes=pack("V",$xmitlen);
        if (syswrite($bf,$bytes,4)!=4)
        {
                warn "Server hung up during authentication\n";
                return 0;
        }
        $bytes=pack("C" x $xmitlen,encryptXOR($user,@xor),0);
        if (syswrite($bf,$bytes,$xmitlen)!=$xmitlen)
        {
                warn "Server hung up during authentication\n";
                return 0;
        }
        
        # encrypt & send the password
        print "pass: ";
        $xmitlen=length($pass)+1;
        $bytes=pack("V",$xmitlen);
        if (syswrite($bf,$bytes,4)!=4)
        {
                warn "Server hung up during authentication\n";
                return 0;
        }
        $bytes=pack("C" x $xmitlen,encryptXOR($pass,@xor),0);
        if (syswrite($bf,$bytes,$xmitlen)!=$xmitlen)
        {
                warn "Server hung up during authentication\n";
                return 0;
        }
        
        # read back authentication result
        print "answer: ";
        if (sysread($bf,$bytes,1)!=1)
        {
                warn "Server hung up during authentication\n";
                return 0;
        }
        my @answer=unpack("C",$bytes);
        if ($answer[0]==1)
        {
                # start up console mode (what is this garbage?)
                unless (sendNum($bf,2) &&
                        sendLenStr($bf,"ConsoleRun $server") &&
                        sendNum($bf,2) &&
                        sendString($bf,"") &&
                        sendString($bf,""))
                {
                        warn "Server hung up after authentication\n";
                        return 0;
                }
        
                my $code=getNum($bf);
                my $response=getLenStr($bf);
        
                if ($code!=1)
                {
                        warn "Failed during 'ConsoleRun'!?\n";
                        return 0;
                }
                else
                {
                        print "okay\n";
                        return 1;
                }
        }
        else
        {
                warn "Bad username or password\n";
                return 0;
        }
}

sub encryptXOR
{
        my($plain,@mask)=@_;
        my $index=0;
        my $masklen=scalar(@mask);
        my @ciphers=();
        my $letter;

        my @letters=split(//,$plain);
        foreach $letter (@letters)
        {
                $letter=ord($letter) ^ $mask[$index];
                push(@ciphers,$letter);

                $index=($index+1) % $masklen;
        }

        return @ciphers;
}

sub sendString
{
        my ($sock,$line)=@_;

        my $xmitlen=length($line)+1;
        # what's the right way to pack/unpack a null string?
        my @bytes=();
        grep(push(@bytes,ord($_)),split(//,$line));
        my $bytes=pack("C" x $xmitlen,@bytes,0);
        return (syswrite($bf,$bytes,$xmitlen)==$xmitlen);
}

sub sendNum
{
        my ($sock,$num)=@_;

        my $bytes=pack("V",$num);
        return (syswrite($sock,$bytes,4)==4);
}

sub sendLenStr
{
        my ($sock,$line)=@_;

        return (sendNum($sock,length($line)+1) &&
                sendString($sock,$line));
}

sub getNum
{
        my ($sock)=@_;

        my $bytes;
        if (sysread($sock,$bytes,4)==4)
        {
                my @answer=unpack("V",$bytes);
                return $answer[0];
        }
        return undef;
}

sub getString
{
        my ($sock,$len)=@_;

        my $bytes=undef;
        if ($len>0 && sysread($sock,$bytes,$len)==$len)
        {

                # how do I unpack this?
                my @bytes=unpack("C" x $len,$bytes);
                $bytes="";
                grep($bytes.=chr($_),@bytes);
        }
        return $bytes;
}

sub getLenStr
{
        my ($sock)=@_;

        return getString($sock,getNum($sock));
}

sub sendConsole
{
        my ($command)=@_;
        
        my $okay=0;
        my $code=undef;

        while (!$okay)
        {
                $okay=1;
                if (!defined($bf) ||
                    !sendNum($bf,2) ||
                    !sendLenStr($bf,"ConsoleMessage $server") ||
                    !sendLenStr($bf,$command) ||
                    !defined($code=getNum($bf))
                )
                {
                        $okay=0;
                        die "Aborting.\n" if (!Authenticate());
                }
        }
        $code;
}

sub complete
{
        my($text, $line, $start) = @_;

my $cmds=<<__EOF__;
debug.doNotDisplayErrorWindow 1/0
ToolHandler
playerstats
game.sayTeam (Team chat)
game.sayAll (Say a message to everyone)
game.listPlayers (Lists players with there id numbers)
game.listMaps (Lists the maps and number assignments in the server rotation)
game.voteMap # (Votes to change the map to the number specified)
game.voteKickPlayer (ID) (Calls a vote to kick a player, to vote enter this command with the same number)
game.voteKickTeamPlayer (ID) (Same thing as votekick but only teammembers are allowed to vote)
game.changePlayerName (name) (Renames your player in-game)
game.dumpNetworkDebugStats 1/0
game.debugCallBackDisabled 1/0
game.useHUD 1/0 (If you want to play this game like a movie, try turning this off)
game.setShadows 1/0 (Toggles shadows on and off)
game.setEnvironmentMapping 1/0
game.setToolTip 1/0 (Toggles tool tip)
game.setRadioToolTip 1/0
game.setCrossHairColor # # # (Adjusts the color of your cross by RGB)
game.setStaticMiniMap 1/0 (When disabled the minimap will rotate to the direction you are facing)
game.setMiniMapTransparency # (The higher the number the more transparent)
game.RadioToolTipColor # # #
game.getIp (Prints the IP in the message window)
game.getLevelName (Prints the name of the level in the message window)
game.enableFreeCamera 1/0 (Enable/disable the ability to look while waiting to spawn)
game.killPlayer (ID) (Kills player with that id number, Admin only)
game.disconnect (Quits the current server)
game.suicide (When you just can't take the horror anymore)
game.setCommonMouseSensitivity # (Sensitivity is most commonly between 0-1)
game.setAirKeyboardSensitivity # (How sensitive your keyboard is when flying)
game.setAirMouseSensitivity # (How sensitive your mouse is when flying)
game.setAirMouseInvert 1/0 (1 for inverted, 0 for uninverted)
game.setInfMouseSensitivity #
game.setInfMouseInvert 1/0
game.setLandSeaKeyboardSensitivity #
game.setLandSeaMouseSensitivity #
game.setLandSeaMouseInvert 1/0
game.setConnection (1-4) (Sets the type of connection)
game.setDisableSound 1/0
game.setChannels # (Sets number of sound channels)
game.setMasterVolume # (How loud the sound will be)
game.setMenuMusicVolume # (How loud the menu music will be)
game.setMusicOnOff 1/0
game.setLocalizedDialog 0
game.setQuality #
game.setSoundDetail
game.setHardware #
game.setGameDisplayMode 800 600 32 0 (Those numbers are used as an example)
game.setDetailTexture #
game.setGraphicsQuality #
game.setLightmaps 1/0
game.setRenderWhenSpawnMenu 1
game.setMenuViewdistance #
game.setEffectsQuality #
game.setPerformance #
admin.getRemoteConsoleEnabled (Prints in the console if remote console is enabled)
admin.enableRemoteConsole (username) (password) # (tom_tAylor says to Run the AdminTool on other machines to remotely access and admin the bf1942 dedicated server )
admin.disableRemoteConsole
admin.maxAllowedConnectionType (type)
admin.enableRemoteAdmin (password) (Server must execute this command every map. May contain either only letters or only numbers, but no spaces)
admin.disableRemoteAdmin (Stops players from using remote admin)
admin.execremotecommand "command" (Must have command in quation marks to work)
admin.voteMapMajority # (Number should be greater than .01 and less than 1. Number is percentage of voters required to pass. Ex. 0.6 = 60%)
admin.voteKickPlayerMajority
admin.voteKickTeamPlayerMajority
admin.enableMapVote 1/0 (Toggle if you want to allow people to vote for another map)
admin.enableKickPlayerVote 1/0
admin.enableKickTeamPlayerVote 1/0
admin.votingTime # (Sets the number of seconds a player has to cast a vote)
admin.kickPlayer (ID) (Kicks the player with the id you specify)
admin.banPlayer (ID)
admin.changeMap (map name) (Changes the map to the map you Specify by name)
admin.addAddressToBanList (IP)
admin.removeAddressFromBanList (IP)
admin.listBannedAdresses (Lists all banned IPs)
admin.clearBanList (Clears all banned ips)
admin.banTime # (How long someone is banned for, I'm not exactly sure how this works)
admin.tagPlayer (ID) (Not sure)
admin.bandWidthChokeLimit # (again not sure)
admin.allownosecam 1/0 (Allows player to turn off HUD while flying)
admin.externalviews 1/0 (Allows/disallows 3d person view and nose cam)
admin.togglegamepause (Extremely annoying, don't pause unless its for a good reason)
admin.setTicketRatio #
admin.autoBalanceTeam 1/0 (Will disable players from making teams un-even)
admin.delayBeforeStartingGame # (Time before the game will start)
admin.roundDelayBeforeStartingGame # (Time before a new round will start)
admin.soldierFFRatio # (The amount of damage done by a soldier to a teammate)
admin.vehicleFFratio #
admin.soldierFFRatioOnSplash # (The amount of damage is done by FF splash)
admin.vehcileFFRatioOnSplash #
admin.kickBack # (How much you get knocked back by getting hit)
admin.kickBackOnSplash #
admin.timeLimit # (How long the match last)
admin.scoreLimit # (Greatest score before it willstop the match)
admin.restartMap (Restarts the current map)
admin.setNextLevel (map) (Changes or adds the next map)
admin.timeBeforeRestartMap # (How much time until the map will restart)
admin.SetNrOfRounds # (yes that is spelled correctly, sets the number of rounds)
admin.timeToNextWave # (Not sure)
admin.spawnWaveTime #
admin.quit Shutdown server
renderer.allowAllRefreshRates 1/0 (Allows you to select the refresh rates in Options/Video)
renderer.extrapolateFrame 1/0
renderer.mipMapBias # (The lower the number, the more detailed the textures are. Anything greater than 13 is the same)
renderer.setVSyncEnabled 0/1 0/1 (You have to list both 1/0 for it to turn on/off)
renderer.getVSyncEnabled (Gives you a 1 or 0 if it is turned on/off)
shadow
hud
Console.showfps 1/0 (FPS appears in the top left corner, I recommend you turn off the Message display at the top to see it)
Console.showstats 1/0 (Shows FPS and more info)
Sound.setDopplerFactor 1/0
Sound.setRolloffFactor 1/0
Sound.setDistanceFactor 1/0
Sound.setPitchChangeRate #
Sound.showSoundInfo 0
Sound.drawSoundObjects 0
player
memory
profiler.enable 1/0
profiler.report # # 1/0 (The first number is a float and the second is an integer)
profiler.reportfile # # 1/0 (The first number is a float and the second is an integer)
profiler.reportfile (file) # # 1/0 (The first number is a float and the second is an integer)
profiler.reset
profiler.enableVTune
profiler.enableAllTimers
profiler.disableAllTimers
profiler.enableTimer (timer number)
profiler.disableTimer (Timer number)
profiler.enableVTuneForTimer (Timer number)
profiler.disableVTuneForTimer (Timer number)
profiler.enableTimerByName (Timer Name)
profiler.disableTimerByName (Timer Name)
profiler.enableVTuneForTimerFromName (Timer Name)
profiler.disableVTuneForTimerFromName (Timer Name)
profiler.listTimers
profiler.reportPerTimer
quit Quit out of remote console
__EOF__
        my @full=split(/\n/,$cmds);
        my %cmds=();
        foreach $cmds (@full)
        {
                my ($cmd,$args)=split(/\s+/,$cmds,2);
                $cmds{$cmd}=$args;
        }
        
        ## return commands which may match if at the beginning....
        my @list=keys %cmds;
        return grep(/^$text/, @list) if $start == 0;

        my ($cmd)=split(/\s+/,$line);
        
        if (defined($cmds{$cmd}))
        {
                print "\n$cmds{$cmd}\n$prompt$line";
        }
        return ();
}
