Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cbd9402d0 | |||
| 0396337994 | |||
| 6fbc0829ae | |||
| 4f41cbc9ce | |||
| d1e6d75d70 | |||
| 067f2439ca | |||
| 2d2edbd7be | |||
| f5f457aa6f | |||
| aabbafb749 | |||
| e72e37f713 | |||
| f884893b18 | |||
| c74c0e2250 | |||
| 5f4d2ec891 | |||
| 33b0ab3ed3 |
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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, ×tamp, 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, ×tamp, 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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") {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user