Merge Receive and Send base modules

Forum closed. All further discussion to be discussed at https://github.com/OpenKore/

Moderator: Moderators

Message
Author
User avatar
kLabMouse
Administrator
Administrator
Posts: 1301
Joined: 24 Apr 2008, 12:02

Re: Merge Receive and Send base modules

#21 Post by kLabMouse »

Great.

About the GG thing.
I think that there should be 2 phases: Account/Char and Zone.
But it needs some sniffing on working Client with GG to find out.

EternalHarvest
Developers
Developers
Posts: 1798
Joined: 05 Dec 2008, 05:42
Noob?: Yes

Re: Merge Receive and Send base modules

#22 Post by EternalHarvest »

Looks like there's need to mangle packets right in the handler. XKore2::MapServer and parseOutgoingClientMessage are basically written like that, except they haven't used parse(). Hooks alone won't be a very good idea here, because it looks like the same two examples are two layers of parsing packet stream twice and each one's mangle should be in its place. Old mangle hooks should probably be rethought to be called only once and not from any possible instance of Network::PacketParser.

User avatar
kLabMouse
Administrator
Administrator
Posts: 1301
Joined: 24 Apr 2008, 12:02

Re: Merge Receive and Send base modules

#23 Post by kLabMouse »

EternalHarvest wrote:Looks like there's need to mangle packets right in the handler. XKore2::MapServer and parseOutgoingClientMessage are basically written like that, except they haven't used parse(). Hooks alone won't be a very good idea here, because it looks like the same two examples are two layers of parsing packet stream twice and each one's mangle should be in its place. Old mangle hooks should probably be rethought to be called only once and not from any possible instance of Network::PacketParser.
OK.

EternalHarvest
Developers
Developers
Posts: 1798
Joined: 05 Dec 2008, 05:42
Noob?: Yes

Re: Merge Receive and Send base modules

#24 Post by EternalHarvest »

No more parseIncomingMessage and parseOutgoingClientMessage.

Code: Select all

Index: src/Globals.pm
===================================================================
--- src/Globals.pm	(revision 7792)
+++ src/Globals.pm	(working copy)
@@ -29,7 +29,7 @@
 	config  => [qw(%arrowcraft_items %avoid @chatResponses %cities_lut %config %consoleColors %directions_lut %equipTypes_lut %equipSlot_rlut %equipSlot_lut %haircolors @headgears_lut %items_control %items_lut %itemSlotCount_lut %itemsDesc_lut %itemTypes_lut %jobs_lut %maps_lut %masterServers %monsters_lut %npcs_lut %packetDescriptions %portals_lut %responses %sex_lut %shop %skillsDesc_lut %lookHandle %skillsArea %skillsEncore %spells_lut %emotions_lut %timeout $char %mon_control %priority %routeWeights %pickupitems %rpackets %itemSlots_lut %statusHandle %statusName %effectName %portals_los %stateHandle %ailmentHandle %mapTypeHandle %mapPropertyTypeHandle %mapPropertyInfoHandle %elements_lut %mapAlias_lut %quests_lut)],
 	ai      => [qw(@ai_seq @ai_seq_args %ai_v $AI $AI_forcedOff %targetTimeout)],
 	state   => [qw($accountID $cardMergeIndex @cardMergeItemsID $charID @chars @chars_old %cart @friendsID %friends %incomingFriend $field %homunculus $itemsList @itemsID %items $monstersList @monstersID %monsters @npcsID %npcs $npcsList @playersID %players @portalsID @portalsID_old %portals %portals_old $portalsList @storeList $currentChatRoom @currentChatRoomUsers @chatRoomsID %createdChatRoom %chatRooms @skillsID %storage @storageID @arrowCraftID %guild %incomingGuild @spellsID %spells @unknownPlayers @unknownNPCs $useArrowCraft %currentDeal %incomingDeal %outgoingDeal @identifyID @partyUsersID %incomingParty @petsID %pets @venderItemList $venderID $venderCID @venderListsID @buyerItemList $buyerID $buyerCID @buyerListsID @articles $articles %venderLists %buyerLists %monsters_old @monstersID_old %npcs_old %items_old %players_old @playersID_old @servers $sessionID $sessionID2 $accountSex $accountSex2 $map_ip $map_port $KoreStartTime $secureLoginKey $initSync $lastConfChangeTime $petsList $playersList $portalsList @playerNameCacheIDs %playerNameCache %pet $pvp @cashList $slavesList @slavesID %slaves)],
-	network => [qw($remote_socket $net $messageSender $charServer $conState $conState_tries $encryptVal $ipc $bus $lastPacketTime $masterServer $lastswitch $packetParser $bytesSent $bytesReceived $incomingMessages $outgoingClientMessages $enc_val1 $enc_val2 $captcha_state)],
+	network => [qw($remote_socket $net $messageSender $charServer $conState $conState_tries $encryptVal $ipc $bus $masterServer $lastswitch $packetParser $clientPacketHandler $bytesSent $incomingMessages $outgoingClientMessages $enc_val1 $enc_val2 $captcha_state)],
 	interface => [qw($interface)],
 	misc    => [qw($quit $reconnectCount @lastpm %lastpm @privMsgUsers %timeout_ex $shopstarted $dmgpsec $totalelasped $elasped $totaldmg %overallAuth %responseVars %talk $startTime_EXP $startingzeny @monsters_Killed $bExpSwitch $jExpSwitch $totalBaseExp $totalJobExp $shopEarned %itemChange $XKore_dontRedirect $monkilltime $monstarttime $startedattack $firstLoginMap $sentWelcomeMessage $versionSearch $monsterBaseExp $monsterJobExp %descriptions %flags %damageTaken $logAppend @sellList $userSeed $taskManager $repairList $mailList $auctionList $questList $hotkeyList $devotionList $cookingList)],
 	syncs => [qw($syncSync $syncMapSync)],
@@ -455,14 +455,16 @@
 # Network
 our $remote_socket;	# Unused, but required for outdated plugins
 our $net;
+our $packetParser;
 our $messageSender;
+our $clientPacketHandler;
 our $charServer;
 our $conState;
 our $conState_tries;
 our $encryptVal;
 our $ipc;
 our $bus;
-our $lastPacketTime;
+#our $lastPacketTime; # replaced by $packetParser->{lastPacketTime}
 our $masterServer;
 our $incomingMessages;
 our $outgoingClientMessages;
@@ -516,7 +518,7 @@
 our $taskManager;
 
 our $bytesSent = 0;
-our $bytesReceived = 0;
+#our $bytesReceived = 0; # replaced by $packetParser->{bytesProcessed}
 
 our $syncSync;
 our $syncMapSync;
Index: src/Interface/Wx/StatView/Exp.pm
===================================================================
--- src/Interface/Wx/StatView/Exp.pm	(revision 7792)
+++ src/Interface/Wx/StatView/Exp.pm	(working copy)
@@ -3,7 +3,7 @@
 use strict;
 use base 'Interface::Wx::StatView';
 
-use Globals qw/$char $conState $startTime_EXP $startingzeny $totalBaseExp $totalJobExp $bytesSent $bytesReceived/;
+use Globals qw/$char $conState $packetParser $startTime_EXP $startingzeny $totalBaseExp $totalJobExp $bytesSent/;
 use Translation qw/T TF/;
 use Utils qw/formatNumber timeConvert timeOut/;
 
@@ -86,7 +86,7 @@
 		$self->set ('zeny', formatNumber ($value = $char->{zeny} - $startingzeny));
 		$self->set ('zenyPerHour', formatNumber (int $value / $bottingHours));
 		$self->set ('bytesSent', formatNumber ($bytesSent));
-		$self->set ('bytesReceived', formatNumber ($bytesReceived));
+		$self->set ('bytesReceived', $packetParser && formatNumber($packetParser->{bytesProcessed}));
 	}
 	
 	$self->GetSizer->Layout;
Index: src/doc/modules.txt
===================================================================
--- src/doc/modules.txt	(revision 7792)
+++ src/doc/modules.txt	(working copy)
@@ -21,6 +21,7 @@
 Task::Wait
 TaskManager
 Modules
+Network::ClientReceive
 Network::DirectConnection
 Network::PacketParser
 Network::Receive
Index: src/Base/Ragnarok/CharServer.pm
===================================================================
--- src/Base/Ragnarok/CharServer.pm	(revision 7792)
+++ src/Base/Ragnarok/CharServer.pm	(working copy)
@@ -8,7 +8,7 @@
 use Base::RagnarokServer;
 use base qw(Base::RagnarokServer);
 use Misc;
-use Globals qw(%config $packetParser);
+use Globals qw(%config);
 
 use constant SESSION_TIMEOUT => 120;
 use constant DUMMY_CHARACTER => {
@@ -69,14 +69,19 @@
 	return $_[0]->{charBlockSize};
 }
 
