Compare commits

..

14 Commits

Author SHA1 Message Date
Martin Michelsen 7cbd9402d0 fix CallNativeFunctionGC
Docker / Build (push) Has been cancelled
2025-05-31 15:15:03 -07:00
Martin Michelsen 0396337994 fix inventory/bank debug messages 2025-05-31 15:14:04 -07:00
Martin Michelsen 6fbc0829ae add patch to replace Pinz shop cards 2025-05-31 10:56:01 -07:00
Martin Michelsen 4f41cbc9ce fix description generated in $item command 2025-05-31 10:07:11 -07:00
Martin Michelsen d1e6d75d70 fix TethVer detection hack 2025-05-31 10:04:09 -07:00
Martin Michelsen 067f2439ca make redirect wait apply to SocketChannels as well 2025-05-31 09:34:09 -07:00
Martin Michelsen 2d2edbd7be fix ping exception handler 2025-05-31 09:29:01 -07:00
Vargur f5f457aa6f Fix HTTP endpoint logic: remove incorrect negation in rare-tables path check
The !req.path.starts_with( was causing every subsequent command to be processed as a rare-tables substring command.
2025-05-30 19:29:52 -07:00
Martin Michelsen aabbafb749 fix game flag translation across v2/v3 boundary 2025-05-28 22:01:54 -07:00
Martin Michelsen e72e37f713 implement extended $infhp features on proxy server; closes #501 2025-05-27 19:34:47 -07:00
Martin Michelsen f884893b18 reprioritize to-do list 2025-05-27 19:34:25 -07:00
Martin Michelsen c74c0e2250 fix conditions 2025-05-26 23:52:43 -07:00
Martin Michelsen 5f4d2ec891 complete implementation of $checkchar and make slot count configurable; closes #645 2025-05-26 21:55:19 -07:00
Martin Michelsen 33b0ab3ed3 improve BB proxy functionality 2025-05-26 18:56:23 -07:00
41 changed files with 625 additions and 255 deletions
+1 -1
View File
@@ -602,7 +602,7 @@ Some commands only work for clients not in proxy sessions. The chat commands are
* `$savechar <slot>`: Save your current character data on the server in the specified slot. See the [server-side saves section](#server-side-saves) for more details.
* `$loadchar <slot>`: Save your current character data on the server in the specified slot. See the [server-side saves section](#server-side-saves) for more details.
* `$bbchar <username> <password> <slot>`: Save your current character data on the server in a different account's BB character slots. See the [server-side saves section](#server-side-saves) for more details.
* `$checkchar <slot>`: Tells you basic information about a server-side character previously saved using `$savechar`.
* `$checkchar [slot]`: Tells you basic information about a server-side character previously saved using `$savechar`. If `slot` is not given, tells you which slots are used and which are free.
* `$deletechar <slot>`: Deletes a server-side character previously saved using `$savechar`.
* `$edit <stat> <value>`: Modify your character data. See the [using $edit](#using-edit) section for details.
+6 -5
View File
@@ -1,13 +1,14 @@
## General
- Make UI strings localizable (e.g. entries in menus, welcome message, etc.)
- Clean up ItemParameterTable implementation (see comment at the top of the class definition)
- Handle MeetUserExtensions properly in 41 and C4 commands on the proxy (rewrite the embedded 19 command and put some metadata in the persistent config, perhaps)
- Make a server patch version of story flag fixer quest
- Make proxy server handle all login commands, including sending 9C when needed
- Fix enemy flag mapping in v2/v3 crossplay and test
- Handle items in crossplay - use the replacement table
- Make proxy server handle all login commands on non-BB, including sending 9C when needed
- Add $switchit command (activates switch flag(s) for nearest object, e.g. laser fence, door, fog collision)
- Add a way to persist flags across connections, at least on v3, because of Meet User + B2 enable quest interactions - maybe update the quest to patch one of the login commands so the server can tell it's enabled
- Handle items in crossplay - use the replacement table
- Handle MeetUserExtensions properly in 41 and C4 commands on the proxy (rewrite the embedded 19 command and put some metadata in the persistent config, perhaps)
- Clean up ItemParameterTable implementation (see comment at the top of the class definition)
- Make UI strings localizable (e.g. entries in menus, welcome message, etc.)
## PSO DC
+87 -33
View File
@@ -24,6 +24,41 @@
using namespace std;
////////////////////////////////////////////////////////////////////////////////
// Tools
string str_for_flag_ranges(const vector<bool>& flags) {
string ret;
auto add_result = [&](size_t start, size_t end) {
if (!ret.empty()) {
ret.push_back(',');
}
if (start == end) {
ret += std::format("{}", start);
} else if (start == end - 1) {
ret += std::format("{},{}", start, end);
} else {
ret += std::format("{}-{}", start, end);
}
};
size_t range_start = 0;
bool in_range = false;
for (size_t z = 0; z < flags.size(); z++) {
if (flags[z] && !in_range) {
in_range = true;
range_start = z;
} else if (!flags[z] && in_range) {
in_range = false;
add_result(range_start, z - 1);
}
}
if (in_range) {
add_result(range_start, flags.size() - 1);
}
return ret;
}
////////////////////////////////////////////////////////////////////////////////
// Checks
@@ -421,7 +456,7 @@ static asio::awaitable<void> server_command_bbchar_savechar(const Args& a, bool
shared_ptr<Account> dest_account;
shared_ptr<BBLicense> dest_bb_license;
ssize_t dest_character_index = 0;
size_t dest_character_index = 0;
if (is_bb_conversion) {
vector<string> tokens = phosg::split(a.text, ' ');
if (tokens.size() != 3) {
@@ -429,9 +464,9 @@ static asio::awaitable<void> server_command_bbchar_savechar(const Args& a, bool
}
// username/password are tokens[0] and [1]
dest_character_index = stoll(tokens[2]) - 1;
if ((dest_character_index > 3) || (dest_character_index < 0)) {
throw precondition_failed("$C6Player index must\nbe in range 1-4");
dest_character_index = stoull(tokens[2]) - 1;
if (dest_character_index >= 127) {
throw precondition_failed("$C6Player index must\nbe in range 1-127");
}
try {
@@ -443,9 +478,9 @@ static asio::awaitable<void> server_command_bbchar_savechar(const Args& a, bool
}
} else {
dest_character_index = stoll(a.text) - 1;
if ((dest_character_index > 15) || (dest_character_index < 0)) {
throw precondition_failed("$C6Player index must\nbe in range 1-16");
dest_character_index = stoull(a.text) - 1;
if (dest_character_index >= s->num_backup_character_slots) {
throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->num_backup_character_slots);
}
dest_account = a.c->login->account;
}
@@ -574,30 +609,49 @@ ChatCommandDefinition cc_checkchar(
throw precondition_failed("$C7This command cannot\nbe used on a shared\naccount");
}
size_t index = stoull(a.text, nullptr, 0) - 1;
if (index > 15) {
throw precondition_failed("$C6Player index must\nbe in range 1-16");
}
auto s = a.c->require_server_state();
try {
if (is_ep3(a.c->version())) {
string filename = a.c->backup_character_filename(a.c->login->account->account_id, index, true);
auto ch = phosg::load_object_file<PSOGCEp3CharacterFile::Character>(filename);
send_text_message_fmt(a.c, "Slot {}: $C6{}$C7\n{} {}\nCLv: on {}.{}, off {}.{}",
index + 1, ch.disp.visual.name.decode(),
name_for_section_id(ch.disp.visual.section_id), name_for_char_class(ch.disp.visual.char_class),
(ch.ep3_config.online_clv_exp / 100) + 1, ch.ep3_config.online_clv_exp % 100,
(ch.ep3_config.offline_clv_exp / 100) + 1, ch.ep3_config.offline_clv_exp % 100);
} else {
string filename = a.c->backup_character_filename(a.c->login->account->account_id, index, false);
auto ch = PSOCHARFile::load_shared(filename, false).character_file;
send_text_message_fmt(a.c, "Slot {}: $C6{}$C7\n{} {}\nLevel {}",
index + 1, ch->disp.name.decode(),
name_for_section_id(ch->disp.visual.section_id), name_for_char_class(ch->disp.visual.char_class),
ch->disp.stats.level + 1);
if (a.text.empty()) {
bool is_ep3 = ::is_ep3(a.c->version());
vector<bool> flags;
flags.emplace_back(false);
for (size_t z = 0; z < s->num_backup_character_slots; z++) {
string filename = a.c->backup_character_filename(a.c->login->account->account_id, z, is_ep3);
flags.emplace_back(std::filesystem::is_regular_file(filename));
}
string used_str = str_for_flag_ranges(flags);
flags.flip();
flags[0] = false;
string free_str = str_for_flag_ranges(flags);
send_text_message_fmt(a.c, "Used: {}\nFree: {}", used_str, free_str);
} else {
size_t index = stoull(a.text, nullptr, 0) - 1;
if (index >= s->num_backup_character_slots) {
throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->num_backup_character_slots);
}
try {
if (is_ep3(a.c->version())) {
string filename = a.c->backup_character_filename(a.c->login->account->account_id, index, true);
auto ch = phosg::load_object_file<PSOGCEp3CharacterFile::Character>(filename);
send_text_message_fmt(a.c, "Slot {}: $C6{}$C7\n{} {}\nCLv: on {}.{}, off {}.{}",
index + 1, ch.disp.visual.name.decode(),
name_for_section_id(ch.disp.visual.section_id), name_for_char_class(ch.disp.visual.char_class),
(ch.ep3_config.online_clv_exp / 100) + 1, ch.ep3_config.online_clv_exp % 100,
(ch.ep3_config.offline_clv_exp / 100) + 1, ch.ep3_config.offline_clv_exp % 100);
} else {
string filename = a.c->backup_character_filename(a.c->login->account->account_id, index, false);
auto ch = PSOCHARFile::load_shared(filename, false).character_file;
send_text_message_fmt(a.c, "Slot {}: $C6{}$C7\n{} {}\nLevel {}",
index + 1, ch->disp.name.decode(),
name_for_section_id(ch->disp.visual.section_id), name_for_char_class(ch->disp.visual.char_class),
ch->disp.stats.level + 1);
}
} catch (const phosg::cannot_open_file&) {
send_text_message_fmt(a.c, "No character in\nslot {}", index + 1);
}
} catch (const phosg::cannot_open_file&) {
send_text_message_fmt(a.c, "No character in\nslot {}", index + 1);
}
co_return;
@@ -1230,7 +1284,7 @@ ChatCommandDefinition cc_item(
} else {
auto l = a.c->require_lobby();
ItemData item = s->parse_item_description(a.c->version(), a.text);
item = s->parse_item_description(a.c->version(), a.text);
item.id = l->generate_item_id(a.c->lobby_client_id);
if ((l->drop_mode == Lobby::DropMode::SERVER_PRIVATE) || (l->drop_mode == Lobby::DropMode::SERVER_DUPLICATE)) {
@@ -1508,11 +1562,12 @@ ChatCommandDefinition cc_loadchar(
throw precondition_failed("$C7This command cannot\nbe used on a shared\naccount");
}
auto s = a.c->require_server_state();
auto l = a.c->require_lobby();
size_t index = stoull(a.text, nullptr, 0) - 1;
if (index > 15) {
throw precondition_failed("$C6Player index must\nbe in range 1-16");
if (index >= s->num_backup_character_slots) {
throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->num_backup_character_slots);
}
shared_ptr<PSOGCEp3CharacterFile::Character> ep3_char;
@@ -1524,7 +1579,6 @@ ChatCommandDefinition cc_loadchar(
if (a.c->version() == Version::BB_V4) {
// On BB, it suffices to simply send the character file again
auto s = a.c->require_server_state();
send_complete_player_bb(a.c);
send_player_leave_notification(l, a.c->lobby_client_id);
s->send_lobby_join_notifications(l, a.c);
+13 -9
View File
@@ -277,9 +277,13 @@ void Client::reschedule_ping_and_timeout_timers() {
this->send_ping_timer.async_wait([this](std::error_code ec) {
if (!ec) {
this->log.info_f("Sending ping command");
// The game doesn't use this timestamp; we only use it for debugging purposes
be_uint64_t timestamp = phosg::now();
this->channel->send(0x1D, 0x00, &timestamp, sizeof(be_uint64_t));
try {
// The game doesn't use this timestamp; we only use it for debugging purposes
be_uint64_t timestamp = phosg::now();
this->channel->send(0x1D, 0x00, &timestamp, sizeof(be_uint64_t));
} catch (const exception& e) {
this->log.warning_f("Failed to send ping: {}", e.what());
}
}
});
}
@@ -1049,27 +1053,27 @@ void Client::use_character_bank(ssize_t index) {
void Client::print_inventory() const {
auto s = this->require_server_state();
auto p = this->character();
this->log.info_f("[PlayerInventory] Meseta: {}\n", p->disp.stats.meseta);
this->log.info_f("[PlayerInventory] {} items\n", p->inventory.num_items);
this->log.info_f("[PlayerInventory] Meseta: {}", p->disp.stats.meseta);
this->log.info_f("[PlayerInventory] {} items", p->inventory.num_items);
for (size_t x = 0; x < p->inventory.num_items; x++) {
const auto& item = p->inventory.items[x];
auto hex = item.data.hex();
auto name = s->describe_item(this->version(), item.data, false);
this->log.info_f("[PlayerInventory] {:2}: [+{:08X}] {} ({})\n", x, item.flags, hex, name);
this->log.info_f("[PlayerInventory] {:2}: [+{:08X}] {} ({})", x, item.flags, hex, name);
}
}
void Client::print_bank() const {
auto s = this->require_server_state();
auto bank = this->current_bank();
this->log.info_f("[PlayerBank] Meseta: {}\n", bank.meseta);
this->log.info_f("[PlayerBank] {} items\n", bank.num_items);
this->log.info_f("[PlayerBank] Meseta: {}", bank.meseta);
this->log.info_f("[PlayerBank] {} items", bank.num_items);
for (size_t x = 0; x < bank.num_items; x++) {
const auto& item = bank.items[x];
const char* present_token = item.present ? "" : " (missing present flag)";
auto hex = item.data.hex();
auto name = s->describe_item(this->version(), item.data, false);
this->log.info_f("[PlayerBank] {:3}: {} ({}) (x{}){}\n", x, hex, name, item.amount, present_token);
this->log.info_f("[PlayerBank] {:3}: {} ({}) (x{}){}", x, hex, name, item.amount, present_token);
}
}
+1 -3
View File
@@ -4982,9 +4982,7 @@ struct G_6x70_Base_DCNTE {
/* 0002 */ le_uint16_t room_id = 0;
/* 0004 */ le_uint32_t flags1 = 0;
/* 0008 */ VectorXYZF pos;
/* 0014 */ le_uint32_t angle_x = 0;
/* 0018 */ le_uint32_t angle_y = 0;
/* 001C */ le_uint32_t angle_z = 0;
/* 0014 */ VectorXYZI angle;
/* 0020 */ le_uint16_t unknown_a3a = 0;
/* 0022 */ le_uint16_t current_hp = 0;
} __packed_ws__(G_6x70_Base_DCNTE, 0x24);
+1 -1
View File
@@ -798,7 +798,7 @@ asio::awaitable<std::unique_ptr<HTTPResponse>> HTTPServer::handle_request(shared
} else if (req.path == "/y/data/rare-tables") {
this->require_GET(req);
ret = this->generate_rare_table_list_json();
} else if (!req.path.starts_with("/y/data/rare-tables/")) {
} else if (req.path.starts_with("/y/data/rare-tables/")) {
this->require_GET(req);
ret = co_await this->generate_rare_table_json(req.path.substr(20));
} else if (req.path == "/y/data/quests") {
+17
View File
@@ -28,6 +28,23 @@ static void set_log_level_from_json(
}
}
void set_all_log_levels(phosg::LogLevel level) {
channel_exceptions_log.min_level = level;
client_log.min_level = level;
command_data_log.min_level = level;
config_log.min_level = level;
dns_server_log.min_level = level;
function_compiler_log.min_level = level;
ip_stack_simulator_log.min_level = level;
lobby_log.min_level = level;
patch_index_log.min_level = level;
player_data_log.min_level = level;
proxy_server_log.min_level = level;
replay_log.min_level = level;
server_log.min_level = level;
static_game_data_log.min_level = level;
}
void set_log_levels_from_json(const phosg::JSON& json) {
set_log_level_from_json(channel_exceptions_log, json, "ChannelExceptions");
set_log_level_from_json(client_log, json, "Clients");
+1
View File
@@ -18,4 +18,5 @@ extern phosg::PrefixedLogger replay_log;
extern phosg::PrefixedLogger server_log;
extern phosg::PrefixedLogger static_game_data_log;
void set_all_log_levels(phosg::LogLevel level);
void set_log_levels_from_json(const phosg::JSON& json);
+16
View File
@@ -663,6 +663,19 @@ Action a_generate_pc_v2_registry(
write_output_data(args, output_data.data(), output_data.size(), "reg");
});
Action a_encrypt_challenge_time(
"encrypt-challenge-time", nullptr, +[](phosg::Arguments& args) {
uint16_t time = args.get<uint16_t>(1);
uint32_t ret = encrypt_challenge_time(time);
phosg::fwrite_fmt(stderr, "{} => {:08X}\n", phosg::format_duration(time * 1000000), ret);
});
Action a_decrypt_challenge_time(
"decrypt-challenge-time", nullptr, +[](phosg::Arguments& args) {
uint32_t time = args.get<uint32_t>(1, phosg::Arguments::IntFormat::HEX);
uint16_t ret = decrypt_challenge_time(time);
phosg::fwrite_fmt(stderr, "{:08X} => {}\n", time, phosg::format_duration(ret * 1000000));
});
Action a_encrypt_challenge_data(
"encrypt-challenge-data", nullptr, +[](phosg::Arguments& args) {
string data = read_input_data(args);
@@ -3182,6 +3195,9 @@ Action a_run_server_replay_log(
}
auto state = make_shared<ServerState>(get_config_filename(args));
if (args.get<bool>("debug")) {
state->is_debug = true;
}
state->load_all(true);
if (state->dns_server_port) {
+7 -62
View File
@@ -5848,55 +5848,12 @@ uint32_t MapState::RareEnemyRates::for_enemy_type(EnemyType type) const {
}
}
const shared_ptr<const MapState::RareEnemyRates> MapState::NO_RARE_ENEMIES = make_shared<MapState::RareEnemyRates>(
0, 0, 0);
const shared_ptr<const MapState::RareEnemyRates> MapState::NO_RARE_ENEMIES = make_shared<MapState::RareEnemyRates>(0, 0, 0);
const shared_ptr<const MapState::RareEnemyRates> MapState::DEFAULT_RARE_ENEMIES = make_shared<MapState::RareEnemyRates>(
MapState::RareEnemyRates::DEFAULT_RARE_ENEMY_RATE_V3,
MapState::RareEnemyRates::DEFAULT_MERICARAND_RATE_V3,
MapState::RareEnemyRates::DEFAULT_RARE_BOSS_RATE_V4);
uint32_t MapState::EnemyState::convert_game_flags(uint32_t game_flags, bool to_v3) {
// The format of game_flags was changed significantly between v2 and v3, and
// not accounting for this results in odd effects like other characters not
// appearing when joining a game. Unfortunately, some bits were deleted on v3
// and other bits were added, so it doesn't suffice to simply store the most
// complete format of this field - we have to be able to convert between the
// two.
// Bits on v2: ?IHCBAzy xwvutsrq ponmlkji hgfedcba
// Bits on v3: ?IHGFEDC BAzyxwvu srqponkj hgfedcba
// The bits ilmt were removed in v3 and the bits to their left were shifted
// right. The bits DEFG were added in v3 and do not exist on v2.
// Known meanings for these bits:
// o = is dead
// n = should play hit animation
// y = is near enemy
// H = is enemy?
// I = is object? (some entities have both H and I set though)
// TODO: The above might all be wrong.
// GC 00100000 10010000 00001110 00000000
// PC 00101001 00000000 01100100 00000000
// PC 00101001 10110000 00101110 00000000
// GC 00100000 10011011 00000111 00000000
// PC 00101001 10010000 00101110 00000000
// GC 00100000 10011001 00000111 00000000
if (to_v3) {
return (game_flags & 0xE00000FF) |
((game_flags & 0x00000600) >> 1) |
((game_flags & 0x0007E000) >> 3) |
((game_flags & 0x1FF00000) >> 4);
} else {
return (game_flags & 0xE00000FF) |
((game_flags << 1) & 0x00000600) |
((game_flags << 3) & 0x0007E000) |
((game_flags << 4) & 0x1FF00000);
}
}
MapState::EntityIterator::EntityIterator(MapState* map_state, Version version, bool at_end)
: map_state(map_state),
version(version),
@@ -6413,7 +6370,6 @@ void MapState::import_object_states_from_sync(
void MapState::import_enemy_states_from_sync(Version from_version, const SyncEnemyStateEntry* entries, size_t entry_count) {
this->log.info_f("Importing enemy state from sync command");
size_t enemy_index = 0;
bool is_v3 = !is_v1_or_v2(from_version);
for (const auto& fc : this->floor_config_entries) {
if (!fc.super_map) {
continue;
@@ -6434,15 +6390,10 @@ void MapState::import_enemy_states_from_sync(Version from_version, const SyncEne
if (ene_st->super_ene != ene) {
throw logic_error("super enemy link is incorrect");
}
if (ene_st->get_game_flags(is_v3) != entry.flags) {
this->log.warning_f("({:04X} => E-{:03X}) Flags from client ({:08X}({})) do not match game flags from map ({:08X}({}))",
enemy_index,
ene_st->e_id,
entry.flags,
is_v3 ? "v3" : "v2",
ene_st->game_flags,
(ene_st->server_flags & MapState::EnemyState::Flag::GAME_FLAGS_IS_V3) ? "v3" : "v2");
ene_st->set_game_flags(entry.flags, !is_v1_or_v2(from_version));
if (ene_st->game_flags != entry.flags) {
this->log.warning_f("({:04X} => E-{:03X}) Flags from client ({:08X}) do not match game flags from map ({:08X})",
enemy_index, ene_st->e_id, entry.flags, ene_st->game_flags);
ene_st->game_flags = entry.flags;
}
if (ene_st->total_damage != entry.total_damage) {
this->log.warning_f("({:04X} => E-{:03X}) Total damage from client ({}) does not match total damage from map ({})",
@@ -6771,14 +6722,8 @@ void MapState::print(FILE* stream) const {
}
}
string ene_str = ene_st->super_ene->str();
phosg::fwrite_fmt(stream, " {} total_damage={:04X} rare_flags={:04X} game_flags={:08X}({}) set_flags={:04X} server_flags={:04X}\n",
ene_str,
ene_st->total_damage,
ene_st->rare_flags,
ene_st->game_flags,
(ene_st->server_flags & MapState::EnemyState::Flag::GAME_FLAGS_IS_V3) ? "v3" : "v2",
ene_st->set_flags,
ene_st->server_flags);
phosg::fwrite_fmt(stream, " {} total_damage={:04X} rare_flags={:04X} game_flags={:08X} set_flags={:04X} server_flags={:04X}\n",
ene_str, ene_st->total_damage, ene_st->rare_flags, ene_st->game_flags, ene_st->set_flags, ene_st->server_flags);
}
if (this->bb_rare_enemy_indexes.empty()) {
-20
View File
@@ -716,7 +716,6 @@ public:
ITEM_DROPPED = 0x0008,
ALL_HITS_MASK_FIRST = 0x0010,
ALL_HITS_MASK = 0x00F0,
GAME_FLAGS_IS_V3 = 0x0100,
};
size_t e_id = 0;
size_t set_id = 0;
@@ -727,8 +726,6 @@ public:
uint16_t set_flags = 0; // Only used if super_ene->child_index == 0
uint16_t server_flags = 0;
static uint32_t convert_game_flags(uint32_t game_flags, bool to_v3);
inline void reset() {
this->total_damage = 0;
this->rare_flags = 0;
@@ -737,23 +734,6 @@ public:
this->server_flags = 0;
}
inline void set_game_flags(uint32_t game_flags, bool is_v3) {
this->game_flags = game_flags;
if (is_v3) {
this->server_flags |= Flag::GAME_FLAGS_IS_V3;
} else {
this->server_flags &= ~Flag::GAME_FLAGS_IS_V3;
}
}
inline uint32_t get_game_flags(bool is_v3) const {
bool flags_is_v3 = (this->server_flags & Flag::GAME_FLAGS_IS_V3);
if (flags_is_v3 == is_v3) {
return this->game_flags;
} else {
return this->convert_game_flags(this->game_flags, is_v3);
}
}
inline bool is_rare(Version version) const {
return (((this->rare_flags >> static_cast<size_t>(version)) & 1) ||
((version == Version::BB_V4) ? this->super_ene->is_default_rare_bb : this->super_ene->is_default_rare_v123));
+21 -20
View File
@@ -706,27 +706,28 @@ void PSOBBMultiKeyDetectorEncryption::encrypt(void* data, size_t size) {
}
void PSOBBMultiKeyDetectorEncryption::decrypt(void* data, size_t size) {
if (!this->active_crypt.get()) {
if (size != 8) {
throw logic_error("initial decryption size does not match expected first data size");
}
for (const auto& key : this->possible_keys) {
this->active_key = key;
this->active_crypt = make_shared<PSOBBEncryption>(*this->active_key, this->seed.data(), this->seed.size());
string test_data(reinterpret_cast<const char*>(data), size);
this->active_crypt->decrypt(test_data.data(), test_data.size());
if (this->expected_first_data.count(test_data)) {
break;
}
this->active_key.reset();
this->active_crypt.reset();
}
if (!this->active_crypt.get()) {
throw runtime_error("none of the registered private keys are valid for this client");
}
if (this->active_crypt.get()) {
this->active_crypt->decrypt(data, size);
return;
}
this->active_crypt->decrypt(data, size);
if (size != 8) {
throw logic_error("initial decryption size does not match expected first data size");
}
for (const auto& key : this->possible_keys) {
this->active_key = key;
this->active_crypt = make_shared<PSOBBEncryption>(*this->active_key, this->seed.data(), this->seed.size());
string test_data(reinterpret_cast<const char*>(data), size);
this->active_crypt->decrypt(test_data.data(), test_data.size());
if (this->expected_first_data.count(test_data)) {
memcpy(data, test_data.data(), size);
return;
}
this->active_key.reset();
this->active_crypt.reset();
}
throw runtime_error("none of the registered private keys are valid for this client");
}
PSOEncryption::Type PSOBBMultiKeyDetectorEncryption::type() const {
+29 -21
View File
@@ -356,11 +356,11 @@ static asio::awaitable<HandlerResult> S_B_03(shared_ptr<Client> c, Channel::Mess
resp.character_slot = c->bb_character_index;
resp.connection_phase = c->bb_connection_phase;
resp.client_code = c->bb_client_code;
resp.security_token = c->proxy_session->remote_bb_security_token;
resp.security_token = c->bb_security_token;
resp.username.encode(c->username, c->language());
resp.password.encode(c->password, c->language());
resp.hardware_id = c->hardware_id;
resp.client_config = c->proxy_session->remote_client_config_data;
resp.client_config = c->bb_client_config;
if (c->proxy_session->enable_remote_ip_crc_patch) {
*reinterpret_cast<le_uint32_t*>(resp.client_config.data() + 0x10) =
c->proxy_session->remote_ip_crc ^ (1309539928UL + 1248334810UL);
@@ -373,27 +373,17 @@ static asio::awaitable<HandlerResult> S_B_03(shared_ptr<Client> c, Channel::Mess
static asio::awaitable<HandlerResult> S_B_E6(shared_ptr<Client> c, Channel::Message& msg) {
const auto& cmd = msg.check_size_t<S_ClientInit_BB_00E6>(0xFFFF);
c->proxy_session->remote_guild_card_number = cmd.guild_card_number;
c->proxy_session->remote_bb_security_token = cmd.security_token;
c->proxy_session->remote_client_config_data = cmd.client_config;
c->bb_security_token = cmd.security_token;
c->bb_client_config = cmd.client_config;
auto s = c->require_server_state();
auto& pc = s->proxy_persistent_configs[c->login->account->account_id];
pc.account_id = c->login->account->account_id;
pc.remote_guild_card_number = c->proxy_session->remote_guild_card_number;
pc.remote_bb_security_token = c->proxy_session->remote_bb_security_token;
pc.remote_client_config_data = c->proxy_session->remote_client_config_data;
pc.enable_remote_ip_crc_patch = c->proxy_session->enable_remote_ip_crc_patch;
c->log.info_f("Updated persistent config for proxy session");
if ((c->bb_connection_phase == 0) && c->proxy_session->received_reconnect) {
c->proxy_session->server_channel->send(0x00E0); // Request system file
}
co_return HandlerResult::SUPPRESS;
}
static asio::awaitable<HandlerResult> C_B_E0(shared_ptr<Client>, Channel::Message&) {
co_return HandlerResult::SUPPRESS;
co_return HandlerResult::FORWARD;
}
static asio::awaitable<HandlerResult> S_V123_04(shared_ptr<Client> c, Channel::Message& msg) {
@@ -748,18 +738,18 @@ static asio::awaitable<HandlerResult> S_G_E4(shared_ptr<Client> c, Channel::Mess
static asio::awaitable<HandlerResult> S_B_22(shared_ptr<Client> c, Channel::Message& msg) {
// We use this command (which is sent before the init encryption command) to
// detect a particular server behavior that we'll have to work around later.
// It looks like this command's existence is another anti-proxy measure, since
// It looks like this command's existence is an anti-proxy measure, since
// this command is 0x34 bytes in total, and the logic that adds padding bytes
// when the command size isn't a multiple of 8 is only active when encryption
// is enabled. Presumably some simpler proxies would get this wrong.
// Editor's note: There's an unsavory message in this command's data field,
// hence the hash here instead of a direct string comparison. I'd love to hear
// the story behind why they put that string there.
// hence the hash here instead of a direct string comparison. I'd love to
// hear the story behind why they put that string there.
if ((msg.data.size() == 0x2C) && (phosg::fnv1a64(msg.data.data(), msg.data.size()) == 0x8AF8314316A27994)) {
c->log.info_f("Enabling remote IP CRC patch");
c->proxy_session->enable_remote_ip_crc_patch = true;
}
co_return HandlerResult::FORWARD;
co_return HandlerResult::SUPPRESS;
}
static asio::awaitable<HandlerResult> S_19_U_14(shared_ptr<Client> c, Channel::Message& msg) {
@@ -1965,6 +1955,20 @@ asio::awaitable<HandlerResult> C_6x(shared_ptr<Client> c, Channel::Message& msg)
}
break;
case 0x4E: {
if (c->check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
if (is_v1_or_v2(c->version()) && (c->version() != Version::GC_NTE)) {
G_UseMedicalCenter_6x31 cmd = {0x31, 0x01, c->lobby_client_id};
send_command_t(c->channel, 0x60, 0x00, cmd);
send_command_t(c->proxy_session->server_channel, 0x60, 0x00, cmd);
} else {
G_RevivePlayer_V3_BB_6xA1 cmd = {0xA1, 0x01, c->lobby_client_id};
co_await send_protected_command(c, &cmd, sizeof(cmd), true);
}
}
break;
}
case 0x5F:
send_item_notification_if_needed(
c, msg.check_size_t<G_DropItem_DC_6x5F>(sizeof(G_DropItem_PC_V3_BB_6x5F)).item.item, true);
@@ -2249,7 +2253,7 @@ static on_message_t handlers[0x100][NUM_VERSIONS][2] = {
/* DE */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}},
/* DF */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_12_2000 C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
/* E0 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {nullptr, C_B_E0}},
/* E0 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}},
/* E1 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}},
/* E2 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_B_E2, nullptr}},
/* E3 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}},
@@ -2317,7 +2321,8 @@ asio::awaitable<void> on_proxy_command(shared_ptr<Client> c, bool from_server, u
}
}
asio::awaitable<void> handle_proxy_server_commands(shared_ptr<Client> c, shared_ptr<ProxySession> ses, shared_ptr<Channel> channel) {
asio::awaitable<void> handle_proxy_server_commands(
shared_ptr<Client> c, shared_ptr<ProxySession> ses, shared_ptr<Channel> channel) {
std::string error_str;
// server_channel can be changed by receiving a 19 command, hence the
// exception handler is inside the loop here
@@ -2326,6 +2331,9 @@ asio::awaitable<void> handle_proxy_server_commands(shared_ptr<Client> c, shared_
try {
msg = make_unique<Channel::Message>(co_await channel->recv());
if (c->proxy_session == ses) {
for (size_t z = 0; z < std::min<size_t>(c->proxy_session->prev_server_command_bytes.size(), msg->data.size()); z++) {
c->proxy_session->prev_server_command_bytes[z] = msg->data[z];
}
asio::co_spawn(co_await asio::this_coro::executor, on_proxy_command(c, true, std::move(msg)), asio::detached);
}
} catch (const std::system_error& e) {
-2
View File
@@ -10,8 +10,6 @@ ProxySession::ProxySession(shared_ptr<Channel> server_channel, const PersistentC
: server_channel(server_channel) {
if (pc) {
this->remote_guild_card_number = pc->remote_guild_card_number;
this->remote_bb_security_token = pc->remote_bb_security_token;
this->remote_client_config_data = pc->remote_client_config_data;
this->enable_remote_ip_crc_patch = pc->enable_remote_ip_crc_patch;
} else if (is_v4(this->server_channel->version)) {
this->remote_guild_card_number = 0;
-3
View File
@@ -44,7 +44,6 @@ struct ProxySession {
uint64_t server_ping_start_time = 0;
int64_t remote_guild_card_number = -1;
uint32_t remote_bb_security_token = 0;
parray<uint8_t, 0x28> remote_client_config_data;
enum class DropMode {
@@ -64,8 +63,6 @@ struct ProxySession {
struct PersistentConfig {
uint32_t account_id;
uint32_t remote_guild_card_number;
uint32_t remote_bb_security_token;
parray<uint8_t, 0x28> remote_client_config_data;
bool enable_remote_ip_crc_patch;
std::unique_ptr<asio::steady_timer> expire_timer;
};
+36 -30
View File
@@ -43,18 +43,14 @@ asio::awaitable<void> on_connect(std::shared_ptr<Client> c) {
uint16_t pc_port = s->name_to_port_config.at("pc")->port;
uint16_t console_port = s->name_to_port_config.at("gc-us3")->port;
send_pc_console_split_reconnect(c, s->connect_address_for_client(c), pc_port, console_port);
// TODO: There appears to be a bug that occurs rarely when an IPSSClient
// TODO: There appears to be a bug that occurs rarely when a client
// connects to this port; sometimes it disconnects before receiving the
// data it needs. My hypothesis is that there's either a bug in
// IPSSClient where the data isn't being sent before the RST, or there's
// a bug in AVE-TCP where it doesn't forward the last data to the app if
// the RST is received on the same frame as the last PSH. In either case,
// waiting a short amount of time here should mitigate it. This doesn't
// seem to happen at all with SocketChannel, so we only do it for
// IPSSChannel.
if (dynamic_pointer_cast<IPSSChannel>(c->channel)) {
co_await async_sleep(std::chrono::seconds(1));
}
// data it needs. My hypothesis is that there's either a bug in Channel
// where the data isn't being sent before the RST, or there's a bug in
// AVE-TCP where it doesn't forward the last data to the app if the RST
// is received on the same frame as the last PSH. In either case, waiting
// a short amount of time here should mitigate it.
co_await async_sleep(std::chrono::seconds(2));
c->channel->disconnect();
break;
}
@@ -454,7 +450,7 @@ asio::awaitable<void> start_proxy_session(shared_ptr<Client> c, const string& ho
send_proxy_destinations_menu(c);
}
} else {
// Get persistent config if client is BB
// Get persistent config if available
ProxySession::PersistentConfig* pc = nullptr;
if (use_persistent_config) {
try {
@@ -477,8 +473,8 @@ asio::awaitable<void> start_proxy_session(shared_ptr<Client> c, const string& ho
std::format("C-{} proxy remote server at {}", c->id, netloc_str),
phosg::TerminalFormat::FG_YELLOW,
phosg::TerminalFormat::FG_RED);
c->proxy_session = make_shared<ProxySession>(channel, pc);
c->log.info_f("Server channel connected");
asio::co_spawn(*s->io_context, handle_proxy_server_commands(c, c->proxy_session, channel), asio::detached);
}
@@ -507,15 +503,6 @@ asio::awaitable<void> end_proxy_session(shared_ptr<Client> c, const std::string&
} catch (const out_of_range&) {
}
bool is_in_game = c->proxy_session->is_in_game;
c->proxy_session->server_channel->disconnect();
c->proxy_session.reset();
if (is_v4(c->version())) {
c->channel->disconnect();
co_return;
}
// Delete all the other players
for (size_t x = 0; x < c->proxy_session->lobby_players.size(); x++) {
if (c->proxy_session->lobby_players[x].guild_card_number == 0) {
@@ -527,6 +514,15 @@ asio::awaitable<void> end_proxy_session(shared_ptr<Client> c, const std::string&
c->channel->send(c->proxy_session->is_in_game ? 0x66 : 0x69, leaving_id, &cmd, sizeof(cmd));
}
bool is_in_game = c->proxy_session->is_in_game;
c->proxy_session->server_channel->disconnect();
c->proxy_session.reset();
if (is_v4(c->version())) {
c->channel->disconnect();
co_return;
}
if (is_in_game) {
string msg = std::format("You cannot return\nto $C6{}$C7\nwhile in a game.\n\n{}",
s->name, error_message);
@@ -1480,24 +1476,34 @@ static asio::awaitable<void> on_93_BB(shared_ptr<Client> c, Channel::Message& ms
c->preferred_lobby_id = base_cmd.preferred_lobby_id;
}
send_client_init_bb(c, 0);
if (!c->bb_client_config.is_filled_with(0xFF)) {
if (base_cmd.guild_card_number == 0) {
// There is a (bug? feature?) in the BB client such that it has to receive
// a reconnect command during the data server phase, or else it won't know
// where to connect to during character selection. It's not clear why they
// didn't just make it use the initial connection address by default...
send_client_init_bb(c, 0);
send_reconnect(c, s->connect_address_for_client(c), s->name_to_port_config.at("bb-data1")->port);
co_return;
} else if (s->proxy_destination_bb.has_value()) {
// Start a proxy session if there's a destination configured Ignore the
// persistent config if this is the first data server connection, to
// prevent quick reconnects from incorrectly reusing the old session's
// state.
// Start a proxy session immediately if there's a destination set. Two
// things to watch out for:
// - Ignore the persistent config if this is the first data server
// connection, to prevent quick reconnects from incorrectly reusing the
// old session's state.
// - We don't send 00E6 (send_client_init_bb) in this case. This is because
// the login command is resent to the remote server, and we forward its
// response back to the client directly.
const auto& [host, port] = *s->proxy_destination_bb;
co_await start_proxy_session(c, host, port, c->bb_connection_phase != 0);
c->proxy_session->remote_client_config_data = c->bb_client_config;
co_return;
} else if (c->bb_connection_phase >= 0x04) {
} else {
send_client_init_bb(c, 0);
}
if (c->bb_connection_phase >= 0x04) {
// This means the client is done with the data server phase and is in the
// game server phase; we should send the ship select menu or a lobby join
// command.
+41 -21
View File
@@ -1154,10 +1154,42 @@ G_6x70_Base_V1 Parsed6x70Data::base_v1(bool is_v3) const {
return ret;
}
uint32_t Parsed6x70Data::convert_game_flags(uint32_t game_flags, bool to_v3) {
// The format of game_flags for players was changed significantly between v2
// and v3, and not accounting for this results in odd effects like other
// characters not appearing when joining a game. Unfortunately, some bits
// were deleted on v3 and other bits were added, so it doesn't suffice to
// simply store the most complete format of this field - we have to be able
// to convert between the two.
// Bits on v2: ?IHCBAzy xwvutsrq ponmlkji hgfedcba
// Bits on v3: ?IHGFEDC BAzyxwvu srqponkj hgfedcba
// The bits ilmt were removed in v3 and the bits to their left were shifted
// right. The bits DEFG were added in v3 and do not exist on v2.
// Known meanings for these bits:
// o = is dead
// n = should play hit animation
// y = is near enemy
// H = is enemy?
// I = is object? (some entities have both H and I set though)
if (to_v3) {
return (game_flags & 0xE00000FF) |
((game_flags & 0x00000600) >> 1) |
((game_flags & 0x0007E000) >> 3) |
((game_flags & 0x1FF00000) >> 4);
} else {
return (game_flags & 0xE00000FF) |
((game_flags << 1) & 0x00000600) |
((game_flags << 3) & 0x0007E000) |
((game_flags << 4) & 0x1FF00000);
}
}
uint32_t Parsed6x70Data::get_game_flags(bool is_v3) const {
return (this->game_flags_is_v3 == is_v3)
? this->game_flags
: MapState::EnemyState::convert_game_flags(this->game_flags, is_v3);
: Parsed6x70Data::convert_game_flags(this->game_flags, is_v3);
}
static asio::awaitable<void> on_sync_joining_player_disp_and_inventory(
@@ -3423,36 +3455,29 @@ static asio::awaitable<void> on_update_enemy_state(shared_ptr<Client> c, Subcomm
if ((cmd.enemy_index & 0xF000) || (cmd.header.entity_id != (cmd.enemy_index | 0x1000))) {
throw runtime_error("mismatched enemy id/index");
}
bool is_v3 = !is_v1_or_v2(c->version());
auto ene_st = l->map_state->enemy_state_for_index(c->version(), c->floor, cmd.enemy_index);
uint32_t src_flags = is_big_endian(c->version()) ? bswap32(cmd.game_flags) : cmd.game_flags.load();
if (l->difficulty == 3) {
src_flags = (src_flags & 0xFFFFFFC0) | (ene_st->get_game_flags(is_v3) & 0x0000003F);
src_flags = (src_flags & 0xFFFFFFC0) | (ene_st->game_flags & 0x0000003F);
}
ene_st->set_game_flags(src_flags, is_v3);
ene_st->game_flags = src_flags;
ene_st->total_damage = cmd.total_damage;
ene_st->set_last_hit_by_client_id(c->lobby_client_id);
l->log.info_f("E-{:03X} updated to damage={} game_flags={:08X} ({})",
ene_st->e_id,
ene_st->total_damage,
ene_st->game_flags,
(ene_st->server_flags & MapState::EnemyState::Flag::GAME_FLAGS_IS_V3) ? "v3" : "v2");
l->log.info_f("E-{:03X} updated to damage={} game_flags={:08X}", ene_st->e_id, ene_st->total_damage, ene_st->game_flags);
for (auto lc : l->clients) {
if (lc && (lc != c)) {
cmd.enemy_index = l->map_state->index_for_enemy_state(lc->version(), ene_st);
if (cmd.enemy_index != 0xFFFF) {
cmd.header.entity_id = 0x1000 | cmd.enemy_index;
uint32_t game_flags = ene_st->get_game_flags(!is_v1_or_v2(lc->version()));
cmd.game_flags = is_big_endian(lc->version()) ? phosg::bswap32(game_flags) : game_flags;
cmd.game_flags = is_big_endian(lc->version()) ? phosg::bswap32(ene_st->game_flags) : ene_st->game_flags;
send_command_t(lc, 0x60, 0x00, cmd);
}
}
}
}
static asio::awaitable<void> on_set_enemy_low_game_flags_ultimate(
shared_ptr<Client> c, SubcommandMessage& msg) {
static asio::awaitable<void> on_set_enemy_low_game_flags_ultimate(shared_ptr<Client> c, SubcommandMessage& msg) {
auto& cmd = msg.check_size_t<G_SetEnemyLowGameFlagsUltimate_6x9C>();
if (command_is_private(msg.command) ||
@@ -3467,15 +3492,10 @@ static asio::awaitable<void> on_set_enemy_low_game_flags_ultimate(
co_return;
}
bool is_v3 = !is_v1_or_v2(c->version());
auto ene_st = l->map_state->enemy_state_for_index(c->version(), c->floor, cmd.header.entity_id - 0x1000);
uint32_t game_flags = ene_st->get_game_flags(is_v3);
if (!(game_flags & cmd.low_game_flags)) {
ene_st->set_game_flags(game_flags | cmd.low_game_flags, is_v3);
l->log.info_f("E-{:03X} updated to game_flags={:08X} ({})",
ene_st->e_id,
ene_st->game_flags,
(ene_st->server_flags & MapState::EnemyState::Flag::GAME_FLAGS_IS_V3) ? "v3" : "v2");
if (!(ene_st->game_flags & cmd.low_game_flags)) {
ene_st->game_flags |= cmd.low_game_flags;
l->log.info_f("E-{:03X} updated to game_flags={:08X}", ene_st->e_id, ene_st->game_flags);
}
co_await forward_subcommand_with_entity_id_transcode_t<G_SetEnemyLowGameFlagsUltimate_6x9C>(c, msg);
+1
View File
@@ -121,6 +121,7 @@ protected:
Version from_version,
bool from_client_customization);
G_6x70_Base_V1 base_v1(bool is_v3) const;
static uint32_t convert_game_flags(uint32_t game_flags, bool to_v3);
uint32_t get_game_flags(bool is_v3) const;
};
+2 -5
View File
@@ -237,8 +237,6 @@ void send_server_init_bb(shared_ptr<Client> c, uint8_t flags) {
auto cmd = prepare_server_init_contents_bb(server_key, client_key, flags);
send_command_t(c, use_secondary_message ? 0x9B : 0x03, 0x00, cmd);
static const string primary_expected_first_data("\xB4\x00\x93\x00\x00\x00\x00\x00", 8);
static const string secondary_expected_first_data("\xDC\x00\xDB\x00\x00\x00\x00\x00", 8);
c->bb_detector_crypt = make_shared<PSOBBMultiKeyDetectorEncryption>(
c->require_server_state()->bb_private_keys,
bb_crypt_initial_client_commands,
@@ -647,7 +645,7 @@ void send_client_init_bb(shared_ptr<Client> c, uint32_t error_code) {
cmd.player_tag = 0x00010000;
cmd.guild_card_number = c->login->account->account_id;
cmd.security_token = team ? team->team_id : 0;
cmd.client_config.clear(0xFF);
cmd.client_config = c->bb_client_config;
cmd.can_create_team = 1;
cmd.episode_4_unlocked = 1;
@@ -2788,10 +2786,9 @@ void send_game_enemy_state(shared_ptr<Client> c) {
auto s = c->require_server_state();
vector<SyncEnemyStateEntry> entries;
bool is_v3 = !is_v1_or_v2(c->version());
for (auto ene_st : l->map_state->iter_enemy_states(c->version())) {
auto& entry = entries.emplace_back();
entry.flags = ene_st->get_game_flags(is_v3);
entry.flags = ene_st->game_flags;
entry.item_drop_id = (ene_st->server_flags & MapState::EnemyState::Flag::ITEM_DROPPED)
? 0xFFFF
: (0xCA0 + l->map_state->index_for_enemy_state(c->version(), ene_st));
+6 -1
View File
@@ -1059,7 +1059,11 @@ void ServerState::load_config_early() {
this->exp_share_multiplier = this->config_json->get_float("BBEXPShareMultiplier", 0.5);
this->server_global_drop_rate_multiplier = this->config_json->get_float("ServerGlobalDropRateMultiplier", 1);
set_log_levels_from_json(this->config_json->get("LogLevels", phosg::JSON::dict()));
if (this->is_debug) {
set_all_log_levels(phosg::LogLevel::L_DEBUG);
} else {
set_log_levels_from_json(this->config_json->get("LogLevels", phosg::JSON::dict()));
}
try {
this->run_shell_behavior = this->config_json->at("RunInteractiveShell").as_bool()
@@ -1095,6 +1099,7 @@ void ServerState::load_config_early() {
}
this->enable_chat_commands = this->config_json->get_bool("EnableChatCommands", true);
this->num_backup_character_slots = this->config_json->get_int("BackupCharacterSlots", 16);
this->version_name_colors.reset();
this->client_customization_name_color = 0;
+2
View File
@@ -109,12 +109,14 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
uint64_t client_ping_interval_usecs = 30000000;
uint64_t client_idle_timeout_usecs = 60000000;
uint64_t patch_client_idle_timeout_usecs = 300000000;
bool is_debug = false;
bool ip_stack_debug = false;
bool allow_unregistered_users = false;
bool allow_pc_nte = false;
bool use_temp_accounts_for_prototypes = true;
std::array<uint16_t, NUM_VERSIONS> compatibility_groups = {};
bool enable_chat_commands = true;
size_t num_backup_character_slots = 16;
std::unique_ptr<std::array<uint32_t, NUM_NON_PATCH_VERSIONS>> version_name_colors;
uint32_t client_customization_name_color = 0x00000000;
uint8_t allowed_drop_modes_v1_v2_normal = 0x1F;
+5 -1
View File
@@ -908,7 +908,11 @@ asio::awaitable<deque<string>> f_sc_ss(ShellCommand::Args& args) {
auto c = args.get_client();
if (args.command[1] == 's') {
co_await on_command_with_header(c, data);
if (c->proxy_session) {
send_command_with_header(c->proxy_session->server_channel, data.data(), data.size());
} else {
co_await on_command_with_header(c, data);
}
} else {
send_command_with_header(c->channel, data.data(), data.size());
}
@@ -119,7 +119,7 @@ fix_scroll_patch1_end:
apply_fix_scroll_patch2:
# This patch changes the TAdSinglePlyChrSelectGC::selected_index_within_view
# to be the selected character's absolute index (including scroll_offset),
# not the index only within to the displayed four characters
# not the index only within the displayed four characters
push 6 # Call size
push 0x00413CD8 # Call address
call get_code_size_for_fix_scroll_patch2
@@ -166,7 +166,7 @@ selection_index_fix2_end:
apply_preview_window_fix:
# This patch fixes the preview display so it will show the correct section
# ID, etc.
# ID, level, etc.
push 5 # Call size
push 0x0040216C # Call address
call get_code_size_for_preview_window_fix
@@ -9,7 +9,7 @@ start:
.data 0x80134FE0
.data 0x00000004
.data 0x38600000
li r3, 0
.data 0x00000000
.data 0x00000000
@@ -9,7 +9,7 @@ start:
.data 0x80134FE0
.data 0x00000004
.data 0x38600000
li r3, 0
.data 0x00000000
.data 0x00000000
@@ -9,7 +9,7 @@ start:
.data 0x80135050
.data 0x00000004
.data 0x38600000
li r3, 0
.data 0x00000000
.data 0x00000000
@@ -9,7 +9,7 @@ start:
.data 0x80134D3C
.data 0x00000004
.data 0x38600000
li r3, 0
.data 0x00000000
.data 0x00000000
@@ -9,7 +9,7 @@ start:
.data 0x80134FA0
.data 0x00000004
.data 0x38600000
li r3, 0
.data 0x00000000
.data 0x00000000
@@ -9,7 +9,7 @@ start:
.data 0x80135108
.data 0x00000004
.data 0x38600000
li r3, 0
.data 0x00000000
.data 0x00000000
@@ -9,7 +9,7 @@ start:
.data 0x80135040
.data 0x00000004
.data 0x38600000
li r3, 0
.data 0x00000000
.data 0x00000000
@@ -9,7 +9,7 @@ start:
.data 0x801352D0
.data 0x00000004
.data 0x38600000
li r3, 0
.data 0x00000000
.data 0x00000000
@@ -9,7 +9,7 @@ start:
.data 0x80092380
.data 0x00000004
.data 0x38600000
li r3, 0
.data 0x00000000
.data 0x00000000
@@ -9,7 +9,7 @@ start:
.data 0x8009242C
.data 0x00000004
.data 0x38600000
li r3, 0
.data 0x00000000
.data 0x00000000
@@ -9,7 +9,7 @@ start:
.data 0x80092C78
.data 0x00000004
.data 0x38600000
li r3, 0
.data 0x00000000
.data 0x00000000
@@ -9,7 +9,7 @@ start:
.data 0x80092588
.data 0x00000004
.data 0x38600000
li r3, 0
.data 0x00000000
.data 0x00000000
@@ -1,8 +1,9 @@
# This patch enables the debug menus in PSO Episode 3 USA. Specifically, it
# causes them all to load, but only activates one (selected by uncommenting a
# line below). See the comments for more information. Most of these editors are
# present in PSO PC and PSOX as well, but not in GC Episodes 1 & 2. There are
# notes in the below comments that may help get these editors working on PSO PC.
# present in PSO PC and PSO Xbox as well, but not in GC Episodes 1 & 2. There
# are notes in the below comments that may help get these editors working on
# PSO PC.
# This patch must not be run from the Patches menu - it should only be run with
# the $patch command, since the client will likely crash if the player is not
@@ -0,0 +1,103 @@
# This patch replaces the prices and contents of Pinz's Shop.
# Each entry is structured as follows:
# uint16_t card_id;
# int16_t min_clv; // -1 = limit doesn't apply
# int16_t max_clv; // -1 = limit doesn't apply
# uint16_t relative_chance;
# The values in the patch data below are the defaults.
.meta name="New Pinz cards"
.meta description="Replaces the cards\navailable in Pinz's\nShop"
entry_ptr:
reloc0:
.offsetof start
start:
.include WriteCodeBlocksGC
# Meseta prices
.data 0x80487E80
.data 0x00000010
.data 50
.data 100
.data 150
.data 0xFFFFFFFF
# Card Capsule Machine 1
.data 0x80487F40
.data 0x00000078
.binary 017C FFFF FFFF 1B58
.binary 0173 FFFF FFFF 1B58
.binary 0176 FFFF FFFF 1F40
.binary 006A FFFF FFFF 2710
.binary 01EB FFFF FFFF 1F40
.binary 01F1 FFFF FFFF 1770
.binary 020E FFFF FFFF 1770
.binary 0177 FFFF FFFF 1B58
.binary 01AE FFFF FFFF 1770
.binary 028A FFFF FFFF 1770
.binary 01E8 FFFF FFFF 1770
.binary 00A6 FFFF FFFF 1770
.binary 023D FFFF FFFF 1388
.binary 0208 FFFF FFFF 03E8
.binary FFFF FFFF FFFF FFFF
# Card Capsule Machine 2
.data 0x80487FB8
.data 0x00000078
.binary 017C FFFF FFFF 2710
.binary 027E FFFF FFFF 1388
.binary 0075 FFFF FFFF 1388
.binary 020E FFFF FFFF 1388
.binary 014D FFFF FFFF 1388
.binary 000F FFFF FFFF 1770
.binary 0269 FFFF FFFF 1F40
.binary 006D FFFF FFFF 1B58
.binary 0071 FFFF FFFF 1F40
.binary 00C3 FFFF FFFF 1F40
.binary 0208 FFFF FFFF 0BB8
.binary 0138 FFFF FFFF 1F40
.binary 0235 FFFF FFFF 1770
.binary 00E6 FFFF FFFF 03E8
.binary FFFF FFFF FFFF FFFF
# Card Capsule Machine 3
.data 0x80488030
.data 0x00000078
.binary 01AE FFFF FFFF 1F40
.binary 014D FFFF FFFF 2328
.binary 00BA FFFF FFFF 2328
.binary 00A5 FFFF FFFF 2710
.binary 01E8 FFFF FFFF 1F40
.binary 025D FFFF FFFF 1F40
.binary 028A FFFF FFFF 1F40
.binary 0249 FFFF FFFF 2328
.binary 0071 FFFF FFFF 1F40
.binary 00B2 FFFF FFFF 1F40
.binary 0129 FFFF FFFF 1F40
.binary 01C1 FFFF FFFF 0BB8
.binary 0132 FFFF FFFF 0BB8
.binary 0148 FFFF FFFF 0BB8
.binary FFFF FFFF FFFF FFFF
# Coin machine
.data 0x804880A8
.data 0x00000070
.binary 00A6 FFFF FFFF 2710
.binary 01C1 FFFF FFFF 2710
.binary 01FA FFFF FFFF 2710
.binary 0208 FFFF FFFF 2710
.binary 00E6 FFFF FFFF 2710
.binary 00FF FFFF FFFF 2710
.binary 0132 FFFF FFFF 2710
.binary 013C FFFF FFFF 2710
.binary 0148 FFFF FFFF 2710
.binary 0198 FFFF FFFF 2710
.binary 023D FFFF FFFF 2710
.binary 00CA FFFF FFFF 2710
.binary 00CF FFFF FFFF 2710
.binary FFFF FFFF FFFF FFFF
.data 0x00000000
.data 0x00000000
@@ -0,0 +1,103 @@
# This patch replaces the prices and contents of Pinz's Shop.
# Each entry is structured as follows:
# uint16_t card_id;
# int16_t min_clv; // -1 = limit doesn't apply
# int16_t max_clv; // -1 = limit doesn't apply
# uint16_t relative_chance;
# The values in the patch data below are the defaults.
.meta name="New Pinz cards"
.meta description="Replaces the cards\navailable in Pinz's\nShop"
entry_ptr:
reloc0:
.offsetof start
start:
.include WriteCodeBlocksGC
# Meseta prices
.data 0x80487140
.data 0x00000010
.data 50
.data 100
.data 150
.data 0xFFFFFFFF
# Card Capsule Machine 1
.data 0x80487200
.data 0x00000078
.binary 017C FFFF FFFF 1B58
.binary 0173 FFFF FFFF 1B58
.binary 0176 FFFF FFFF 1F40
.binary 006A FFFF FFFF 2710
.binary 01EB FFFF FFFF 1F40
.binary 01F1 FFFF FFFF 1770
.binary 020E FFFF FFFF 1770
.binary 0177 FFFF FFFF 1B58
.binary 01AE FFFF FFFF 1770
.binary 028A FFFF FFFF 1770
.binary 01E8 FFFF FFFF 1770
.binary 00A6 FFFF FFFF 1770
.binary 023D FFFF FFFF 1388
.binary 0208 FFFF FFFF 03E8
.binary FFFF FFFF FFFF FFFF
# Card Capsule Machine 2
.data 0x80487278
.data 0x00000078
.binary 017C FFFF FFFF 2710
.binary 027E FFFF FFFF 1388
.binary 0075 FFFF FFFF 1388
.binary 020E FFFF FFFF 1388
.binary 014D FFFF FFFF 1388
.binary 000F FFFF FFFF 1770
.binary 0269 FFFF FFFF 1F40
.binary 006D FFFF FFFF 1B58
.binary 0071 FFFF FFFF 1F40
.binary 00C3 FFFF FFFF 1F40
.binary 0208 FFFF FFFF 0BB8
.binary 0138 FFFF FFFF 1F40
.binary 0235 FFFF FFFF 1770
.binary 00E6 FFFF FFFF 03E8
.binary FFFF FFFF FFFF FFFF
# Card Capsule Machine 3
.data 0x804872F0
.data 0x00000078
.binary 01AE FFFF FFFF 1F40
.binary 014D FFFF FFFF 2328
.binary 00BA FFFF FFFF 2328
.binary 00A5 FFFF FFFF 2710
.binary 01E8 FFFF FFFF 1F40
.binary 025D FFFF FFFF 1F40
.binary 028A FFFF FFFF 1F40
.binary 0249 FFFF FFFF 2328
.binary 0071 FFFF FFFF 1F40
.binary 00B2 FFFF FFFF 1F40
.binary 0129 FFFF FFFF 1F40
.binary 01C1 FFFF FFFF 0BB8
.binary 0132 FFFF FFFF 0BB8
.binary 0148 FFFF FFFF 0BB8
.binary FFFF FFFF FFFF FFFF
# Coin machine
.data 0x80487368
.data 0x00000070
.binary 00A6 FFFF FFFF 2710
.binary 01C1 FFFF FFFF 2710
.binary 01FA FFFF FFFF 2710
.binary 0208 FFFF FFFF 2710
.binary 00E6 FFFF FFFF 2710
.binary 00FF FFFF FFFF 2710
.binary 0132 FFFF FFFF 2710
.binary 013C FFFF FFFF 2710
.binary 0148 FFFF FFFF 2710
.binary 0198 FFFF FFFF 2710
.binary 023D FFFF FFFF 2710
.binary 00CA FFFF FFFF 2710
.binary 00CF FFFF FFFF 2710
.binary FFFF FFFF FFFF FFFF
.data 0x00000000
.data 0x00000000
@@ -0,0 +1,103 @@
# This patch replaces the prices and contents of Pinz's Shop.
# Each entry is structured as follows:
# uint16_t card_id;
# int16_t min_clv; // -1 = limit doesn't apply
# int16_t max_clv; // -1 = limit doesn't apply
# uint16_t relative_chance;
# The values in the patch data below are the defaults.
.meta name="New Pinz cards"
.meta description="Replaces the cards\navailable in Pinz's\nShop"
entry_ptr:
reloc0:
.offsetof start
start:
.include WriteCodeBlocksGC
# Meseta prices
.data 0x8048A260
.data 0x00000010
.data 50
.data 100
.data 150
.data 0xFFFFFFFF
# Card Capsule Machine 1
.data 0x8048A320
.data 0x00000078
.binary 017C FFFF FFFF 1B58
.binary 0173 FFFF FFFF 1B58
.binary 0176 FFFF FFFF 1F40
.binary 006A FFFF FFFF 2710
.binary 01EB FFFF FFFF 1F40
.binary 01F1 FFFF FFFF 1770
.binary 020E FFFF FFFF 1770
.binary 0177 FFFF FFFF 1B58
.binary 01AE FFFF FFFF 1770
.binary 028A FFFF FFFF 1770
.binary 01E8 FFFF FFFF 1770
.binary 00A6 FFFF FFFF 1770
.binary 023D FFFF FFFF 1388
.binary 0208 FFFF FFFF 03E8
.binary FFFF FFFF FFFF FFFF
# Card Capsule Machine 2
.data 0x8048A398
.data 0x00000078
.binary 017C FFFF FFFF 2710
.binary 027E FFFF FFFF 1388
.binary 0075 FFFF FFFF 1388
.binary 020E FFFF FFFF 1388
.binary 014D FFFF FFFF 1388
.binary 000F FFFF FFFF 1770
.binary 0269 FFFF FFFF 1F40
.binary 006D FFFF FFFF 1B58
.binary 0071 FFFF FFFF 1F40
.binary 00C3 FFFF FFFF 1F40
.binary 0208 FFFF FFFF 0BB8
.binary 0138 FFFF FFFF 1F40
.binary 0235 FFFF FFFF 1770
.binary 00E6 FFFF FFFF 03E8
.binary FFFF FFFF FFFF FFFF
# Card Capsule Machine 3
.data 0x8048A410
.data 0x00000078
.binary 01AE FFFF FFFF 1F40
.binary 014D FFFF FFFF 2328
.binary 00BA FFFF FFFF 2328
.binary 00A5 FFFF FFFF 2710
.binary 01E8 FFFF FFFF 1F40
.binary 025D FFFF FFFF 1F40
.binary 028A FFFF FFFF 1F40
.binary 0249 FFFF FFFF 2328
.binary 0071 FFFF FFFF 1F40
.binary 00B2 FFFF FFFF 1F40
.binary 0129 FFFF FFFF 1F40
.binary 01C1 FFFF FFFF 0BB8
.binary 0132 FFFF FFFF 0BB8
.binary 0148 FFFF FFFF 0BB8
.binary FFFF FFFF FFFF FFFF
# Coin machine
.data 0x8048A488
.data 0x00000070
.binary 00A6 FFFF FFFF 2710
.binary 01C1 FFFF FFFF 2710
.binary 01FA FFFF FFFF 2710
.binary 0208 FFFF FFFF 2710
.binary 00E6 FFFF FFFF 2710
.binary 00FF FFFF FFFF 2710
.binary 0132 FFFF FFFF 2710
.binary 013C FFFF FFFF 2710
.binary 0148 FFFF FFFF 2710
.binary 0198 FFFF FFFF 2710
.binary 023D FFFF FFFF 2710
.binary 00CA FFFF FFFF 2710
.binary 00CF FFFF FFFF 2710
.binary FFFF FFFF FFFF FFFF
.data 0x00000000
.data 0x00000000
@@ -8,7 +8,7 @@ start:
mflr r0
stw [r1 + 4], r0
stwu [r1 - 0x20], r1
bl read
bl resume
call_addr:
.zero
arg0:
+5
View File
@@ -279,6 +279,11 @@
// use chat commands.
"EnableChatCommands": true,
// Number of backup character slots for each account, accessible with the
// $savechar, $loadchar, and $checkchar commands. This can be any value, but
// it's recommended to use a small number such as 16.
"BackupCharacterSlots": 16,
// Information menu contents. Each entry is a list containing [title,
// short description, full contents]. In the short description and full
// contents, you can use PSO escape codes with the $ character (for example,