diff --git a/src/Base/Ragnarok/AccountServer.pm b/src/Base/Ragnarok/AccountServer.pm index 725e4ca8d1..c973c73c35 100644 --- a/src/Base/Ragnarok/AccountServer.pm +++ b/src/Base/Ragnarok/AccountServer.pm @@ -104,50 +104,54 @@ sub master_login { ip => $ip, port => $charServer->getPort, name => $charServer->getName, + state => 0, users => $charServer->getPlayersCount, - display => 5, # don't show number of players + display => 0, # don't show number of players + property => 0, ip_port => $ip . ':' . $charServer->getPort, + unknown => 0, }; } - + $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}), + lastLoginIP => 0, + lastLoginTime => time, accountSex => $session{sex}, + iAccountSID => 0, servers => \@servers, })); - $client->close(); } elsif ($result == ACCOUNT_NOT_FOUND) { $client->send($self->{recvPacketParser}->reconstruct({ switch => 'login_error', type => Network::Receive::ServerType0::REFUSE_INVALID_ID, })); - $client->close(); } elsif ($result == PASSWORD_INCORRECT) { $client->send($self->{recvPacketParser}->reconstruct({ switch => 'login_error', type => Network::Receive::ServerType0::REFUSE_INVALID_PASSWD, })); - $client->close(); } elsif ($result == ACCOUNT_BANNED) { $client->send($self->{recvPacketParser}->reconstruct({ switch => 'login_error', type => Network::Receive::ServerType0::REFUSE_NOT_CONFIRMED, })); - $client->close(); } elsif ($result == SERVER_REFUSED) { $client->send($self->{recvPacketParser}->reconstruct({ switch => 'login_error', type => Network::Receive::ServerType0::ACCEPT_ID_PASSWD, })); - $client->close(); } else { die "Unexpected result $result."; } + + sleep 1; + $client->close(); } sub token_login { @@ -167,6 +171,7 @@ sub token_login { switch => 'login_error', type => Network::Receive::ServerType0::ACCEPT_ID_PASSWD, })); + sleep 1; $client->close(); } }; @@ -187,11 +192,12 @@ sub token_login { ip => $ip, port => $charServer->getPort, name => $charServer->getName, - users => $charServer->getPlayersCount, state => 0, + users => $charServer->getPlayersCount, property => 0, + display => 0, # don't show number of players + ip_port => $ip . ':' . $charServer->getPort, unknown => 0, - display => 5, # don't show number of players }; } @@ -204,37 +210,36 @@ sub token_login { lastLoginIP => 0, lastLoginTime => time, accountSex => $session{sex}, + iAccountSID => 0, servers => \@servers, })); - $client->close(); } elsif ($result == ACCOUNT_NOT_FOUND) { $client->send($self->{recvPacketParser}->reconstruct({ switch => 'login_error', type => Network::Receive::ServerType0::REFUSE_INVALID_ID, })); - $client->close(); } elsif ($result == PASSWORD_INCORRECT) { $client->send($self->{recvPacketParser}->reconstruct({ switch => 'login_error', type => Network::Receive::ServerType0::REFUSE_INVALID_PASSWD, })); - $client->close(); } elsif ($result == ACCOUNT_BANNED) { $client->send($self->{recvPacketParser}->reconstruct({ switch => 'login_error', type => Network::Receive::ServerType0::REFUSE_NOT_CONFIRMED, })); - $client->close(); } elsif ($result == SERVER_REFUSED) { $client->send($self->{recvPacketParser}->reconstruct({ switch => 'login_error', type => Network::Receive::ServerType0::ACCEPT_ID_PASSWD, })); - $client->close(); } else { die "Unexpected result $result."; } + sleep 1; + $client->close(); + $session{state} = 'About to select character'; } diff --git a/src/Base/Ragnarok/CharServer.pm b/src/Base/Ragnarok/CharServer.pm index aa8b2251e6..3efbd63fbf 100644 --- a/src/Base/Ragnarok/CharServer.pm +++ b/src/Base/Ragnarok/CharServer.pm @@ -98,42 +98,51 @@ sub game_login { $client->send($args->{accountID}); my $output; - # 099D - my @char_list = $self->getCharacters($session); - if ($self->{recvPacketParser}{packet_lut}{received_characters} eq '099D') { - $output = $self->{recvPacketParser}->reconstruct({ - switch => 'received_characters_info', - normal_slot => $charSvrSet{normal_slot}, - premium_slot => $charSvrSet{premium_slot}, - billing_slot => $charSvrSet{billing_slot}, - producible_slot => $charSvrSet{producible_slot}, - valid_slot => $charSvrSet{valid_slot}, - chars => \@char_list, - }); - - if ($charSvrSet{sync_Count} > 0) { - $output .= $self->{recvPacketParser}->reconstruct({ - switch => 'sync_received_characters', - sync_Count => $charSvrSet{sync_Count}, + my $replay_character_flow = @{$charSvrSet{xkore2_character_bootstrap_raw} || []}; + if ($replay_character_flow) { + $output = join('', @{$charSvrSet{xkore2_character_bootstrap_raw}}); + $client->{xkore2_character_pages_raw} = [@{$charSvrSet{xkore2_character_pages_raw} || []}]; + } else { + # Fall back to reconstruction for older servers that do not use the + # modern multi-packet character-selection flow. + my @char_list = $self->getCharacters($session); + if ($self->{recvPacketParser}{packet_lut}{received_characters} eq '099D') { + $output = $self->{recvPacketParser}->reconstruct({ + switch => 'received_characters_info', + normal_slot => $charSvrSet{normal_slot}, + premium_slot => $charSvrSet{premium_slot}, + billing_slot => $charSvrSet{billing_slot}, + producible_slot => $charSvrSet{producible_slot}, + valid_slot => $charSvrSet{valid_slot}, + chars => \@char_list, + }); + + if ($charSvrSet{sync_Count} > 0) { + $output .= $self->{recvPacketParser}->reconstruct({ + switch => 'sync_received_characters', + sync_Count => $charSvrSet{sync_Count}, + }); + } + } else { # 006B + $output = $self->{recvPacketParser}->reconstruct({ + switch => 'received_characters_info', + total_slot => $charSvrSet{total_slot}, + premium_start_slot => $charSvrSet{premium_start_slot}, + premium_end_slot => $charSvrSet{premium_end_slot}, + chars => \@char_list, }); } - } else { # 006B - $output = $self->{recvPacketParser}->reconstruct({ - switch => 'received_characters_info', - total_slot => $charSvrSet{total_slot}, - premium_start_slot => $charSvrSet{premium_start_slot}, - premium_end_slot => $charSvrSet{premium_end_slot}, - chars => \@char_list, - }); } $client->send($output); # Show list of characters. - &sendCharInfo; + &sendCharInfo unless $replay_character_flow; # TODO: Check if we need send accountID - my $data .= pack('C2 x4 a4 v', 0xB9, 0x08, $args->{accountID}, 0); - $client->send($data); + unless ($replay_character_flow) { + my $data .= pack('C2 x4 a4 v', 0xB9, 0x08, $args->{accountID}, 0); + $client->send($data); + } } } @@ -165,8 +174,13 @@ sub char_login { # We can't get the character information for some reason. $client->send(pack('C*', 0x6C, 0x00, 0)); } else { - my $host = inet_aton($self->{mapServer}->getHost); - $host = inet_aton($client->{BSC_sock}->sockhost) if $host eq "\000\000\000\000"; + my $mapHost = $self->{mapServer}->getHost; + my $host = inet_aton($mapHost); + if ($host eq "\000\000\000\000") { + $mapHost = $client->{BSC_sock}->sockhost; + $host = inet_aton($mapHost); + } + my $mapPort = $self->{mapServer}->getPort; $session->{charID} = $char->{charID}; $session->{state} = 'About to load map'; @@ -175,16 +189,23 @@ sub char_login { charID => $char->{charID}, mapName => $charInfo->{map}, mapIP => $host, - mapPort => $self->{mapServer}->getPort, + mapPort => $mapPort, + mapUrl => $mapHost . ':' . $mapPort, })); } } } + sleep 1; $client->close(); } sub sync_received_characters { my ($self, $args, $client) = @_; + if (exists $client->{xkore2_character_pages_raw}) { + my $page = shift @{$client->{xkore2_character_pages_raw}}; + $client->send($page) if defined $page; + return; + } if ($nChar == 0) { &sendCharInfo; $nChar = 1; diff --git a/src/Network/Receive.pm b/src/Network/Receive.pm index ca6ac3c12e..7d01b09249 100644 --- a/src/Network/Receive.pm +++ b/src/Network/Receive.pm @@ -850,6 +850,11 @@ sub received_characters_slots_info { # CHARACTER_INFO_NEO_UNION3 is based in charblocksize, check sub received_characters_unpackString sub received_characters { my ($self, $args) = @_; + $charSvrSet{received_characters_switch} = $args->{switch} if $args->{switch}; + if ($args->{switch} && $args->{switch} eq $self->{packet_lut}{received_characters} && $args->{RAW_MSG}) { + push @{$charSvrSet{xkore2_character_pages_raw}}, + substr($args->{RAW_MSG}, 0, $args->{RAW_MSG_SIZE}); + } my $blockSize = $self->received_characters_blockSize(); my $char_info = $self->received_characters_unpackString; @@ -929,6 +934,11 @@ sub received_characters { # total count: Server send from total pages until 1 page sub sync_received_characters { my ($self, $args) = @_; + $charSvrSet{sync_received_characters_switch} = $args->{switch} if $args->{switch}; + if ($args->{RAW_MSG}) { + push @{$charSvrSet{xkore2_character_bootstrap_raw}}, + substr($args->{RAW_MSG}, 0, $args->{RAW_MSG_SIZE}); + } return unless (UNIVERSAL::isa($net, 'Network::DirectConnection')); @@ -1036,6 +1046,13 @@ sub character_creation_failed { # CHARACTER_INFO_NEO_UNION3 and CHARACTER_INFO_NEO_UNION2 are based in charblocksize, check sub received_characters_unpackString sub received_characters_info { my ($self, $args) = @_; + $charSvrSet{received_characters_info_switch} = $args->{switch} if $args->{switch}; + $charSvrSet{xkore2_character_bootstrap_raw} = []; + $charSvrSet{xkore2_character_pages_raw} = []; + if ($args->{RAW_MSG}) { + push @{$charSvrSet{xkore2_character_bootstrap_raw}}, + substr($args->{RAW_MSG}, 0, $args->{RAW_MSG_SIZE}); + } Scalar::Util::weaken(my $weak = $self); my $timeout = {timeout => 6, time => time}; @@ -4167,6 +4184,10 @@ sub marriage_partner_name { sub login_pin_code_request { # This is ten second-level password login for 2013/3/29 upgrading of twRO my ($self, $args) = @_; + if ($args->{RAW_MSG} && ref($charSvrSet{xkore2_character_bootstrap_raw}) eq 'ARRAY') { + push @{$charSvrSet{xkore2_character_bootstrap_raw}}, + substr($args->{RAW_MSG}, 0, $args->{RAW_MSG_SIZE}); + } if ($args->{flag} ne 0 && ($config{XKore} eq "1" || $config{XKore} eq "3")) { $timeout{master}{time} = time; @@ -5450,6 +5471,7 @@ sub cart_add_failed { sub inventory_items_stackable { my ($self, $args) = @_; return unless changeToInGameState(); + $char->{xkore2_inventory_stackable_packet} = $args->{RAW_MSG}; $self->_items_list({ class => 'Actor::Item', @@ -5472,6 +5494,10 @@ sub inventory_items_stackable { sub item_list_start { my ($self, $args) = @_; $current_item_list = $args->{type}; + if ($args->{type} == INVTYPE_INVENTORY) { + $char->{xkore2_inventory_packets} = [$args->{RAW_MSG}]; + $char->{xkore2_inventory_complete} = 0; + } debug "Starting Item List. ID: $args->{type}". ($args->{name} ? " ($args->{name})\n" : "\n"), "info"; @@ -5489,6 +5515,9 @@ sub item_list_start { sub item_list_stackable { my ($self, $args) = @_; return unless changeToInGameState(); + if ($args->{type} == INVTYPE_INVENTORY && ref $char->{xkore2_inventory_packets} eq 'ARRAY') { + push @{$char->{xkore2_inventory_packets}}, $args->{RAW_MSG}; + } my $arguments = { class => 'Actor::Item', @@ -5531,6 +5560,9 @@ sub item_list_stackable { sub item_list_nonstackable { my ($self, $args) = @_; return unless changeToInGameState(); + if ($args->{type} == INVTYPE_INVENTORY && ref $char->{xkore2_inventory_packets} eq 'ARRAY') { + push @{$char->{xkore2_inventory_packets}}, $args->{RAW_MSG}; + } my $arguments = { class => 'Actor::Item', @@ -5580,6 +5612,10 @@ sub item_list_nonstackable { sub item_list_end { my ($self, $args) = @_; + if ($args->{type} == INVTYPE_INVENTORY && ref $char->{xkore2_inventory_packets} eq 'ARRAY') { + push @{$char->{xkore2_inventory_packets}}, $args->{RAW_MSG}; + $char->{xkore2_inventory_complete} = 1; + } debug TF("Ending Item List. ID: %s\n", $args->{type}), "info"; if ( $args->{type} == INVTYPE_INVENTORY ) { $char->inventory->onitemListEnd(); diff --git a/src/Network/Receive/kRO/RagexeRE_2014_10_22b.pm b/src/Network/Receive/kRO/RagexeRE_2014_10_22b.pm index aaf5a86bf1..d2dab0f548 100644 --- a/src/Network/Receive/kRO/RagexeRE_2014_10_22b.pm +++ b/src/Network/Receive/kRO/RagexeRE_2014_10_22b.pm @@ -18,8 +18,16 @@ use strict; use base qw(Network::Receive::kRO::RagexeRE_2014_09_17c); sub new { - my ($class) = @_; - return $class->SUPER::new(@_); + my ($class) = @_; + my $self = $class->SUPER::new(@_); + + my %handlers = qw( + map_loaded 0A18 + ); + + $self->{packet_lut}{$_} = $handlers{$_} for keys %handlers; + + return $self; } 1; diff --git a/src/Network/Receive/kRO/RagexeRE_2016_04_14b.pm b/src/Network/Receive/kRO/RagexeRE_2016_04_14b.pm index efa293aae6..fb4e073720 100644 --- a/src/Network/Receive/kRO/RagexeRE_2016_04_14b.pm +++ b/src/Network/Receive/kRO/RagexeRE_2016_04_14b.pm @@ -18,7 +18,15 @@ use base qw(Network::Receive::kRO::RagexeRE_2016_03_02b); sub new { my ($class) = @_; - return $class->SUPER::new(@_); + my $self = $class->SUPER::new(@_); + + my %handlers = qw( + map_loaded 02EB + ); + + $self->{packet_lut}{$_} = $handlers{$_} for keys %handlers; + + return $self; } 1; diff --git a/src/Network/Receive/kRO/RagexeRE_2017_04_12a.pm b/src/Network/Receive/kRO/RagexeRE_2017_04_12a.pm index 2cd31cc27a..72df25824b 100644 --- a/src/Network/Receive/kRO/RagexeRE_2017_04_12a.pm +++ b/src/Network/Receive/kRO/RagexeRE_2017_04_12a.pm @@ -18,7 +18,16 @@ use base qw(Network::Receive::kRO::RagexeRE_2017_02_08b); sub new { my ($class) = @_; - return $class->SUPER::new(@_); + my $self = $class->SUPER::new(@_); + + my %handlers = qw( + account_server_info 0AC4 + received_character_ID_and_Map 0AC5 + ); + + $self->{packet_lut}{$_} = $handlers{$_} for keys %handlers; + + return $self; } 1; diff --git a/src/Network/Receive/kRO/RagexeRE_2018_11_21.pm b/src/Network/Receive/kRO/RagexeRE_2018_11_21.pm index 4d653adf6d..0abdbaf948 100644 --- a/src/Network/Receive/kRO/RagexeRE_2018_11_21.pm +++ b/src/Network/Receive/kRO/RagexeRE_2018_11_21.pm @@ -28,6 +28,12 @@ sub new { $self->{packet_list}{$_} = $packets{$_} for keys %packets; + my %handlers = qw( + inventory_expansion_result 0B18 + ); + + $self->{packet_lut}{$_} = $handlers{$_} for keys %handlers; + return $self; } diff --git a/src/Network/Receive/kRO/RagexeRE_2020_04_01b.pm b/src/Network/Receive/kRO/RagexeRE_2020_04_01b.pm index 2946ab99a1..873be48068 100644 --- a/src/Network/Receive/kRO/RagexeRE_2020_04_01b.pm +++ b/src/Network/Receive/kRO/RagexeRE_2020_04_01b.pm @@ -19,6 +19,19 @@ sub new { my ($class) = @_; my $self = $class->SUPER::new(@_); + # PacketParser keeps several generations for these handler names. Pin the + # opcodes expected by this client so XKore2 reconstructs modern snapshots. + my %handlers = qw( + account_id 0283 + inventory_expansion_result 0B18 + skills_list 0B32 + map_loaded 02EB + actor_moved 09FD + actor_connected 09FE + actor_exists 09FF + ); + $self->{packet_lut}{$_} = $handlers{$_} for keys %handlers; + $self->{vender_items_list_item_pack} = 'V v2 C V C3 a16 a25 V v'; $self->{npc_store_info_pack} = "V V C V"; $self->{buying_store_items_list_pack} = "V v C V"; diff --git a/src/Network/Receive/kRO/RagexeRE_2021_11_03.pm b/src/Network/Receive/kRO/RagexeRE_2021_11_03.pm index a3b784dbe5..79468037e4 100644 --- a/src/Network/Receive/kRO/RagexeRE_2021_11_03.pm +++ b/src/Network/Receive/kRO/RagexeRE_2021_11_03.pm @@ -19,6 +19,12 @@ sub new { my ($class) = @_; my $self = $class->SUPER::new(@_); + my %handlers = qw( + received_characters 0B72 + ); + + $self->{packet_lut}{$_} = $handlers{$_} for keys %handlers; + return $self; } diff --git a/src/Network/Receive/kRO/Sakexe_0.pm b/src/Network/Receive/kRO/Sakexe_0.pm index c66d482fc6..db239029b7 100644 --- a/src/Network/Receive/kRO/Sakexe_0.pm +++ b/src/Network/Receive/kRO/Sakexe_0.pm @@ -1075,6 +1075,7 @@ sub guild_chat { sub inventory_items_nonstackable { my ($self, $args) = @_; return unless changeToInGameState(); + $char->{xkore2_inventory_nonstackable_packet} = $args->{RAW_MSG}; $self->_items_list({ class => 'Actor::Item', diff --git a/src/Network/Receive/kRO/Sakexe_2007_10_02a.pm b/src/Network/Receive/kRO/Sakexe_2007_10_02a.pm index f8f1a9b1b5..42f7512754 100644 --- a/src/Network/Receive/kRO/Sakexe_2007_10_02a.pm +++ b/src/Network/Receive/kRO/Sakexe_2007_10_02a.pm @@ -22,7 +22,15 @@ use base qw(Network::Receive::kRO::Sakexe_2007_05_07a); sub new { my ($class) = @_; - return $class->SUPER::new(@_); + my $self = $class->SUPER::new(@_); + + my %handlers = qw( + account_id 0283 + ); + + $self->{packet_lut}{$_} = $handlers{$_} for keys %handlers; + + return $self; } 1; \ No newline at end of file diff --git a/src/Network/Receive/kRO/Sakexe_2008_01_02a.pm b/src/Network/Receive/kRO/Sakexe_2008_01_02a.pm index c0ec0a3835..bac1a19ea5 100644 --- a/src/Network/Receive/kRO/Sakexe_2008_01_02a.pm +++ b/src/Network/Receive/kRO/Sakexe_2008_01_02a.pm @@ -22,7 +22,15 @@ use base qw(Network::Receive::kRO::Sakexe_2007_11_27a); sub new { my ($class) = @_; - return $class->SUPER::new(@_); + my $self = $class->SUPER::new(@_); + + my %handlers = qw( + map_loaded 02EB + ); + + $self->{packet_lut}{$_} = $handlers{$_} for keys %handlers; + + return $self; } 1; \ No newline at end of file diff --git a/src/Network/Send/kRO/RagexeRE_2020_03_04a.pm b/src/Network/Send/kRO/RagexeRE_2020_03_04a.pm index 82e10f3756..51a348881a 100644 --- a/src/Network/Send/kRO/RagexeRE_2020_03_04a.pm +++ b/src/Network/Send/kRO/RagexeRE_2020_03_04a.pm @@ -14,6 +14,7 @@ package Network::Send::kRO::RagexeRE_2020_03_04a; use strict; use base qw(Network::Send::kRO::RagexeRE_2018_11_21); +use Utils::Rijndael; sub new { my ($class) = @_; @@ -21,7 +22,7 @@ sub new { my %packets = ( '0064' => ['token_login', 'V Z24 Z24 C', [qw(version username password master_version)]], # 55 - '0ACF' => ['master_login', 'a4 Z25 a32 a5', [qw(game_code username password flag)]], + '0ACF' => ['master_login', 'a4 Z25 a32 a5', [qw(game_code username password_rijndael flag)]], ); $self->{packet_list}{$_} = $packets{$_} for keys %packets; @@ -48,4 +49,30 @@ sub new { return $self; } +# 0ACF uses a 32-byte Rijndael block. The generic master-login parser uses +# the older 24-byte variant, so modern clients need a packet-specific parser. +sub parse_master_login { + my ($self, $args) = @_; + + return unless exists $args->{password_rijndael}; + + my $key = pack('C32', ( + 0x06, 0xA9, 0x21, 0x40, 0x36, 0xB8, 0xA1, 0x5B, + 0x51, 0x2E, 0x03, 0xD5, 0x34, 0x12, 0x00, 0x06, + 0x06, 0xA9, 0x21, 0x40, 0x36, 0xB8, 0xA1, 0x5B, + 0x51, 0x2E, 0x03, 0xD5, 0x34, 0x12, 0x00, 0x06, + )); + my $chain = pack('C32', ( + 0x3D, 0xAF, 0xBA, 0x42, 0x9D, 0x9E, 0xB4, 0x30, + 0xB4, 0x22, 0xDA, 0x80, 0x2C, 0x9F, 0xAC, 0x41, + 0x3D, 0xAF, 0xBA, 0x42, 0x9D, 0x9E, 0xB4, 0x30, + 0xB4, 0x22, 0xDA, 0x80, 0x2C, 0x9F, 0xAC, 0x41, + )); + my $rijndael = Utils::Rijndael->new; + $rijndael->MakeKey($key, $chain, 32, 32); + $args->{password} = unpack('Z32', $rijndael->Decrypt( + pack('a32', $args->{password_rijndael}), undef, 32, 0 + )); +} + 1; diff --git a/src/Network/XKore2/MapServer.pm b/src/Network/XKore2/MapServer.pm index da6f0ec318..8cdf4262d7 100644 --- a/src/Network/XKore2/MapServer.pm +++ b/src/Network/XKore2/MapServer.pm @@ -202,7 +202,6 @@ sub map_loaded { $self->send_inventory($client, $char); $self->send_ground_items($client); $self->send_portals($client); - $self->send_npcs($client); $self->send_monsters($client); $self->send_npcs($client); $self->send_pets($client); @@ -472,7 +471,48 @@ sub send_player_info { my ($self, $client, $char) = @_; my $data = undef; + # Send skill information + if (($self->{recvPacketParser}{packet_lut}{skills_list} || '') eq '0B32') { + foreach my $ID (@skillsID) { + $data .= pack('v V v3 C v', + $char->{skills}{$ID}{ID}, $char->{skills}{$ID}{targetType}, + $char->{skills}{$ID}{lv}, $char->{skills}{$ID}{sp}, + $char->{skills}{$ID}{range}, $char->{skills}{$ID}{up}, + $char->{skills}{$ID}{lv2}); + } + $data = pack('C2 v', 0x32, 0x0B, length($data) + 4) . $data; + } else { + foreach my $ID (@skillsID) { + $data .= pack('v2 x2 v3 a24 C', + $char->{skills}{$ID}{ID}, $char->{skills}{$ID}{targetType}, + $char->{skills}{$ID}{lv}, $char->{skills}{$ID}{sp}, + $char->{skills}{$ID}{range}, $ID, $char->{skills}{$ID}{up}); + } + $data = pack('C2 v', 0x0F, 0x01, length($data) + 4) . $data; + } + $client->send($data); + + # Send weapon/shield appearance + $client->send(pack('C2 a4 C v2', 0xD7, 0x01, + $char->{ID}, 2, $char->{weapon}, $char->{shield})); + + # Send attack range + $client->send(pack('C2 v', 0x3A, 0x01, $char->{attack_range})); + + # More stats + $data = pack('C2 v V', 0xB0, 0x00, 0, $char->{walk_speed} * 1000); # Walk speed + $data .= pack('C2 v V', 0xB0, 0x00, 5, $char->{hp}); # Current HP + $data .= pack('C2 v V', 0xB0, 0x00, 6, $char->{hp_max}); # Max HP + $data .= pack('C2 v V', 0xB0, 0x00, 7, $char->{sp}); # Current SP + $data .= pack('C2 v V', 0xB0, 0x00, 8, $char->{sp_max}); # Max SP + $data .= pack('C2 v V', 0xB0, 0x00, 12, $char->{points_skill}); # Skill points left + $data .= pack('C2 v V', 0xB0, 0x00, 24, $char->{weight} * 10); # Current weight + $data .= pack('C2 v V', 0xB0, 0x00, 25, $char->{weight_max} * 10); # Max weight + $data .= pack('C2 v V', 0xB0, 0x00, 53, $char->{attack_delay}); # Attack speed + $client->send($data); + # Player stats. + $data = undef; $data = pack('C2 v1 C12 v12 x4', 0xBD, 0x00, $char->{points_free}, @@ -491,18 +531,6 @@ sub send_player_info { ); $client->send($data); - # More stats - $data = pack('C2 v V', 0xB0, 0x00, 0, $char->{walk_speed} * 1000); # Walk speed - $data .= pack('C2 v V', 0xB0, 0x00, 5, $char->{hp}); # Current HP - $data .= pack('C2 v V', 0xB0, 0x00, 6, $char->{hp_max}); # Max HP - $data .= pack('C2 v V', 0xB0, 0x00, 7, $char->{sp}); # Current SP - $data .= pack('C2 v V', 0xB0, 0x00, 8, $char->{sp_max}); # Max SP - $data .= pack('C2 v V', 0xB0, 0x00, 12, $char->{points_skill}); # Skill points left - $data .= pack('C2 v V', 0xB0, 0x00, 24, $char->{weight} * 10); # Current weight - $data .= pack('C2 v V', 0xB0, 0x00, 25, $char->{weight_max} * 10); # Max weight - $data .= pack('C2 v V', 0xB0, 0x00, 53, $char->{attack_delay}); # Attack speed - $client->send($data); - # Base stat info (str, agi, vit, int, dex, luk) this time with bonus $data = pack('C2 V3', 0x41, 0x01, 13, $char->{str}, $char->{str_bonus}); $data .= pack('C2 V3', 0x41, 0x01, 14, $char->{agi}, $char->{agi_bonus}); @@ -517,13 +545,8 @@ sub send_player_info { $char->{ID}, $char->{look}{head}, $char->{look}{body}) ); - # Send attack range - $data = pack('C2 v', 0x3A, 0x01, $char->{attack_range}); - # Send weapon/shield appearance - $data .= pack('C2 a4 C v2', 0xD7, 0x01, $char->{ID}, 2, $char->{weapon}, $char->{shield}); # Send status info - $data .= pack('v a4 v3 x', 0x119, $char->{ID}, $char->{opt1}, $char->{opt2}, $char->{option}); - $client->send($data); + $data = pack('v a4 v3 x', 0x119, $char->{ID}, $char->{opt1}, $char->{opt2}, $char->{option}); if ($RunOnce) { foreach my $ID (keys %{$char->{statuses}}) { @@ -544,27 +567,17 @@ sub send_player_info { } } } - + $client->send($data) if (length($data) > 0); # Send spirit sphere information - $data = pack('C2 a4 v', 0xD0, 0x01, $char->{ID}, $char->{spirits}) if ($char->{spirits}); + $data = undef; + $data .= pack('C2 a4 v', 0xD0, 0x01, $char->{ID}, $char->{spirits}) if ($char->{spirits}); # Send exp-required-to-level-up info $data .= pack('C2 v V', 0xB1, 0x00, 22, $char->{exp_max}); $data .= pack('C2 v V', 0xB1, 0x00, 23, $char->{exp_job_max}); $client->send($data); - # Send skill information - $data = undef; - foreach my $ID (@skillsID) { - $data .= pack('v2 x2 v3 a24 C', - $char->{skills}{$ID}{ID}, $char->{skills}{$ID}{targetType}, - $char->{skills}{$ID}{lv}, $char->{skills}{$ID}{sp}, - $char->{skills}{$ID}{range}, $ID, $char->{skills}{$ID}{up}); - } - $data = pack('C2 v', 0x0F, 0x01, length($data) + 4) . $data; - $client->send($data); - # Send Hotkeys if ($hotkeyList) { $data = undef; @@ -591,6 +604,7 @@ sub send_player_info { # # Send info about surrounding players + $data = undef; foreach my $player (@{$playersList->getItems()}) { my $coords = ''; shiftPack(\$coords, $player->{pos_to}{x}, 10); @@ -611,6 +625,31 @@ sub send_player_info { sub send_inventory { my ($self, $client, $char) = @_; my $data = undef; + + # Modern item-list records contain fields that the legacy 00A3/00A4 + # reconstruction below cannot represent. Replay the snapshot received + # from the real server to preserve equipment, cards and random options. + if (UNIVERSAL::isa($char, 'Actor::You') + && $char->{xkore2_inventory_complete} + && ref $char->{xkore2_inventory_packets} eq 'ARRAY' + && @{$char->{xkore2_inventory_packets}}) { + $client->send(join('', @{$char->{xkore2_inventory_packets}})); + $client->send(pack('C2 v', 0x3C, 0x01, $char->{arrow})) if ($char->{arrow}); + return; + } + + if (UNIVERSAL::isa($char, 'Actor::You')) { + my @captured = grep { defined $_ && length $_ } ( + $char->{xkore2_inventory_stackable_packet}, + $char->{xkore2_inventory_nonstackable_packet}, + ); + if (@captured) { + $client->send(join('', @captured)); + $client->send(pack('C2 v', 0x3C, 0x01, $char->{arrow})) if ($char->{arrow}); + return; + } + } + # Send cart information includeing the items if (!$client->{session}{dummy} && $char->cartActive && $RunOnce) { $data = pack('C2 v2 V2', 0x21, 0x01, $char->cart->items, $char->cart->items_max, ($char->cart->{weight} * 10), ($char->cart->{weight_max} * 10)); @@ -735,9 +774,7 @@ sub send_npc_info { sub send_avoid_sprite_error_hack { my ($self, $client, $char) = @_; - my $data = pack('C15', 0x29, 0x02, 0xA7, 0x94, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40); - $client->send($data); - $data = $self->{recvPacketParser}->reconstruct({ + my $data = $self->{recvPacketParser}->reconstruct({ switch => '0229', ID => $char->{ID}, opt1 => $char->{opt1}, @@ -774,6 +811,13 @@ sub map_login { })); } + if (exists $self->{recvPacketParser}{packet_lut}{inventory_expansion_result}) { + $client->send($self->{recvPacketParser}->reconstruct({ + switch => 'inventory_expansion_result', + result => 0, + })); + } + if (exists $self->{recvPacketParser}{packet_lut}{account_id}) { $client->send($self->{recvPacketParser}->reconstruct({ switch => 'account_id', diff --git a/src/test/NetworkTest.pm b/src/test/NetworkTest.pm index 3c6b277e68..d8769c793a 100644 --- a/src/test/NetworkTest.pm +++ b/src/test/NetworkTest.pm @@ -6,6 +6,7 @@ use Test::More; use Misc; use Network::Receive; use Network::Send; +use Utils::Rijndael; sub start { my %tests = ( @@ -102,6 +103,35 @@ sub start { done_testing(); } } + + subtest '0ACF 32-byte Rijndael password' => sub { + my $instance = Network::Send->create(undef, 'kRO_RagexeRE_2020_04_01b'); + ok($instance, 'create modern kRO sender'); + + my $password = 'xkore2-modern'; + my $key = pack('C32', ( + 0x06, 0xA9, 0x21, 0x40, 0x36, 0xB8, 0xA1, 0x5B, + 0x51, 0x2E, 0x03, 0xD5, 0x34, 0x12, 0x00, 0x06, + 0x06, 0xA9, 0x21, 0x40, 0x36, 0xB8, 0xA1, 0x5B, + 0x51, 0x2E, 0x03, 0xD5, 0x34, 0x12, 0x00, 0x06, + )); + my $chain = pack('C32', ( + 0x3D, 0xAF, 0xBA, 0x42, 0x9D, 0x9E, 0xB4, 0x30, + 0xB4, 0x22, 0xDA, 0x80, 0x2C, 0x9F, 0xAC, 0x41, + 0x3D, 0xAF, 0xBA, 0x42, 0x9D, 0x9E, 0xB4, 0x30, + 0xB4, 0x22, 0xDA, 0x80, 0x2C, 0x9F, 0xAC, 0x41, + )); + my $rijndael = Utils::Rijndael->new; + $rijndael->MakeKey($key, $chain, 32, 32); + my $args = { + password_rijndael => $rijndael->Encrypt( + pack('a32', $password), undef, 32, 0 + ), + }; + $instance->parse_master_login($args); + is($args->{password}, $password, 'decrypts the client password'); + done_testing(); + }; } sub reduce_struct {