tests: extend login smoke to entergame
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@@ -23,6 +24,9 @@ namespace
|
||||
constexpr size_t LOGIN_MAX_LEN_LOCAL = 30;
|
||||
constexpr size_t PASSWD_MAX_LEN_LOCAL = 16;
|
||||
constexpr size_t ACCOUNT_STATUS_MAX_LEN_LOCAL = 8;
|
||||
constexpr size_t PLAYER_PER_ACCOUNT_LOCAL = 4;
|
||||
constexpr size_t CHARACTER_NAME_MAX_LEN_LOCAL = 64;
|
||||
constexpr size_t GUILD_NAME_MAX_LEN_LOCAL = 12;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct PacketGCPhase
|
||||
@@ -73,6 +77,39 @@ struct PacketCGLogin2
|
||||
uint32_t login_key;
|
||||
};
|
||||
|
||||
struct SimplePlayerLocal
|
||||
{
|
||||
uint32_t id;
|
||||
char name[CHARACTER_NAME_MAX_LEN_LOCAL + 1];
|
||||
uint8_t job;
|
||||
uint8_t level;
|
||||
uint32_t play_minutes;
|
||||
uint8_t st;
|
||||
uint8_t ht;
|
||||
uint8_t dx;
|
||||
uint8_t iq;
|
||||
uint16_t main_part;
|
||||
uint8_t change_name;
|
||||
uint16_t hair_part;
|
||||
uint8_t dummy[4];
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
uint32_t addr;
|
||||
uint16_t port;
|
||||
uint8_t skill_group;
|
||||
};
|
||||
|
||||
struct PacketGCLoginSuccess4
|
||||
{
|
||||
uint16_t header;
|
||||
uint16_t length;
|
||||
SimplePlayerLocal players[PLAYER_PER_ACCOUNT_LOCAL];
|
||||
uint32_t guild_id[PLAYER_PER_ACCOUNT_LOCAL];
|
||||
char guild_name[PLAYER_PER_ACCOUNT_LOCAL][GUILD_NAME_MAX_LEN_LOCAL + 1];
|
||||
uint32_t handle;
|
||||
uint32_t random_key;
|
||||
};
|
||||
|
||||
struct PacketGCAuthSuccess
|
||||
{
|
||||
uint16_t header;
|
||||
@@ -94,6 +131,91 @@ struct PacketGCEmpire
|
||||
uint16_t length;
|
||||
uint8_t empire;
|
||||
};
|
||||
|
||||
struct PacketCGPlayerCreate
|
||||
{
|
||||
uint16_t header;
|
||||
uint16_t length;
|
||||
uint8_t index;
|
||||
char name[CHARACTER_NAME_MAX_LEN_LOCAL + 1];
|
||||
uint16_t job;
|
||||
uint8_t shape;
|
||||
uint8_t con;
|
||||
uint8_t intel;
|
||||
uint8_t str;
|
||||
uint8_t dex;
|
||||
};
|
||||
|
||||
struct PacketGCPlayerCreateSuccess
|
||||
{
|
||||
uint16_t header;
|
||||
uint16_t length;
|
||||
uint8_t account_character_index;
|
||||
SimplePlayerLocal player;
|
||||
};
|
||||
|
||||
struct PacketGCCreateFailure
|
||||
{
|
||||
uint16_t header;
|
||||
uint16_t length;
|
||||
uint8_t type;
|
||||
};
|
||||
|
||||
struct PacketCGPlayerSelect
|
||||
{
|
||||
uint16_t header;
|
||||
uint16_t length;
|
||||
uint8_t index;
|
||||
};
|
||||
|
||||
struct PacketCGEnterGame
|
||||
{
|
||||
uint16_t header;
|
||||
uint16_t length;
|
||||
};
|
||||
|
||||
struct PacketCGClientVersion
|
||||
{
|
||||
uint16_t header;
|
||||
uint16_t length;
|
||||
char filename[32 + 1];
|
||||
char timestamp[32 + 1];
|
||||
};
|
||||
|
||||
struct PacketGCMainCharacter
|
||||
{
|
||||
enum
|
||||
{
|
||||
MUSIC_NAME_LEN = 24
|
||||
};
|
||||
|
||||
uint16_t header;
|
||||
uint16_t length;
|
||||
uint32_t vid;
|
||||
uint16_t race_num;
|
||||
char name[CHARACTER_NAME_MAX_LEN_LOCAL + 1];
|
||||
char bgm_name[MUSIC_NAME_LEN + 1];
|
||||
float bgm_vol;
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
int32_t z;
|
||||
uint8_t empire;
|
||||
uint8_t skill_group;
|
||||
};
|
||||
|
||||
struct PacketGCTime
|
||||
{
|
||||
uint16_t header;
|
||||
uint16_t length;
|
||||
time_t time;
|
||||
};
|
||||
|
||||
struct PacketGCChannel
|
||||
{
|
||||
uint16_t header;
|
||||
uint16_t length;
|
||||
uint8_t channel;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
void Expect(bool condition, std::string_view message)
|
||||
@@ -281,6 +403,26 @@ uint16_t FrameLength(const std::vector<uint8_t>& frame)
|
||||
return *reinterpret_cast<const uint16_t*>(frame.data() + sizeof(uint16_t));
|
||||
}
|
||||
|
||||
int RemainingTimeoutMs(std::chrono::steady_clock::time_point deadline)
|
||||
{
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= deadline)
|
||||
return 0;
|
||||
|
||||
return static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now).count());
|
||||
}
|
||||
|
||||
uint8_t FindFirstPlayableCharacterIndex(const PacketGCLoginSuccess4& success)
|
||||
{
|
||||
for (uint8_t index = 0; index < PLAYER_PER_ACCOUNT_LOCAL; ++index)
|
||||
{
|
||||
if (success.players[index].id != 0 && success.players[index].change_name == 0)
|
||||
return index;
|
||||
}
|
||||
|
||||
throw std::runtime_error("account has no playable characters");
|
||||
}
|
||||
|
||||
uint32_t Authenticate(const std::string& host, uint16_t auth_port, const std::string& login, const std::string& password)
|
||||
{
|
||||
EncryptedClient auth_client(host, auth_port);
|
||||
@@ -322,11 +464,8 @@ uint32_t Authenticate(const std::string& host, uint16_t auth_port, const std::st
|
||||
throw std::runtime_error("did not receive AUTH_SUCCESS");
|
||||
}
|
||||
|
||||
void LoginToChannel(const std::string& host, uint16_t channel_port, const std::string& login, uint32_t login_key)
|
||||
PacketGCLoginSuccess4 LoginToChannel(EncryptedClient& channel_client, const std::string& login, uint32_t login_key)
|
||||
{
|
||||
EncryptedClient channel_client(host, channel_port);
|
||||
channel_client.Handshake();
|
||||
|
||||
PacketCGLogin2 login_packet {};
|
||||
login_packet.header = CG::LOGIN2;
|
||||
login_packet.length = sizeof(login_packet);
|
||||
@@ -337,6 +476,7 @@ void LoginToChannel(const std::string& host, uint16_t channel_port, const std::s
|
||||
bool saw_empire = false;
|
||||
bool saw_login_success = false;
|
||||
uint8_t empire = 0;
|
||||
PacketGCLoginSuccess4 login_success {};
|
||||
std::vector<uint8_t> frame;
|
||||
|
||||
for (int i = 0; i < 16; ++i)
|
||||
@@ -367,6 +507,8 @@ void LoginToChannel(const std::string& host, uint16_t channel_port, const std::s
|
||||
|
||||
if (header == GC::LOGIN_SUCCESS4)
|
||||
{
|
||||
Expect(frame.size() == sizeof(PacketGCLoginSuccess4), "unexpected channel login success size");
|
||||
std::memcpy(&login_success, frame.data(), sizeof(login_success));
|
||||
saw_login_success = true;
|
||||
std::cout << "channel_login_success length=" << length << "\n";
|
||||
break;
|
||||
@@ -375,6 +517,178 @@ void LoginToChannel(const std::string& host, uint16_t channel_port, const std::s
|
||||
|
||||
Expect(saw_empire, "did not receive EMPIRE");
|
||||
Expect(saw_login_success, "did not receive LOGIN_SUCCESS4");
|
||||
return login_success;
|
||||
}
|
||||
|
||||
uint8_t CreateCharacter(EncryptedClient& channel_client, std::string_view create_name)
|
||||
{
|
||||
PacketCGPlayerCreate create_packet {};
|
||||
create_packet.header = CG::CHARACTER_CREATE;
|
||||
create_packet.length = sizeof(create_packet);
|
||||
create_packet.index = 0;
|
||||
create_packet.job = 0;
|
||||
create_packet.shape = 0;
|
||||
create_packet.con = 4;
|
||||
create_packet.intel = 4;
|
||||
create_packet.str = 4;
|
||||
create_packet.dex = 4;
|
||||
std::strncpy(create_packet.name, create_name.data(), sizeof(create_packet.name) - 1);
|
||||
channel_client.SendEncryptedPacket(create_packet);
|
||||
|
||||
std::vector<uint8_t> frame;
|
||||
for (int i = 0; i < 16; ++i)
|
||||
{
|
||||
Expect(channel_client.WaitForFrame(frame, 5000), "timed out waiting for character create response");
|
||||
const auto header = FrameHeader(frame);
|
||||
|
||||
if (header == GC::PHASE)
|
||||
continue;
|
||||
|
||||
if (header == GC::PLAYER_CREATE_FAILURE)
|
||||
{
|
||||
if (frame.size() == sizeof(PacketGCCreateFailure))
|
||||
{
|
||||
const auto* failure = reinterpret_cast<const PacketGCCreateFailure*>(frame.data());
|
||||
throw std::runtime_error("character create failed type=" + std::to_string(failure->type));
|
||||
}
|
||||
|
||||
throw std::runtime_error("character create failed");
|
||||
}
|
||||
|
||||
if (header == GC::PLAYER_CREATE_SUCCESS)
|
||||
{
|
||||
Expect(frame.size() == sizeof(PacketGCPlayerCreateSuccess), "unexpected player create success size");
|
||||
const auto* success = reinterpret_cast<const PacketGCPlayerCreateSuccess*>(frame.data());
|
||||
std::cout << "character_created index=" << static_cast<int>(success->account_character_index)
|
||||
<< " name=" << success->player.name << "\n";
|
||||
return success->account_character_index;
|
||||
}
|
||||
}
|
||||
|
||||
throw std::runtime_error("did not receive PLAYER_CREATE_SUCCESS");
|
||||
}
|
||||
|
||||
void SelectCharacter(EncryptedClient& channel_client, uint8_t character_index)
|
||||
{
|
||||
PacketCGPlayerSelect select_packet {};
|
||||
select_packet.header = CG::CHARACTER_SELECT;
|
||||
select_packet.length = sizeof(select_packet);
|
||||
select_packet.index = character_index;
|
||||
channel_client.SendEncryptedPacket(select_packet);
|
||||
|
||||
bool saw_phase_loading = false;
|
||||
bool saw_main_character = false;
|
||||
std::vector<uint8_t> frame;
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
|
||||
while (true)
|
||||
{
|
||||
const int timeout_ms = RemainingTimeoutMs(deadline);
|
||||
Expect(timeout_ms > 0, "timed out waiting for character select response");
|
||||
Expect(channel_client.WaitForFrame(frame, timeout_ms), "timed out waiting for character select response");
|
||||
const auto header = FrameHeader(frame);
|
||||
|
||||
if (header == GC::LOGIN_FAILURE)
|
||||
{
|
||||
Expect(frame.size() == sizeof(PacketGCLoginFailure), "unexpected character select failure size");
|
||||
const auto* failure = reinterpret_cast<const PacketGCLoginFailure*>(frame.data());
|
||||
throw std::runtime_error(std::string("character select failed: ") + failure->status);
|
||||
}
|
||||
|
||||
if (header == GC::PHASE)
|
||||
{
|
||||
Expect(frame.size() == sizeof(PacketGCPhase), "unexpected phase packet size");
|
||||
const auto* phase = reinterpret_cast<const PacketGCPhase*>(frame.data());
|
||||
if (phase->phase == PHASE_LOADING)
|
||||
{
|
||||
saw_phase_loading = true;
|
||||
std::cout << "phase_loading\n";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (header == GC::MAIN_CHARACTER)
|
||||
{
|
||||
Expect(frame.size() == sizeof(PacketGCMainCharacter), "unexpected main character packet size");
|
||||
const auto* main_character = reinterpret_cast<const PacketGCMainCharacter*>(frame.data());
|
||||
Expect(main_character->name[0] != '\0', "main character name is empty");
|
||||
saw_main_character = true;
|
||||
std::cout << "main_character name=" << main_character->name << "\n";
|
||||
}
|
||||
|
||||
if (saw_phase_loading && saw_main_character)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void SendClientVersion(EncryptedClient& channel_client, std::string_view client_version)
|
||||
{
|
||||
PacketCGClientVersion version_packet {};
|
||||
version_packet.header = CG::CLIENT_VERSION;
|
||||
version_packet.length = sizeof(version_packet);
|
||||
std::strncpy(version_packet.filename, "Metin2_Debug.exe", sizeof(version_packet.filename) - 1);
|
||||
std::strncpy(version_packet.timestamp, client_version.data(), sizeof(version_packet.timestamp) - 1);
|
||||
channel_client.SendEncryptedPacket(version_packet);
|
||||
std::cout << "client_version " << version_packet.timestamp << "\n";
|
||||
}
|
||||
|
||||
void EnterGame(EncryptedClient& channel_client)
|
||||
{
|
||||
PacketCGEnterGame enter_game {};
|
||||
enter_game.header = CG::ENTERGAME;
|
||||
enter_game.length = sizeof(enter_game);
|
||||
channel_client.SendEncryptedPacket(enter_game);
|
||||
|
||||
bool saw_phase_game = false;
|
||||
bool saw_time = false;
|
||||
bool saw_channel = false;
|
||||
std::vector<uint8_t> frame;
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
|
||||
while (true)
|
||||
{
|
||||
const int timeout_ms = RemainingTimeoutMs(deadline);
|
||||
Expect(timeout_ms > 0, "timed out waiting for entergame response");
|
||||
Expect(channel_client.WaitForFrame(frame, timeout_ms), "timed out waiting for entergame response");
|
||||
const auto header = FrameHeader(frame);
|
||||
|
||||
if (header == GC::LOGIN_FAILURE)
|
||||
{
|
||||
Expect(frame.size() == sizeof(PacketGCLoginFailure), "unexpected entergame failure size");
|
||||
const auto* failure = reinterpret_cast<const PacketGCLoginFailure*>(frame.data());
|
||||
throw std::runtime_error(std::string("entergame failed: ") + failure->status);
|
||||
}
|
||||
|
||||
if (header == GC::PHASE)
|
||||
{
|
||||
Expect(frame.size() == sizeof(PacketGCPhase), "unexpected phase packet size");
|
||||
const auto* phase = reinterpret_cast<const PacketGCPhase*>(frame.data());
|
||||
if (phase->phase == PHASE_GAME)
|
||||
{
|
||||
saw_phase_game = true;
|
||||
std::cout << "phase_game\n";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (header == GC::TIME)
|
||||
{
|
||||
Expect(frame.size() == sizeof(PacketGCTime), "unexpected time packet size");
|
||||
saw_time = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (header == GC::CHANNEL)
|
||||
{
|
||||
Expect(frame.size() == sizeof(PacketGCChannel), "unexpected channel packet size");
|
||||
const auto* channel = reinterpret_cast<const PacketGCChannel*>(frame.data());
|
||||
saw_channel = true;
|
||||
std::cout << "game_channel channel=" << static_cast<int>(channel->channel) << "\n";
|
||||
}
|
||||
|
||||
if (saw_phase_game && saw_time && saw_channel)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,15 +696,18 @@ int main(int argc, char** argv)
|
||||
{
|
||||
try
|
||||
{
|
||||
Expect(argc == 6,
|
||||
Expect(argc >= 6 && argc <= 8,
|
||||
"usage: metin_login_smoke <host> <auth_port> <channel_port> <login> <password>\n"
|
||||
" or: metin_login_smoke <host> <auth_port> <channel_port> <login> --password-env=ENV_NAME");
|
||||
" or: metin_login_smoke <host> <auth_port> <channel_port> <login> --password-env=ENV_NAME\n"
|
||||
" optional: --create-character-name=NAME [--client-version=VERSION]");
|
||||
|
||||
const std::string host = argv[1];
|
||||
const uint16_t auth_port = static_cast<uint16_t>(std::stoi(argv[2]));
|
||||
const uint16_t channel_port = static_cast<uint16_t>(std::stoi(argv[3]));
|
||||
const std::string login = argv[4];
|
||||
std::string password;
|
||||
std::string create_character_name;
|
||||
std::string client_version = "1215955205";
|
||||
|
||||
const std::string password_arg = argv[5];
|
||||
const std::string prefix = "--password-env=";
|
||||
@@ -405,11 +722,52 @@ int main(int argc, char** argv)
|
||||
password = password_arg;
|
||||
}
|
||||
|
||||
for (int i = 6; i < argc; ++i)
|
||||
{
|
||||
const std::string create_arg = argv[i];
|
||||
const std::string create_prefix = "--create-character-name=";
|
||||
const std::string version_prefix = "--client-version=";
|
||||
if (create_arg.rfind(create_prefix, 0) == 0)
|
||||
{
|
||||
create_character_name = create_arg.substr(create_prefix.size());
|
||||
Expect(!create_character_name.empty(), "create character name is empty");
|
||||
Expect(create_character_name.size() <= CHARACTER_NAME_MAX_LEN_LOCAL, "create character name too long");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (create_arg.rfind(version_prefix, 0) == 0)
|
||||
{
|
||||
client_version = create_arg.substr(version_prefix.size());
|
||||
Expect(!client_version.empty(), "client version is empty");
|
||||
continue;
|
||||
}
|
||||
|
||||
throw std::runtime_error("unknown optional argument");
|
||||
}
|
||||
|
||||
Expect(login.size() <= LOGIN_MAX_LEN_LOCAL, "login too long");
|
||||
Expect(password.size() <= PASSWD_MAX_LEN_LOCAL, "password too long");
|
||||
|
||||
const uint32_t login_key = Authenticate(host, auth_port, login, password);
|
||||
LoginToChannel(host, channel_port, login, login_key);
|
||||
EncryptedClient channel_client(host, channel_port);
|
||||
channel_client.Handshake();
|
||||
const PacketGCLoginSuccess4 login_success = LoginToChannel(channel_client, login, login_key);
|
||||
|
||||
uint8_t character_index = 0;
|
||||
try
|
||||
{
|
||||
character_index = FindFirstPlayableCharacterIndex(login_success);
|
||||
std::cout << "selected_character index=" << static_cast<int>(character_index) << "\n";
|
||||
}
|
||||
catch (const std::exception&)
|
||||
{
|
||||
Expect(!create_character_name.empty(), "account has no playable characters and no create-character name was provided");
|
||||
character_index = CreateCharacter(channel_client, create_character_name);
|
||||
}
|
||||
|
||||
SelectCharacter(channel_client, character_index);
|
||||
SendClientVersion(channel_client, client_version);
|
||||
EnterGame(channel_client);
|
||||
|
||||
std::cout << "full_login_success\n";
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user