-sub process_0065 {
+sub game_login {
 	# Character server login.
-	my ($self, $client, $message) = @_;
-	my ($accountID, $sessionID, $sessionID2, $gender) = unpack('x2 a4 V V x2 C', $message);
-	my $session = $self->{sessionStore}->get($sessionID);
+	my ($self, $args, $client) = @_;
+	# maybe sessionstore should store sessionID as bytes?
+	my $session = $self->{sessionStore}->get(unpack('V', $args->{sessionID}));
 
-	if (!$session || $session->{accountID} ne $accountID || $session->{sessionID} != $sessionID
-	  || $session->{sex} != $gender || $session->{state} ne 'About to select character') {
+	unless (
+		$session && $session->{accountID} eq $args->{accountID}
+		# maybe sessionstore should store sessionID as bytes?
+		&& pack('V', $session->{sessionID}) eq $args->{sessionID}
+		&& $session->{sex} == $args->{accountSex}
+		&& $session->{state} eq 'About to select character'
+	) {
 		$client->close();
 
 	} else {
@@ -91,7 +96,7 @@
 			next if (!$char);
 
 			$output .= pack(
-				$packetParser->received_characters_unpackString,
+				$self->{recvPacketParser}->received_characters_unpackString,
 				$char->{charID},	# character ID
 				$char->{exp},		# base experience
 				$char->{zeny},		# zeny
@@ -143,11 +148,11 @@
 		$self->{sessionStore}->mark($session);
 		$client->{session} = $session;
 		$session->{time} = time;
-		$client->send($accountID);
+		$client->send($args->{accountID});
 		if ($config{XKore_altCharServer} == 1){
 			$client->send(pack('C2 v', 0x72, 0x00, length($output) + 4) . $output);
 		}else{
-			$client->send($packetParser->reconstruct({
+			$client->send($self->{recvPacketParser}->reconstruct({
 				switch => 'received_characters',
 				charInfo => $output,
 				
@@ -163,19 +168,18 @@
 
 }
 
-sub process_0066 {
+sub char_login {
 	# Select character.
-	my ($self, $client, $message) = @_;
+	my ($self, $args, $client) = @_;
 	my $session = $client->{session};
 	if ($session) {
 		$self->{sessionStore}->mark($session);
-		my ($charIndex) = unpack('x2 C', $message);
 		my @characters = $self->getCharacters();
-		if (!$characters[$charIndex]) {
+		if (!$characters[$args->{slot}]) {
 			# Invalid character selected.
 			$client->send(pack('C*', 0x6C, 0x00, 0));
 		} else {
-			my $char = $characters[$charIndex];
+			my $char = $characters[$args->{slot}];
 			my $charInfo = $self->{mapServer}->getCharInfo($session);
 			if (!$charInfo) {
 				# We can't get the character information for some reason.
@@ -186,7 +190,7 @@
 				
 				$session->{charID} = $char->{charID};
 				$session->{state} = 'About to load map';
-				$client->send($packetParser->reconstruct({
+				$client->send($self->{recvPacketParser}->reconstruct({
 					switch => 'received_character_ID_and_Map',
 					charID => $char->{charID},
 					mapName => $charInfo->{map},
@@ -199,27 +203,27 @@
 	$client->close();
 }
 
-sub process_0187 {
+sub ban_check {
 	# Ban check.
 	# Doing nothing seems to work.
 }
 
-sub process_0067 {
+sub char_create {
 	# Character creation.
-	my ($self, $client) = @_;
+	my ($self, $args, $client) = @_;
 	# Deny it.
 	$client->send(pack('C*', 0x6E, 0x00, 2));
 }
 
-sub process_0067 {
+sub char_delete {
 	# Character deletion.
-	my ($self, $client) = @_;
+	my ($self, $args, $client) = @_;
 	# Deny it.
 	$client->send(pack('C*', 0x70, 0x00, 1));
 }
 
 sub unhandledMessage {
-	my ($self, $client) = @_;
+	my ($self, $args, $client) = @_;
 	$client->close();
 }
 
Index: src/Base/Ragnarok/MapServer.pm
===================================================================
--- src/Base/Ragnarok/MapServer.pm	(revision 7792)
+++ src/Base/Ragnarok/MapServer.pm	(working copy)
@@ -5,7 +5,6 @@
 no encoding 'utf8';
 use bytes;
 
-use Globals;
 use Modules 'register';
 use Base::RagnarokServer;
 use Misc;
@@ -38,13 +37,19 @@
 	#my ($self, $session) = @_;
 }
 
-sub handleLogin {
-	my ($self, $client, $accountID, $charID, $sessionID, $gender) = @_;
-	my $session = $self->{sessionStore}->get($sessionID);
+sub map_login {
+	my ($self, $args, $client) = @_;
+	# maybe sessionstore should store sessionID as bytes?
+	my $session = $self->{sessionStore}->get(unpack('V', $args->{sessionID}));
 
-	if (!$session || $session->{accountID} ne $accountID || $session->{sessionID} != $sessionID
-	  || $session->{sex} != $gender || $session->{charID} ne $charID
-	  || $session->{state} ne 'About to load map') {
+	unless (
+		$session && $session->{accountID} eq $args->{accountID}
+		# maybe sessionstore should store sessionID as bytes?
+		&& pack('V', $session->{sessionID}) eq $args->{sessionID}
+		&& $session->{sex} == $args->{sex}
+		&& $session->{charID} eq $args->{charID}
+		&& $session->{state} eq 'About to load map'
+	) {
 		$client->close();
 
 	} else {
@@ -52,20 +57,20 @@
 		$client->{session} = $session;
 
 		# # TODO: use result from the server?
-		# if (exists $packetParser->{packet_lut}{define_check}) {
-		# 	$client->send($packetParser->reconstruct({
+		# if (exists $self->{recvPacketParser}{packet_lut}{define_check}) {
+		# 	$client->send($self->{recvPacketParser}->reconstruct({
 		# 		switch => 'define_check',
 		# 		result => Network::Receive::ServerType0::DEFINE__BROADCASTING_SPECIAL_ITEM_OBTAIN | Network::Receive::ServerType0::DEFINE__RENEWAL_ADD_2,
 		# 	}));
 		# }
 
-		if (exists $packetParser->{packet_lut}{account_id}) {
-			$client->send($packetParser->reconstruct({
+		if (exists $self->{recvPacketParser}{packet_lut}{account_id}) {
+			$client->send($self->{recvPacketParser}->reconstruct({
 				switch => 'account_id',
-				accountID => $accountID,
+				accountID => $args->{accountID},
 			}));
 		} else {
-			$client->send($accountID);
+			$client->send($args->{accountID});
 		}
 
 		my $charInfo = $self->getCharInfo($session);
@@ -73,45 +78,16 @@
 		shiftPack(\$coords, $charInfo->{x}, 10);
 		shiftPack(\$coords, $charInfo->{y}, 10);
 		shiftPack(\$coords, 0, 4);
-		$client->send($packetParser->reconstruct({
+		$client->send($self->{recvPacketParser}->reconstruct({
 			switch => 'map_loaded',
 			syncMapSync => int time,
 			coords => $coords,
 		}));
 	}
+	
+	$args->{mangle} = 2;
 }
 
-sub process_0072 {
-	my ($self, $client, $message) = @_;
-	if ($self->{serverType} == 0 || $self->{serverType} == 21) {
-		# Map server login.
-		my ($accountID, $charID, $sessionID, $gender) = unpack('x2 a4 a4 V x4 C', $message);
-		$self->handleLogin($client, $accountID, $charID, $sessionID, $gender);
-		return 1;
-	} elsif ($self->getServerType()  == 8) {
-		# packet sendSkillUse
-		$self->unhandledMessage($client, $message);
-		return 0;
-	} else { #oRO and pRO and idRO
-		my ($accountID, $charID, $sessionID, $gender) = unpack('x2 a4 x5 a4 x2 V x4 C', $message);
-		$self->handleLogin($client, $accountID, $charID, $sessionID, $gender);
-		return 1;
-	}
-}
-
-sub process_00F3 {
-	my ($self, $client, $message) = @_;
-	if ($self->getServerType() == 18) {
-		# Map server login.
-		my ($charID, $accountID, $sessionID, $gender) = unpack('x5 a4 a4 x V x9 x4 C', $message);
-		$self->handleLogin($client, $accountID, $charID, $sessionID, $gender);
-		return 1;
-	} else {
-		$self->unhandledMessage($client, $message);
-		return 0;
-	}
-}
-
 #	$msg = pack("C*", 0x9b, 0, 0x39, 0x33) .
 #		$accountID .
 #		pack("C*", 0x65) .
@@ -121,36 +97,8 @@
 #		pack("V", getTickCount()) .
 #		pack("C*", $sex);
 
-sub process_009B {
-	my ($self, $client, $message) = @_;
-
-	if ($self->getServerType() == 8) {
-		# Map server login.
-		my ($accountID , $charID, $sessionID, $gender) = unpack('x4 a4 x a4 x4 V x4 C', $message);
-		$self->handleLogin($client, $accountID, $charID, $sessionID, $gender);
-		return 1;
-	} else {
-		$self->unhandledMessage($client, $message);
-		return 0;
-	}
-}
-
-sub process_0436 {
-	my ($self, $client, $message) = @_;
-
-	if ($self->getServerType() =~ m/^8_5$/) {
-		# Map server login.
-		my ($accountID , $charID, $sessionID, $gender) = unpack('x2 a4 a4 V x4 C', $message);
-		$self->handleLogin($client, $accountID, $charID, $sessionID, $gender);
-		return 1;
-	} else {
-		$self->unhandledMessage($client, $message);
-		return 0;
-	}
-}
-
 sub unhandledMessage {
-	my ($self, $client) = @_;
+	my ($self, $args, $client) = @_;
 	if (!$client->{session}) {
 		$client->close();
 		return 0;
Index: src/Base/Ragnarok/AccountServer.pm
===================================================================
--- src/Base/Ragnarok/AccountServer.pm	(revision 7792)
+++ src/Base/Ragnarok/AccountServer.pm	(working copy)
@@ -9,7 +9,6 @@
 	Base::Ragnarok::AccountServer::AccountBanned
 );
 
-use Globals qw($packetParser);
 use Modules 'register';
 use Base::RagnarokServer;
 use base qw(Base::RagnarokServer);
@@ -61,26 +60,15 @@
 	die "This is an abstract method and has not been implemented.";
 }
 
-sub process_0064 {
-	my ($self, $client, $message) = @_;
-	my ($switch, $version, $username, $password, $master_version) = unpack("v V Z24 Z24 C1", $message);
+sub master_login {
+	my ($self, $args, $client) = @_;
 
-	if ($switch == 0x02B0) {
-		# TODO: merge back with sendMasterHANLogin
-		my $key = pack('C24', (6, 169, 33, 64, 54, 184, 161, 91, 81, 46, 3, 213, 52, 18, 0, 6, 61, 175, 186, 66, 157, 158, 180, 48));
-		my $chain = pack('C24', (61, 175, 186, 66, 157, 158, 180, 48, 180, 34, 218, 128, 44, 159, 172, 65, 1, 2, 4, 8, 16, 32, 128));
-		my $in = pack('a24', $password);
-		my $rijndael = Utils::Rijndael->new();
-		$rijndael->MakeKey($key, $chain, 24, 24);
-		$password = unpack("Z24", $rijndael->Decrypt($in, undef, 24, 0));
-	}
-
 	my $sessionID = $self->{sessionStore}->generateSessionID();
 	my %session = (
 		sessionID => $sessionID,
 		sessionID2 => $sessionID
 	);
-	my $result = $self->login(\%session, $username, $password);
+	my $result = $self->login(\%session, @{$args}{qw(username password)});
 	if ($result == LOGIN_SUCCESS) {
 		my $output = '';
 		$self->{sessionStore}->add(\%session);
@@ -100,8 +88,9 @@
 			);
 		}
 		
-		$client->send($packetParser->reconstruct({
+		$client->send($self->{recvPacketParser}->reconstruct({
 			switch => 'account_server_info',
+			# maybe sessionstore should store sessionID as bytes?
 			sessionID => pack('V', $session{sessionID}),
 			accountID => $session{accountID},
 			sessionID2 => pack('V', $session{sessionID2}),
@@ -111,19 +100,19 @@
 		$client->close();
 
 	} elsif ($result == ACCOUNT_NOT_FOUND) {
-		$client->send($packetParser->reconstruct({
+		$client->send($self->{recvPacketParser}->reconstruct({
 			switch => 'login_error',
 			type => Network::Receive::ServerType0::REFUSE_INVALID_ID,
 		}));
 		$client->close();
 	} elsif ($result == PASSWORD_INCORRECT) {
-		$client->send($packetParser->reconstruct({
+		$client->send($self->{recvPacketParser}->reconstruct({
 			switch => 'login_error',
 			type => Network::Receive::ServerType0::REFUSE_INVALID_PASSWD,
 		}));
 		$client->close();
 	} elsif ($result == ACCOUNT_BANNED) {
-		$client->send($packetParser->reconstruct({
+		$client->send($self->{recvPacketParser}->reconstruct({
 			switch => 'login_error',
 			type => Network::Receive::ServerType0::REFUSE_NOT_CONFIRMED,
 		}));
@@ -133,11 +122,10 @@
 	}
 }
 
-# sendClientMD5Hash
-sub process_0204 {}
+sub client_hash {}
 
 sub unhandledMessage {
-	my ($self, $client) = @_;
+	my ($self, $args, $client) = @_;
 	$client->close();
 }
 
Index: src/Base/RagnarokServer.pm
===================================================================
--- src/Base/RagnarokServer.pm	(revision 7792)
+++ src/Base/RagnarokServer.pm	(working copy)
@@ -17,6 +17,8 @@
 	my $self = $class->SUPER::new($port, $host);
 	$self->{serverType} = $serverType;
 	$self->{rpackets} = $rpackets;
+	$self->{recvPacketParser} = Network::Receive->create(undef, $serverType);
+	$self->{sendPacketParser} = Network::Send->create(undef, $serverType);
 	return $self;
 }
 
@@ -50,20 +52,10 @@
 sub onClientData {
 	my ($self, $client, $data) = @_;
 	$client->{tokenizer}->add($data);
-	my $type;
-	while (my $message = $client->{tokenizer}->readNext(\$type)) {
-		if ($type == Network::MessageTokenizer::KNOWN_MESSAGE) {
-			my $ID = Network::MessageTokenizer::getMessageID($message);
-			my $handler = $self->can('process_' . (($ID eq $masterServer->{masterLogin_packet}) ? '0064' : $ID)); # temporary fix for servers that changed the masterLogin_packet switch
-			if ($handler) {
-				$handler->($self, $client, $message);
-			} else {
-				$self->unhandledMessage($client, $message);
-			}
-		} else {
-			$client->close();
-		}
-	}
+	
+	$client->{outbox} && $client->{outbox}->add($_) for $self->{sendPacketParser}->process(
+		$client->{tokenizer}, $self, $client
+	);
 }
 
 sub displayMessage {
@@ -75,4 +67,9 @@
 sub unhandledMessage {
 }
 
+sub unknownMessage {
+	my ($self, $args, $client) = @_;
+	$client->close;
+}
+
 1;
Index: src/Commands.pm
===================================================================
--- src/Commands.pm	(revision 7792)
+++ src/Commands.pm	(working copy)
@@ -1410,9 +1410,9 @@
 		configModify("debug", 2);
 
 	} elsif ($arg1 eq "info") {
-		my $connected = "server=".($net->serverAlive ? "yes" : "no").
+		my $connected = $net && "server=".($net->serverAlive ? "yes" : "no").
 			",client=".($net->clientAlive ? "yes" : "no");
-		my $time = sprintf("%.2f", time - $lastPacketTime);
+		my $time = $packetParser && sprintf("%.2f", time - $packetParser->{lastPacketTime});
 		my $ai_timeout = sprintf("%.2f", time - $timeout{'ai'}{'time'});
 		my $ai_time = sprintf("%.4f", time - $ai_v{'AI_last_finished'});
 
@@ -1424,7 +1424,7 @@
 			"\$timeout{ai}: %.2f secs ago  (value should be >%s)\n" .
 			"Last AI() call: %.2f secs ago\n" .
 			"-------------------------------------------\n",
-		$conState, $connected, $AI, $AI_forcedOff, @ai_seq, $time, $ai_timeout, 
+		$conState, $connected, $AI, $AI_forcedOff, "@ai_seq", $time, $ai_timeout, 
 		$timeout{'ai'}{'timeout'}, $ai_time), "list";
 	}
 }
@@ -1601,7 +1601,7 @@
 		$totalelasped = 0;
 		undef %itemChange;
 		$bytesSent = 0;
-		$bytesReceived = 0;
+		$packetParser->{bytesProcessed} = 0 if $packetParser;
 		message T("Exp counter reset.\n"), "success";
 		return;
 	}
@@ -1649,7 +1649,7 @@
 			timeConvert($w_sec), formatNumber($totalBaseExp), $percentB, formatNumber($totalJobExp), $percentJ,
 			formatNumber($bExpPerHour), $percentBhr, formatNumber($jExpPerHour), $percentJhr,
 			formatNumber($zenyMade), formatNumber($zenyPerHour), timeConvert($EstB_sec), timeConvert($EstJ_sec), 
-			$char->{'deathCount'}, formatNumber($bytesSent), formatNumber($bytesReceived)), "info";
+			$char->{'deathCount'}, formatNumber($bytesSent), $packetParser && formatNumber($packetParser->{bytesProcessed})), "info";
 			
 		if ($arg1 eq "") {
 			message("---------------------------------\n", "list");
Index: src/functions.pl
===================================================================
--- src/functions.pl	(revision 7792)
+++ src/functions.pl	(working copy)
@@ -23,6 +23,7 @@
 use Interface;
 use Network::Receive;
 use Network::Send ();
+use Network::ClientReceive;
 use Network::PaddedPackets;
 use Network::MessageTokenizer;
 use Commands;
@@ -306,6 +307,8 @@
 	our $XKore_dontRedirect = 0;
 	my $XKore_version = $config{XKore};
 	eval {
+		$clientPacketHandler = Network::ClientReceive->new;
+		
 		if ($XKore_version eq "1") {
 			# Inject DLL to running Ragnarok process
 			require Network::XKore;
@@ -694,26 +697,10 @@
 	if (defined($data) && length($data) > 0) {
 		Benchmark::begin("parseMsg") if DEBUG;
 
-		my $type;
 		$incomingMessages->add($data);
-		while ($data = $incomingMessages->readNext(\$type)) {
-			if ($type == Network::MessageTokenizer::KNOWN_MESSAGE) {
-				parseIncomingMessage($data);
-			} else {
-				if ($type == Network::MessageTokenizer::UNKNOWN_MESSAGE) {
-					# Unknown message - ignore it
-					my $messageID = Network::MessageTokenizer::getMessageID($data);
-					if (!existsInList($config{debugPacket_exclude}, $messageID)) {
-						warning TF("Packet Tokenizer: Unknown switch: %s\n", $messageID), "connection";
-						visualDump($data, "<< Received unknown packet") if ($config{debugPacket_unparsed});
-					}
-				} elsif ($config{debugPacket_received}) {
-					debug "Received account ID\n", "parseMsg", 0 ;
-				}
-				# Pass it along to the client, whatever it is
-				$net->clientSend($data);
-			}
-		}
+		$net->clientSend($_) for $packetParser->process(
+			$incomingMessages, $packetParser
+		);
 		$net->clientFlush() if (UNIVERSAL::isa($net, 'Network::XKoreProxy'));
 		Benchmark::end("parseMsg") if DEBUG;
 	}
@@ -723,9 +710,9 @@
 	if (defined($data) && length($data) > 0) {
 		my $type;
 		$outgoingClientMessages->add($data);
-		while ($data = $outgoingClientMessages->readNext(\$type)) {
-			parseOutgoingClientMessage($data);
-		}
+		$net->serverSend($_) for $messageSender->process(
+			$outgoingClientMessages, $clientPacketHandler
+		);
 	}
 
 	# GameGuard support
@@ -987,6 +974,7 @@
 }
 
 
+=pod
 #######################################
 #######################################
 # Parse RO Client Send Message
@@ -1004,7 +992,7 @@
 	}
 	my $switch = Network::MessageTokenizer::getMessageID($msg);
 	
-	parseMessage_pre('ro_sent', $switch, $msg, $sendMsg);
+	parseMessage_pre('Network::Send', $switch, $msg, $sendMsg);
 
 	my $serverType = $masterServer->{serverType};
 
@@ -1311,12 +1299,13 @@
 		}
 	}
 }
+=cut
 
 sub parseMessage_pre {
 	my ($mode, $switch, $msg, $realMsg) = @_;
 	my ($title, $config_suffix, $desc_key, $dumpMethod5_word, $hook) = @{{
-		'received' => ['<< Received packet:', 'received', 'Recv', 'recv', 'parseMsg/pre'],
-		'ro_sent' => ['<< Sent by RO client:', 'ro_sent', 'Send', 'send', 'RO_sendMsg_pre'],
+		'Network::Receive' => ['<< Received packet:', 'received', 'Recv', 'recv', 'parseMsg/pre'],
+		'Network::ClientReceive' => ['<< Sent by RO client:', 'ro_sent', 'Send', 'send', 'RO_sendMsg_pre'],
 	}->{$mode}};
 	
 	if ($config{'debugPacket_'.$config_suffix} && !existsInList($config{'debugPacket_exclude'}, $switch) ||
Index: src/Network/PacketParser.pm
===================================================================
--- src/Network/PacketParser.pm	(revision 7792)
+++ src/Network/PacketParser.pm	(working copy)
@@ -23,11 +23,13 @@
 use encoding 'utf8';
 use Carp::Assert;
 use Scalar::Util;
+use Time::HiRes qw(time);
 
 use Globals;
 #use Settings;
 use Log qw(message warning error debug);
 #use FileParsers;
+use I18N qw(bytesToString stringToBytes);
 use Interface;
 use Network;
 use Network::MessageTokenizer;
@@ -69,6 +71,7 @@
 
 	$self->{packet_list} = {};
 	$self->{packet_lut} = {};
+	$self->{bytesProcessed} = 0;
 
 	return bless $self, $class;
 }
@@ -181,9 +184,8 @@
 # - KEYS: list of argument names from {packet_list}
 # `l`
 sub parse {
-	my ($self, $msg) = @_;
+	my ($self, $msg, $handleContainer, @handleArguments) = @_;
 
-	$bytesReceived += length($msg);
 	my $switch = Network::MessageTokenizer::getMessageID($msg);
 	my $handler = $self->{packet_list}{$switch};
 
@@ -192,9 +194,7 @@
 		return undef;
 	}
 
-	# set this alternative (if any) as the one in use with that server
-	# TODO: permanent storage (with saving)?
-	$self->{packet_lut}{$handler->[0]} = $switch;
+	# $handler->[0] may be (re)binded to $switch here for current serverType
 
 	debug "Received packet: $switch Handler: $handler->[0]\n", "packetParser", 2;
 
@@ -212,21 +212,21 @@
 		$self->$custom_parse(\%args);
 	}
 
-	my $callback = $self->can($handler->[0]);
+	my $callback = $handleContainer->can($handler->[0]);
 	if ($callback) {
 		# Hook names can be made more uniform,
 		# but the ones for Receive must be kept for compatibility anyway.
+		# TODO: restrict to $Globals::packetParser and $Globals::messageSender?
 		if ($self->{hook_prefix} eq 'Network::Receive') {
 			Plugins::callHook("packet_pre/$handler->[0]", \%args);
 		} else {
 			Plugins::callHook("$self->{hook_prefix}/packet_pre/$handler->[0]", \%args);
 		}
 		Misc::checkValidity("Packet: " . $handler->[0] . " (pre)");
-		$self->$callback(\%args);
+		$handleContainer->$callback(\%args, @handleArguments);
 		Misc::checkValidity("Packet: " . $handler->[0]);
 	} else {
-		warning "Packet Parser: Unhandled Packet: $switch Handler: $handler->[0]\n";
-		debug ("Unpacked: " . join(', ', @{\%args}{@{$handler->[2]}}) . "\n"), "packetParser", 2 if $handler->[2];
+		$handleContainer->unhandledMessage(\%args, @handleArguments);
 	}
 
 	if ($self->{hook_prefix} eq 'Network::Receive') {
@@ -237,6 +237,13 @@
 	return \%args;
 }
 
+sub unhandledMessage {
+	my ($self, $args) = @_;
+	
+	warning "Packet Parser: Unhandled Packet: $args->{switch} Handler: $self->{packet_list}{$args->{switch}}[0]\n";
+	debug ("Unpacked: " . join(', ', @{$args}{@{$args->{KEYS}}}) . "\n"), "packetParser", 2 if $args->{KEYS};
+}
+
 ##
 # boolean $packetParser->willMangle(Bytes messageID)
 # messageID: a message ID, such as "008A".
@@ -310,4 +317,103 @@
 	return $hook_args{return};
 }
 
+sub process {
+	my ($self, $tokenizer, $handleContainer, @handleArguments) = @_;
+	
+	my @result;
+	my $type;
+	while (my $message = $tokenizer->readNext(\$type)) {
+		$handleContainer->{bytesProcessed} += length($message);
+		$handleContainer->{lastPacketTime} = time;
+		
+		my $args;
+		
+		if ($type == Network::MessageTokenizer::KNOWN_MESSAGE) {
+			my $switch = Network::MessageTokenizer::getMessageID($message);
+			
+			main::parseMessage_pre($handleContainer->{hook_prefix} || 'Network::ClientReceive', $switch, $message, $message);
+			
+			my $willMangle = $handleContainer->can('willMangle') && $handleContainer->willMangle($switch);
+			
+			if ($args = $self->parse($message, $handleContainer, @handleArguments)) {
+				$args->{mangle} ||= $willMangle && $handleContainer->mangle($args);
+			} else {
+				$args = {
+					switch => $switch,
+					RAW_MSG => $message,
+					(mangle => 2) x!! $willMangle,
+				};
+			}
+			
+		} elsif ($type == Network::MessageTokenizer::ACCOUNT_ID) {
+			$args = {
+				RAW_MSG => $message
+			};
+			
+		} elsif ($type == Network::MessageTokenizer::UNKNOWN_MESSAGE) {
+			$args = {
+				switch => Network::MessageTokenizer::getMessageID($message),
+				RAW_MSG => $message,
+				# RAW_MSG_SIZE => length($message),
+			};
+			$handleContainer->unknownMessage($args, @handleArguments);
+			
+		} else {
+			die "Packet Tokenizer: Unknown type: $type";
+		}
+		
+		unless ($args->{mangle}) {
+			# Packet was not mangled
+			push @result, $args->{RAW_MSG};
+			#$result .= $args->{RAW_MSG};
+		} elsif ($args->{mangle} == 1) {
+			# Packet was mangled
+			push @result, $self->reconstruct($args);
+			#$result .= $self->reconstruct($args);
+		} else {
+			# Packet was suppressed
+		}
+	}
+	
+	# If we're running in X-Kore mode, pass messages back to the RO client.
+	
+	# It seems like messages can't be just concatenated safely
+	# (without "use bytes" pragma or messing with unicode stuff)
+	# http://perldoc.perl.org/perlunicode.html#The-%22Unicode-Bug%22
+	return @result;
+}
+
+sub unknownMessage {
+	my ($self, $args) = @_;
+	
+	# Unknown message - ignore it
+	unless (existsInList($config{debugPacket_exclude}, $args->{switch})) {
+		warning TF("Packet Tokenizer: Unknown switch: %s\n", $args->{switch}), 'connection';
+		visualDump($args->{RAW_MSG}, "<< Received unknown packet") if $config{debugPacket_unparsed};
+	}
+	
+	# Pass it along to the client, whatever it is
+}
+
+# Utility methods used by both Receive and Send
+
+sub parseChat {
+	my ($self, $args) = @_;
+	$args->{message} = bytesToString($args->{message});
+	if ($args->{message} =~ /^(.*?)\s{1,2}:\s{1,2}(.*)$/) {
+		$args->{name} = $1;
+		$args->{message} = $2;
+		stripLanguageCode(\$args->{message});
+	}
+	if (exists $args->{ID}) {
+		$args->{actor} = Actor::get($args->{ID});
+	}
+}
+
+sub reconstructChat {
+	my ($self, $args) = @_;
+	$args->{message} = '|00' . $args->{message} if $config{chatLangCode} && $config{chatLangCode} ne 'none';
+	$args->{message} = stringToBytes($char->{name}) . ' : ' . stringToBytes($args->{message});
+}
+
 1;
Index: src/Network/Receive/ServerType0.pm
===================================================================
--- src/Network/Receive/ServerType0.pm	(revision 7792)
+++ src/Network/Receive/ServerType0.pm	(working copy)
@@ -109,7 +109,7 @@
 		'0097' => ['private_message', 'v Z24 Z*', [qw(len privMsgUser privMsg)]],
 		'0098' => ['private_message_sent', 'C', [qw(type)]],
 		'009A' => ['system_chat', 'v a*', [qw(len message)]], #maybe use a* instead and $message =~ /\000$//; if there are problems
-		'009C' => ['actor_look_at', 'a4 C x C', [qw(ID head body)]],
+		'009C' => ['actor_look_at', 'a4 v C', [qw(ID head body)]],
 		'009D' => ['item_exists', 'a4 v C v3 C2', [qw(ID nameID identified x y amount subx suby)]],
 		'009E' => ['item_appeared', 'a4 v C v2 C2 v', [qw(ID nameID identified x y subx suby amount)]],
 		'00A0' => ['inventory_item_added', 'v3 C3 a8 v C2', [qw(index amount nameID identified broken upgrade cards type_equip type fail)]],
@@ -3176,6 +3176,7 @@
 		message T("Server granted login request to account server\n"), "poseidon";
 	} else {
 		message T("Server granted login request to char/map server\n"), "poseidon";
+		# FIXME
 		change_to_constate25 if ($config{'gameGuard'} eq "2");
 	}
 	$net->setState(1.3) if ($net->getState() == 1.2);
Index: src/Network/Receive/iRO.pm
===================================================================
--- src/Network/Receive/iRO.pm	(revision 7792)
+++ src/Network/Receive/iRO.pm	(working copy)
@@ -35,6 +35,12 @@
 		$self->{packet_list}{$switch} = $packets{$switch};
 	}
 
+	my %handlers = qw(
+		received_characters 082D
+		account_id 0283
+	);
+	$self->{packet_lut}{$_} = $handlers{$_} for keys %handlers;
+	
 	return $self;
 }
 
Index: src/Network/Send/ServerType0.pm
===================================================================
--- src/Network/Send/ServerType0.pm	(revision 7792)
+++ src/Network/Send/ServerType0.pm	(working copy)
@@ -25,7 +25,7 @@
 use Globals qw($accountID $sessionID $sessionID2 $accountSex $char $charID %config %guild @chars $masterServer $syncSync);
 use Log qw(debug);
 use Translation qw(T TF);
-use I18N qw(stringToBytes);
+use I18N qw(bytesToString stringToBytes);
 use Utils;
 use Utils::Exceptions;
 use Utils::Rijndael;
@@ -39,16 +39,45 @@
 	my $self = $class->SUPER::new(@_);
 	
 	my %packets = (
-		'0064' => ['master_login', 'V a24 a24 C', [qw(version username password master_version)]],
-		'0134' => ['buy_bulk_vender', 'v a4 a*', [qw(len venderID itemInfo)]],
-		'02B0' => ['master_login', 'V a24 a24 C H32 H26 C', [qw(version username password_rijndael master_version ip mac isGravityID)]],
-		'0801' => ['buy_bulk_vender', 'v a4 a4 a*', [qw(len venderID venderCID itemInfo)]],
+		'0064' => ['master_login', 'V Z24 Z24 C', [qw(version username password master_version)]],
+		'0065' => ['game_login', 'a4 a4 a4 v C', [qw(accountID sessionID sessionID2 userLevel accountSex)]],
+		'0066' => ['char_login', 'C', [qw(slot)]],
+		'0067' => ['char_create'], # TODO
+		'0068' => ['char_delete'], # TODO
+		'0072' => ['map_login', 'a4 a4 a4 V C', [qw(accountID charID sessionID tick sex)]],
+		'007D' => ['map_loaded'], # len 2
+		'007E' => ['sync'], # TODO
+		'0089' => ['actor_action', 'a4 C', [qw(targetID type)]],
+		'008C' => ['public_chat', 'x2 Z*', [qw(message)]],
+		'0096' => ['private_message', 'x2 Z24 Z*', [qw(privMsgUser privMsg)]],
+		'009B' => ['actor_look_at', 'v C', [qw(head body)]],
+		'009F' => ['item_take', 'a4', [qw(ID)]],
+		'00B2' => ['restart', 'C', [qw(type)]],
+		'00F3' => ['map_login', '', [qw()]],
+		'0108' => ['party_chat', 'x2 Z*', [qw(message)]],
+		'0134' => ['buy_bulk_vender', 'x2 a4 a*', [qw(venderID itemInfo)]],
+		'0149' => ['alignment', 'a4 C v', [qw(targetID type point)]],
+		'014D' => ['guild_check'], # len 2
+		'014F' => ['guild_info_request', 'V', [qw(type)]],
+		'017E' => ['guild_chat', 'x2 Z*', [qw(message)]],
+		'0187' => ['ban_check', 'a4', [qw(accountID)]],
+		'018A' => ['quit_request', 'v', [qw(type)]],
+		'01B2' => ['shop_open'], # TODO
+		'012E' => ['shop_close'], # len 2
+		'0204' => ['client_hash'], # TODO
+		'021D' => ['less_effect'], # TODO
+		'0275' => ['game_login', 'a4 a4 a4 v C x16 v', [qw(accountID sessionID sessionID2 userLevel accountSex iAccountSID)]],
+		'02B0' => ['master_login', 'V Z24 Z24 C H32 H26 C', [qw(version username password_rijndael master_version ip mac isGravityID)]],
+		'0436' => ['map_login', 'a4 a4 a4 V C', [qw(accountID charID sessionID tick sex)]],
+		'0801' => ['buy_bulk_vender', 'x2 a4 a4 a*', [qw(venderID venderCID itemInfo)]],
 	);
 	$self->{packet_list}{$_} = $packets{$_} for keys %packets;
 	
 	# # it would automatically use the first available if not set
 	# my %handlers = qw(
 	# 	master_login 0064
+	# 	game_login 0065
+	# 	map_login 0072
 	# 	buy_bulk_vender 0134
 	# );
 	# $self->{packet_lut}{$_} = $handlers{$_} for keys %handlers;
@@ -74,8 +103,11 @@
 
 sub sendAlignment {
 	my ($self, $ID, $alignment) = @_;
-	my $msg = pack("C*", 0x49, 0x01) . $ID . pack("C*", $alignment);
-	$self->sendToServer($msg);
+	$self->sendToServer($self->reconstruct({
+		switch => 'alignment',
+		targetID => $ID,
+		type => $alignment,
+	}));
 	debug "Sent Alignment: ".getHex($ID).", $alignment\n", "sendPacket", 2;
 }
 
@@ -101,8 +133,7 @@
 		return;
 	}
 
-	my $msg = pack('v a4 C', 0x0089, $monID, $flag);
-	$self->sendToServer($msg);
+	$self->sendToServer($self->reconstruct({switch => 'actor_action', targetID => $monID, type => $flag}));
 	debug "Sent Action: " .$flag. " on: " .getHex($monID)."\n", "sendPacket", 2;
 }
 
@@ -126,8 +157,10 @@
 
 sub sendBanCheck {
 	my ($self, $ID) = @_;
-	my $msg = pack("C*", 0x87, 0x01) . $ID;
-	$self->sendToServer($msg);
+	$self->sendToServer($self->reconstruct({
+		switch => 'ban_check',
+		accountID => $ID,
+	}));
 	debug "Sent Account Ban Check Request : " . getHex($ID) . "\n", "sendPacket", 2;
 }
 
@@ -223,23 +256,22 @@
 
 sub sendCharLogin {
 	my ($self, $char) = @_;
-	my $msg = pack("C*", 0x66,0) . pack("C*",$char);
-	$self->sendToServer($msg);
+	$self->sendToServer($self->reconstruct({switch => 'char_login', slot => $char}));
 }
 
+sub parse_public_chat {
+	my ($self, $args) = @_;
+	$self->parseChat($args);
+}
+
+sub reconstruct_public_chat {
+	my ($self, $args) = @_;
+	$self->reconstructChat($args);
+}
+
 sub sendChat {
 	my ($self, $message) = @_;
-	$message = "|00$message" if ($config{chatLangCode} && $config{chatLangCode} ne "none");
-
-	my ($data, $charName); # Type: Bytes
-	$message = stringToBytes($message); # Type: Bytes
-	$charName = stringToBytes($char->{name});
-
-	$data = pack("C*", 0x8C, 0x00) .
-		pack("v*", length($charName) + length($message) + 8) .
-		$charName . " : " . $message . chr(0);
-		
-	$self->sendToServer($data);
+	$self->sendToServer($self->reconstruct({switch => 'public_chat', message => $message}));
 }
 
 sub sendChatRoomBestow {
@@ -313,8 +345,7 @@
 
 sub sendCloseShop {
 	my $self = shift;
-	my $msg = pack("C*", 0x2E, 0x01);
-	$self->sendToServer($msg);
+	$self->sendToServer($self->reconstruct({switch => 'shop_close'}));
 	debug "Shop Closed\n", "sendPacket", 2;
 }
 
@@ -467,14 +498,21 @@
 	debug "Sent Forge, Produce Item: $ID\n" , 2;
 }
 
+sub reconstruct_game_login {
+	my ($self, $args) = @_;
+	$args->{userLevel} = 0 unless exists $args->{userLevel};
+	($args->{iAccountSID}) = $masterServer->{ip} =~ /\d+\.\d+\.\d+\.(\d+)/ unless exists $args->{iAccountSID};
+}
+
 sub sendGameLogin {
 	my ($self, $accountID, $sessionID, $sessionID2, $sex) = @_;
-	my $msg = pack("v1", hex($masterServer->{gameLogin_packet}) || 0x65) . $accountID . $sessionID . $sessionID2 . pack("C*", 0, 0, $sex);
-	if (hex($masterServer->{gameLogin_packet}) == 0x0273 || hex($masterServer->{gameLogin_packet}) == 0x0275) {
-		my ($serv) = $masterServer->{ip} =~ /\d+\.\d+\.\d+\.(\d+)/;
-		$msg .= pack("x16 C1 x3", $serv);
-	}
-	$self->sendToServer($msg);
+	$self->sendToServer($self->reconstruct({
+		switch => 'game_login',
+		accountID => $accountID,
+		sessionID => $sessionID,
+		sessionID2 => $sessionID2,
+		accountSex => $sex,
+	}));
 	debug "Sent sendGameLogin\n", "sendPacket", 2;
 }
 
@@ -536,18 +574,19 @@
 	debug "Sent Guild Break: $guildName\n", "sendPacket", 2;
 }
 
+sub parse_guild_chat {
+	my ($self, $args) = @_;
+	$self->parseChat($args);
+}
+
+sub reconstruct_guild_chat {
+	my ($self, $args) = @_;
+	$self->reconstructChat($args);
+}
+
 sub sendGuildChat {
 	my ($self, $message) = @_;
-
-	my ($charName);
-	$message = "|00$message" if ($config{chatLangCode} && $config{chatLangCode} ne "none");
-	$message = stringToBytes($message);
-	$charName = stringToBytes($char->{name});
-
-	my $data = pack("C*",0x7E, 0x01) .
-		pack("v*", length($charName) + length($message) + 8) .
-		$charName . " : " . $message . chr(0);
-	$self->sendToServer($data);
+	$self->sendToServer($self->reconstruct({switch => 'guild_chat', message => $message}));
 }
 
 sub sendGuildCreate {
@@ -560,8 +599,7 @@
 
 sub sendGuildMasterMemberCheck {
 	my ($self, $ID) = @_;
-	my $msg = pack("v", 0x014D);
-	$self->sendToServer($msg);
+	$self->sendToServer($self->reconstruct({switch => 'guild_check'}));
 	debug "Sent Guild Master/Member Check.\n", "sendPacket";
 }
 
@@ -654,8 +692,10 @@
 
 sub sendGuildRequestInfo {
 	my ($self, $page) = @_; # page 0-4
-	my $msg = pack("C*", 0x4f, 0x01).pack("V1", $page);
-	$self->sendToServer($msg);
+	$self->sendToServer($self->reconstruct({
+		switch => 'guild_info_request',
+		type => $page,
+	}));
 	debug "Sent Guild Request Page : ".$page."\n", "sendPacket";
 }
 
@@ -765,9 +805,7 @@
 
 sub sendLook {
 	my ($self, $body, $head) = @_;
-	my $msg;
-	$msg = pack("C*", 0x9B, 0x00, $head, 0x00, $body);
-	$self->sendToServer($msg);
+	$self->sendToServer($self->reconstruct({switch => 'actor_look_at', body => $body, head => $head}));
 	debug "Sent look: $body $head\n", "sendPacket", 2;
 	$char->{look}{head} = $head;
 	$char->{look}{body} = $body;
@@ -775,11 +813,9 @@
 
 sub sendMapLoaded {
 	my $self = shift;
-	my $msg;
 	$syncSync = pack("V", getTickCount());
-	$msg = pack("C*", 0x7D,0x00);
 	debug "Sending Map Loaded\n", "sendPacket";
-	$self->sendToServer($msg);
+	$self->sendToServer($self->reconstruct({switch => 'map_loaded'}));
 	Plugins::callHook('packet/sendMapLoaded');
 }
 
@@ -789,13 +825,14 @@
 	$sex = 0 if ($sex > 1 || $sex < 0); # Sex can only be 0 (female) or 1 (male)
 	
 	if ($self->{serverType} == 0 || $self->{serverType} == 21 || $self->{serverType} == 22) {
-		# Server Type 21 is tRO (2008-09-16Ragexe12_Th), 22 is idRO
-		$msg = pack("C*", 0x72,0) .
-			$accountID .
-			$charID .
-			$sessionID .
-			pack("V1", getTickCount()) .
-			pack("C*",$sex);
+		$msg = $self->reconstruct({
+			switch => 'map_login',
+			accountID => $accountID,
+			charID => $charID,
+			sessionID => $sessionID,
+			tick => getTickCount,
+			sex => $sex,
+		});
 
 	} else { #oRO and pRO
 		my $key;
@@ -859,9 +896,9 @@
 sub reconstruct_master_login {
 	my ($self, $args) = @_;
 	
-	exists $args->{ip} or $args->{ip} = '3139322e3136382e322e3400685f4c40'; # gibberish
-	exists $args->{mac} or $args->{mac} = '31313131313131313131313100'; # gibberish
-	exists $args->{isGravityID} or $args->{isGravityID} = 0;
+	$args->{ip} = '3139322e3136382e322e3400685f4c40' unless exists $args->{ip}; # gibberish
+	$args->{mac} = '31313131313131313131313100' unless exists $args->{mac}; # gibberish
+	$args->{isGravityID} = 0 unless exists $args->{isGravityID};
 	
 	my $key = pack('C24', (6, 169, 33, 64, 54, 184, 161, 91, 81, 46, 3, 213, 52, 18, 0, 6, 61, 175, 186, 66, 157, 158, 180, 48));
 	my $chain = pack('C24', (61, 175, 186, 66, 157, 158, 180, 48, 180, 34, 218, 128, 44, 159, 172, 65, 1, 2, 4, 8, 16, 32, 128));
@@ -982,18 +1019,20 @@
 	$self->sendToServer($msg);
 }
 
+sub parse_party_chat {
+	my ($self, $args) = @_;
+	$self->parseChat($args);
+}
+
+sub reconstruct_party_chat {
+	my ($self, $args) = @_;
+	$self->reconstructChat($args);
+}
+
 sub sendPartyChat {
 	my $self = shift;
 	my $message = shift;
-
-	my $charName;
-	$message = "|00$message" if ($config{chatLangCode} && $config{chatLangCode} ne "none");
-	$message = stringToBytes($message);
-	$charName = stringToBytes($char->{name});
-
-	my $msg = pack("C*",0x08, 0x01) . pack("v*", length($charName) + length($message) + 8) .
-		$charName . " : " . $message . chr(0);
-	$self->sendToServer($msg);
+	$self->sendToServer($self->reconstruct({switch => 'party_chat', message => $message}));
 }
 
 sub sendPartyJoin {
@@ -1144,22 +1183,32 @@
 	debug "Sent pre-login packet $type\n", "sendPacket", 2;
 }
 
+sub parse_private_message {
+	my ($self, $args) = @_;
+	$args->{privMsg} = bytesToString($args->{privMsg});
+	stripLanguageCode(\$args->{privMsg});
+	$args->{privMsgUser} = bytesToString($args->{privMsgUser});
+}
+
+sub reconstruct_private_message {
+	my ($self, $args) = @_;
+	$args->{privMsg} = '|00' . $args->{privMsg} if $config{chatLangCode} && $config{chatLangCode} ne 'none';
+	$args->{privMsg} = stringToBytes($args->{privMsg});
+	$args->{privMsgUser} = stringToBytes($args->{privMsgUser});
+}
+
 sub sendPrivateMsg {
 	my ($self, $user, $message) = @_;
-
-	$message = "|00$message" if ($config{chatLangCode} && $config{chatLangCode} ne "none");
-	$message = stringToBytes($message);
-	$user = stringToBytes($user);
-
-	my $msg = pack("C*", 0x96, 0x00) . pack("v*", length($message) + 29) . $user .
-		chr(0) x (24 - length($user)) . $message . chr(0);
-	$self->sendToServer($msg);
+	$self->sendToServer($self->reconstruct({
+		switch => 'private_message',
+		privMsg => $message,
+		privMsgUser => $user,
+	}));
 }
 
 sub sendQuit {
 	my $self = shift;
-	my $msg = pack("C*", 0x8A, 0x01, 0x00, 0x00);
-	$self->sendToServer($msg);
+	$self->sendToServer($self->reconstruct({switch => 'quit_request', type => 0}));
 	debug "Sent Quit\n", "sendPacket", 2;
 }
 
@@ -1220,7 +1269,7 @@
 # type: 0=respawn ; 1=return to char select
 sub sendRestart {
 	my ($self, $type) = @_;
-	$self->sendToServer(pack('v C', 0x00B2, $type));
+	$self->sendToServer($self->reconstruct({switch => 'restart', type => $type}));
 	debug "Sent Restart: " . ($type ? 'Quit To Char Selection' : 'Respawn') . "\n", "sendPacket", 2;
 }
 
@@ -1425,9 +1474,7 @@
 
 sub sendTake {
 	my ($self, $itemID) = @_;
-	my $msg;
-	$msg = pack("C*", 0x9F, 0x00) . $itemID;
-	$self->sendToServer($msg);
+	$self->sendToServer($self->reconstruct({switch => 'item_take', ID => $itemID}));
 	debug "Sent take\n", "sendPacket", 2;
 }
 
Index: src/Network/XKore2/MapServer.pm
===================================================================
--- src/Network/XKore2/MapServer.pm	(revision 7792)
+++ src/Network/XKore2/MapServer.pm	(working copy)
@@ -20,7 +20,7 @@
 	$portalsList $npcsList $monstersList $playersList $petsList
 	@friendsID %friends %pet @partyUsersID %spells
 	@chatRoomsID %chatRooms @venderListsID %venderLists $hotkeyList
-	$packetParser %config
+	%config
 );
 use Base::Ragnarok::MapServer;
 use base qw(Base::Ragnarok::MapServer);
@@ -56,10 +56,10 @@
 	}
 }
 
-sub handleMapLoaded {
+sub map_loaded {
 	# The RO client has finished loading the map.
 	# Send character information to the RO client.
-	my ($self, $client) = @_;
+	my ($self, $args, $client) = @_;
 	no encoding 'utf8';
 	use bytes;
 
@@ -239,7 +239,7 @@
 		shiftPack(\$coords, $portal->{pos}{x}, 10);
 		shiftPack(\$coords, $portal->{pos}{y}, 10);
 		shiftPack(\$coords, 0, 4);
-		$output .= $packetParser->reconstruct({
+		$output .= $self->{recvPacketParser}->reconstruct({
 			switch => '0078',
 			coords => $coords,
 			map { $_ => $portal->{$_} } qw(ID type)
@@ -254,7 +254,7 @@
 		shiftPack(\$coords, $npc->{pos}{x}, 10);
 		shiftPack(\$coords, $npc->{pos}{y}, 10);
 		shiftPack(\$coords, $npc->{look}{body}, 4);
-		$output .= $packetParser->reconstruct({
+		$output .= $self->{recvPacketParser}->reconstruct({
 			switch => '0078',
 			coords => $coords,
 			map { $_ => $npc->{$_} } qw(ID opt1 opt2 option type)
@@ -269,7 +269,7 @@
 		shiftPack(\$coords, $monster->{pos_to}{x}, 10);
 		shiftPack(\$coords, $monster->{pos_to}{y}, 10);
 		shiftPack(\$coords, $monster->{look}{body}, 4);
-		$output .= $packetParser->reconstruct({
+		$output .= $self->{recvPacketParser}->reconstruct({
 			switch => '0078',
 			walk_speed => $monster->{walk_speed} * 1000,
 			coords => $coords,
@@ -285,7 +285,7 @@
 		shiftPack(\$coords, $pet->{pos_to}{x}, 10);
 		shiftPack(\$coords, $pet->{pos_to}{y}, 10);
 		shiftPack(\$coords, $pet->{look}{body}, 4);
-		$output .= $packetParser->reconstruct({
+		$output .= $self->{recvPacketParser}->reconstruct({
 			switch => '0078',
 			walk_speed => $pet->{walk_speed} * 1000,
 			coords => $coords,
@@ -420,76 +420,52 @@
    $client->send($output);
 	
 	if ($config{verbose} && !$config{XKore_silent}) {
-		$client->send($packetParser->reconstruct({switch => '009A', message => $Settings::welcomeText}));
+		$client->send($self->{recvPacketParser}->reconstruct({
+			switch => 'system_chat',
+			message => $Settings::welcomeText,
+		}));
 	}
+	
+	$args->{mangle} = 2;
 }
 
-sub process_007D {
-	my ($self, $client) = @_;
-	handleMapLoaded($self, $client);
-}
-
-sub process_01C0 {
-	my ($self, $client) = @_;
-	handleMapLoaded($self, $client);
-}
-
-sub process_00B2 {
-	my ($self, $client) = @_;
+sub restart {
+	my ($self, $args, $client) = @_;
 	# If they want to character select/respawn, kick them to the login screen
 	# immediately (GM kick)
 	$client->send(pack('C3', 0x81, 0, 15));
+	
+	$args->{mangle} = 2;
 }
 
-sub process_018A {
-	my ($self, $client) = @_;
+sub quit_request {
+	my ($self, $args, $client) = @_;
 	# Client wants to quit
 	$client->send(pack('C*', 0x8B, 0x01, 0, 0));
+	
+	$args->{mangle} = 2;
 }
 
-sub handleSync {
-	my ($self, $client, $message) = @_;
-	my $ID = Network::MessageTokenizer::getMessageID($message);
-	my $serverType = $self->getServerType();
-	if (
-		($ID eq "007E" && (
-			$serverType == 0 ||
-			$serverType == 1 ||
-			$serverType == 2 ||
-			$serverType == 6 ||
-			$serverType == 21)
-		)
-		|| ($ID eq "0089" && (
-			$serverType == 3 ||
-			$serverType == 5 ||
-			$serverType == 8)
-		)
-		|| ($ID eq "0116" &&
-			$serverType == 4 )
-		|| ($ID eq "00A7" &&
-			$serverType == 18)
-	) {
-		# Surpress client sync message.
-	} else {
-		&unhandledMessage;
-	}
+sub sync {
+	my ($self, $args, $client) = @_;
+	$args->{mangle} = 2;
 }
 
-sub process_007E { &handleSync; }
-sub process_0089 { &handleSync; }
-sub process_0116 { &handleSync; }
-sub process_00A7 { &handleSync }
-
 # Not sure what these are, but don't let it get to the RO server.
-sub process_021D {}
-sub process_014D {}
-sub process_014F {}
-sub process_0181 {}
+sub less_effect {
+	my ($self, $args, $client) = @_;
+	$args->{mangle} = 2;
+}
 
-sub unhandledMessage {
-	my ($self, $client, $message) = @_;
-	$client->{outbox}->add($message);
+sub guild_check {
+	my ($self, $args, $client) = @_;
+	$args->{mangle} = 2;
 }
 
+sub guild_info_request {
+	my ($self, $args, $client) = @_;
+	$args->{mangle} = 2;
+}
+
 1;
 
Index: src/Network/ClientReceive.pm
===================================================================
--- src/Network/ClientReceive.pm	(revision 0)
+++ src/Network/ClientReceive.pm	(revision 0)
@@ -0,0 +1,221 @@
+#########################################################################
+# This software is open source, licensed under the GNU General Public
+# License, version 2.
+# Basically, this means that you're allowed to modify and distribute
+# this software. However, if you distribute modified versions, you MUST
+# also distribute the source code.
+# See http://www.gnu.org/licenses/gpl.html for the full license.
+#########################################################################
+##
+# MODULE DESCRIPTION: Outgoing client messages handling
+#
+# This class contains only handler functions
+# which are used to handle messages
+# that are sent by the RO client, if it's present.
+
+package Network::ClientReceive;
+
+use strict;
+use Modules 'register';
+use Time::HiRes qw(time);
+
+use Globals qw($packetParser $incomingMessages %config $char %ai_v %timeout $shopstarted $firstLoginMap $sentWelcomeMessage @lastpm %lastpm);
+use Misc qw(configModify visualDump);
+use Log qw(message warning);
+use Translation;
+use Utils qw(existsInList);
+
+sub new {
+	my $self = {};
+	
+	$self->{hook_prefix} = 'Network::ClientReceive';
+	
+	bless $self, $_[0];
+}
+
+sub handleChat {
+	my ($self, $args, $chat) = @_;
+	
+	my $prefix = quotemeta $config{commandPrefix};
+	if ($chat =~ /^$prefix/) {
+		$chat =~ s/^$prefix//;
+		$chat =~ s/^\s*//;
+		$chat =~ s/\s*$//;
+		main::parseInput($chat, 1);
+		$args->{mangle} = 2;
+		return 1;
+	}
+}
+
+sub game_login {
+	$incomingMessages->nextMessageMightBeAccountID;
+}
+
+sub char_login {
+	my ($self, $args) = @_;
+	
+	configModify('char', $args->{slot});
+}
+
+sub map_login {
+	my ($self, $args) = @_;
+	
+	$incomingMessages->nextMessageMightBeAccountID;
+	
+	if ($config{sex} ne '') {
+		$args->{sex} = $config{sex};
+		$args->{mangle} = 1;
+	}
+}
+
+sub map_loaded {
+	$packetParser->changeToInGameState;
+	AI::clear('clientSuspend');
+	$timeout{ai}{time} = time;
+	if ($firstLoginMap) {
+		undef $sentWelcomeMessage;
+		undef $firstLoginMap;
+	}
+	$timeout{welcomeText}{time} = time;
+	$ai_v{portalTrace_mapChanged} = time;
+	message T("Map loaded\n"), 'connection';
+	
+	Plugins::callHook('map_loaded');
+}
+
+sub actor_action {
+	my ($self, $args) = @_;
+	
+	unless ($config{tankMode} || AI::inQueue('attack')) {
+		AI::clear('clientSuspend');
+		$char->clientSuspend($args->{switch}, 2, $args->{type}, $args->{targetID});
+	} else {
+		$args->{mangle} = 2;
+	}
+}
+
+sub public_chat {
+	my ($self, $args) = @_;
+	
+	$self->handleChat($args, $args->{message});
+}
+
+sub private_message {
+	my ($self, $args) = @_;
+	
+	unless ($self->handleChat($args, $args->{privMsg})) {
+		undef %lastpm;
+		@lastpm{qw(msg user)} = @{$args}{qw(privMsg privMsgUser)};
+		push @lastpm, {%lastpm};
+	}
+}
+
+sub actor_look_at {
+	my ($self, $args) = @_;
+	
+	@{$char->{look}}{qw(head body)} = @{$args}{qw(head body)};
+}
+
+sub item_take {
+	my ($self, $args) = @_;
+	
+	AI::clear('clientSuspend');
+	$char->clientSuspend($args->{switch}, 2, $args->{ID});
+}
+
+sub restart {
+	my ($self, $args) = @_;
+	
+	AI::clear('clientSuspend');
+	$char->clientSuspend($args->{switch}, 10);
+}
+
+sub party_chat {
+	my ($self, $args) = @_;
+	
+	$self->handleChat($args, $args->{message});
+}
+
+sub alignment {
+	my ($self, $args) = @_;
+	
+	# Chat/skill mute
+	$args->{mangle} = 2;
+}
+
+sub guild_chat {
+	my ($self, $args) = @_;
+	
+	$self->handleChat($args, $args->{message});
+}
+
+sub quit_request {
+	my ($self, $args) = @_;
+	
+	AI::clear('clientSuspend');
+	$char->clientSuspend($args->{switch}, 10);
+}
+
+sub shop_open {
+	# client started a shop manually
+	$shopstarted = 1;
+}
+
+sub shop_close {
+	# client stopped shop manually
+	$shopstarted = 0;
+}
+
+=pod
+# sendSync
+	if ($masterServer->{syncID} && $switch eq sprintf('%04X', hex($masterServer->{syncID}))) {
+		#syncSync support for XKore 1 mode
+		$syncSync = substr($msg, $masterServer->{syncTickOffset}, 4);
+
+# sendSync
+	} elsif ($switch eq "00A7") {
+		if($masterServer && $masterServer->{paddedPackets}) {
+			$syncSync = substr($msg, 8, 4);
+		}
+
+# sendSync
+	} elsif ($switch eq "007E") {
+		if ($masterServer && $masterServer->{paddedPackets}) {
+			$syncSync = substr($msg, 4, 4);
+		}
+
+# sendMapLoaded
+	} elsif ($switch eq "007D") {
+		# syncSync support for XKore 1 mode
+		if($masterServer->{serverType} == 11) {
+			$syncSync = substr($msg, 8, 4);
+		} else {
+			# formula: MapLoaded_len + Sync_len - 4 - Sync_packet_last_junk
+			$syncSync = substr($msg, $masterServer->{mapLoadedTickOffset}, 4);
+		}
+
+# sendMove
+	} elsif ($switch eq "0085") {
+		#if ($masterServer->{serverType} == 0 || $masterServer->{serverType} == 1 || $masterServer->{serverType} == 2) {
+		#	#Move
+		#	AI::clear("clientSuspend");
+		#	makeCoordsDir(\%coords, substr($msg, 2, 3));
+		#	ai_clientSuspend($switch, (distance($char->{'pos'}, \%coords) * $char->{walk_speed}) + 4);
+		#}
+=cut
+
+sub unhandledMessage {}
+
+sub unknownMessage {
+	my ($self, $args) = @_;
+	
+	# Unknown message - ignore it
+	unless (existsInList($config{debugPacket_exclude}, $args->{switch})) {
+		warning TF("Packet Tokenizer: Unknown outgoing switch: %s\n", $args->{switch}), 'connection';
+		visualDump($args->{RAW_MSG}, "<< Outgoing unknown packet") if $config{debugPacket_unparsed};
+	}
+	
+	# Pass it along to the server, whatever it is
+}
+
+1;
Index: src/Network/Receive.pm
===================================================================
--- src/Network/Receive.pm	(revision 7792)
+++ src/Network/Receive.pm	(working copy)
@@ -43,6 +43,25 @@
 ### CATEGORY: Class methods
 ######################################
 
+# Just a wrapper for SUPER::parse.
+sub parse {
+	my $self = shift;
+	my $args = $self->SUPER::parse(@_);
+	
+	if ($config{debugPacket_received} == 3 &&
+			existsInList($config{'debugPacket_include'}, $args->{switch})) {
+		my $packet = $self->{packet_list}{$args->{switch}};
+		my ($name, $packString, $varNames) = @{$packet};
+		
+		my @vars = ();
+		for my $varName (@{$varNames}) {
+			message "$varName = $args->{$varName}\n";
+		}
+	}
+	
+	return $args;
+}
+
 ##
 # Network::Receive->decrypt(r_msg, themsg)
 # r_msg: a reference to a scalar.
TODO before committing:
Fix parseMessage_pre and other debug output.
Update tRO serverType to get rid of gameLogin_packet in server settings.

TODO later:
XKore modes wouldn't work properly with kRO ST until it would have packet_list with all required packets in Send part.
XKore 2 won't work properly until used ST would have packet_lut with "account_id 0283" if needed, and possibly other ambiguous packets in Receive part which are used to login and construct initial game state (like "received_characters 082D" for iRO).
Rewrite Poseidon using Base::AccountServer etc.
Handler implementations should be separated from packet information in Receive, like in new module ClientReceive.
Some policy for naming packet handlers.
Structures for all packets in Send.

User avatar
kLabMouse
Administrator
Administrator
Posts: 1301
Joined: 24 Apr 2008, 12:02

Re: Merge Receive and Send base modules

#25 Post by kLabMouse »

NICE WORK!.

Btw:
Some policy for naming packet handlers.
Structures for all packets in Send.
About the Policy: May-be We can use same naming as Original?
About Structures of all packets in Send: Why Send?? May-be something that is one for Send and Receive?

EternalHarvest
Developers
Developers
Posts: 1798
Joined: 05 Dec 2008, 05:42
Noob?: Yes

Re: Merge Receive and Send base modules

#26 Post by EternalHarvest »

kLabMouse wrote:About the Policy: May-be We can use same naming as Original?
I dunno, original names don't always make sense, and currently used in Receive are mostly okay.
About Structures of all packets in Send: Why Send?? May-be something that is one for Send and Receive?
Receive already has most of the packets. Send only required to have packets used in XKore modes, though, so "all packets" is just a possible TODO.

EternalHarvest
Developers
Developers
Posts: 1798
Joined: 05 Dec 2008, 05:42
Noob?: Yes

Re: Merge Receive and Send base modules

#27 Post by EternalHarvest »

tRO settings seem to be messed up: with secureLogin, gameLogin_packet isn't used.

Locked