From f05e68492d2976e77ee49e839cf3af8e5dd8d4ca Mon Sep 17 00:00:00 2001 From: James Osborne Date: Fri, 1 May 2026 23:14:17 -0400 Subject: [PATCH] PSO Peeps Start --- src/Client.hh | 2 + src/GameServer.cc | 2 + src/HTTPServer.cc | 151 +++++++ src/ItemCreator.cc | 16 +- src/ItemCreator.hh | 4 + src/Lobby.cc | 5 + src/Lobby.hh | 2 + src/Menu.hh | 6 + src/ProxyCommands.cc | 18 +- src/ReceiveCommands.cc | 500 +++++++++++++++++++++- src/ReceiveSubcommands.cc | 220 ++++++++++ src/SendCommands.cc | 113 +++-- src/ServerState.cc | 16 + src/ServerState.hh | 8 + system/quests/extermination/q101-bb-e.bin | Bin 2576 -> 8464 bytes system/quests/extermination/q101-bb.dat | Bin 7047 -> 17460 bytes system/quests/solo-story/q001.json | 1 - system/quests/solo-story/q002.json | 1 - system/quests/solo-story/q003.json | 2 - system/quests/solo-story/q004.json | 1 - system/quests/solo-story/q005.json | 2 - system/quests/solo-story/q006.json | 2 - system/quests/solo-story/q007.json | 2 - system/quests/solo-story/q008.json | 2 - system/quests/solo-story/q009.json | 2 - system/quests/solo-story/q010.json | 2 - system/quests/solo-story/q011.json | 2 - system/quests/solo-story/q012.json | 2 - system/quests/solo-story/q013.json | 2 - system/quests/solo-story/q014.json | 2 - system/quests/solo-story/q015.json | 2 - system/quests/solo-story/q016.json | 2 - system/quests/solo-story/q017.json | 2 - system/quests/solo-story/q018.json | 2 - system/quests/solo-story/q019.json | 2 - system/quests/solo-story/q020.json | 2 - system/quests/solo-story/q021.json | 2 - system/quests/solo-story/q022.json | 2 - system/quests/solo-story/q023.json | 2 - system/quests/solo-story/q024.json | 2 - system/quests/solo-story/q025.json | 2 - system/quests/solo-story/q026.json | 1 - 42 files changed, 1019 insertions(+), 92 deletions(-) diff --git a/src/Client.hh b/src/Client.hh index 5ce7030a..5344214c 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -140,6 +140,7 @@ public: std::shared_ptr channel; std::shared_ptr bb_detector_crypt; ServerBehavior server_behavior; + uint16_t listener_port = 0; std::unordered_map> disconnect_hooks; uint64_t ping_start_time = 0; @@ -150,6 +151,7 @@ public: uint8_t override_lobby_event = 0xFF; // FF = no override uint8_t override_lobby_number = 0x80; // 80 = no override int64_t override_random_seed = -1; + int8_t selected_blueballz_tier = -1; // -1 = normal lobby/game; 0..10 = requested Blueballz tier std::unique_ptr override_variations; VectorXYZF pos; uint32_t floor = 0x0F; diff --git a/src/GameServer.cc b/src/GameServer.cc index 8f0d4ed5..5a983799 100644 --- a/src/GameServer.cc +++ b/src/GameServer.cc @@ -44,6 +44,7 @@ void GameServer::listen( shared_ptr GameServer::connect_channel(shared_ptr ch, uint16_t port, ServerBehavior initial_state) { auto c = make_shared(this->shared_from_this(), ch, initial_state); + c->listener_port = port; this->log.info_f("Client connected: C-{:X} via TSI-{}-{}-{}", c->id, port, phosg::name_for_enum(ch->version), phosg::name_for_enum(initial_state)); @@ -133,6 +134,7 @@ shared_ptr GameServer::create_client( phosg::TerminalFormat::FG_YELLOW, phosg::TerminalFormat::FG_GREEN); auto c = make_shared(this->shared_from_this(), channel, listen_sock->behavior); + c->listener_port = listen_sock->endpoint.port(); this->log.info_f("Client connected: C-{:X} via {}", c->id, listen_sock->name); return c; diff --git a/src/HTTPServer.cc b/src/HTTPServer.cc index fa3e44f9..3b3ec446 100644 --- a/src/HTTPServer.cc +++ b/src/HTTPServer.cc @@ -5,6 +5,7 @@ #include #include +#include #include #include "GameServer.hh" @@ -54,6 +55,156 @@ HTTPServer::HTTPServer(shared_ptr state) co_return nullptr; }); + this->router.add(HTTPRequest::Method::GET, "/metrics", [this](ArgsT&&) -> RetT { + auto version_label = +[](Version v) -> const char* { + if (is_patch(v)) { + return "patch"; + } else if (is_v4(v)) { + return "v4"; + } else if (is_v3(v)) { + return "v3"; + } else if (is_v1_or_v2(v)) { + return "v2"; + } else { + return "other"; + } + }; + + auto escape_label = +[](const string& in) -> string { + string out; + for (char ch : in) { + if (ch == '\\') { + out += "\\\\"; + } else if (ch == '"') { + out += "\\\""; + } else if (ch == '\n') { + out += "\\n"; + } else { + out += ch; + } + } + return out; + }; + + auto add_metric = [](string& out, const string& name, uint64_t value) -> void { + out += name; + out += " "; + out += std::to_string(value); + out += "\n"; + }; + + auto add_metric_1label = [](string& out, const string& name, const string& label_name, const string& label_value, uint64_t value) -> void { + out += name; + out += "{"; + out += label_name; + out += "=\""; + out += label_value; + out += "\"} "; + out += std::to_string(value); + out += "\n"; + }; + + map connected_by_version; + map lobby_players_by_version; + map game_players_by_version; + + uint64_t connected_total = 0; + uint64_t lobbies_total = 0; + uint64_t games_total = 0; + uint64_t players_in_lobbies_total = 0; + uint64_t players_in_games_total = 0; + + for (const auto& c : this->state->game_server->all_clients()) { + connected_total++; + connected_by_version[version_label(c->version())]++; + } + + for (const auto& [_, l] : this->state->id_to_lobby) { + if (l->is_game()) { + games_total++; + } else { + lobbies_total++; + } + + for (size_t z = 0; z < l->max_clients; z++) { + auto lc = l->clients[z]; + if (!lc) { + continue; + } + + const char* v = version_label(lc->version()); + if (l->is_game()) { + players_in_games_total++; + game_players_by_version[v]++; + } else { + players_in_lobbies_total++; + lobby_players_by_version[v]++; + } + } + } + + string server_name = escape_label(this->state->name); + string revision = escape_label(GIT_REVISION_HASH); + + string out; + out += "# HELP pso_newserv_up Whether this newserv HTTP metrics endpoint is reachable\n"; + out += "# TYPE pso_newserv_up gauge\n"; + add_metric(out, "pso_newserv_up", 1); + + out += "# HELP pso_newserv_build_info Build and server identity info\n"; + out += "# TYPE pso_newserv_build_info gauge\n"; + out += "pso_newserv_build_info{server_name=\""; + out += server_name; + out += "\",revision=\""; + out += revision; + out += "\",build_time=\""; + out += std::to_string(BUILD_TIMESTAMP); + out += "\"} 1\n"; + + out += "# HELP pso_newserv_clients_connected_total Connected clients, including patch/proxy/menu states\n"; + out += "# TYPE pso_newserv_clients_connected_total gauge\n"; + add_metric(out, "pso_newserv_clients_connected_total", connected_total); + + out += "# HELP pso_newserv_clients_connected Connected clients by coarse version family\n"; + out += "# TYPE pso_newserv_clients_connected gauge\n"; + for (const auto& [version, count] : connected_by_version) { + add_metric_1label(out, "pso_newserv_clients_connected", "version", version, count); + } + + out += "# HELP pso_newserv_lobbies_total Non-game lobby count\n"; + out += "# TYPE pso_newserv_lobbies_total gauge\n"; + add_metric(out, "pso_newserv_lobbies_total", lobbies_total); + + out += "# HELP pso_newserv_games_total Game room count\n"; + out += "# TYPE pso_newserv_games_total gauge\n"; + add_metric(out, "pso_newserv_games_total", games_total); + + out += "# HELP pso_newserv_players_in_lobbies_total Players currently in non-game lobbies\n"; + out += "# TYPE pso_newserv_players_in_lobbies_total gauge\n"; + add_metric(out, "pso_newserv_players_in_lobbies_total", players_in_lobbies_total); + + out += "# HELP pso_newserv_players_in_games_total Players currently in game rooms\n"; + out += "# TYPE pso_newserv_players_in_games_total gauge\n"; + add_metric(out, "pso_newserv_players_in_games_total", players_in_games_total); + + out += "# HELP pso_newserv_players_in_lobbies Players currently in non-game lobbies by coarse version family\n"; + out += "# TYPE pso_newserv_players_in_lobbies gauge\n"; + for (const auto& [version, count] : lobby_players_by_version) { + add_metric_1label(out, "pso_newserv_players_in_lobbies", "version", version, count); + } + + out += "# HELP pso_newserv_players_in_games Players currently in game rooms by coarse version family\n"; + out += "# TYPE pso_newserv_players_in_games gauge\n"; + for (const auto& [version, count] : game_players_by_version) { + add_metric_1label(out, "pso_newserv_players_in_games", "version", version, count); + } + + co_return RouterRetT(RawResponse{ + .content_type = "text/plain; version=0.0.4; charset=utf-8", + .data = std::move(out), + }); + }); + this->router.add(HTTPRequest::Method::GET, "/y/clients", [this](ArgsT&&) -> RetT { auto res = make_shared(phosg::JSON::list()); for (const auto& c : this->state->game_server->all_clients()) { diff --git a/src/ItemCreator.cc b/src/ItemCreator.cc index db97abe9..4a83e576 100644 --- a/src/ItemCreator.cc +++ b/src/ItemCreator.cc @@ -307,12 +307,20 @@ ItemData ItemCreator::check_rare_specs_and_create_rare_item( this->rand_int(0x100000000); } } - this->log.info_f("{} specs to check with det={:08X}", specs.size(), det); + this->log.info_f("{} specs to check with det={:08X} rare_mult={:g}", specs.size(), det, this->rare_drop_rate_multiplier); for (const auto& spec : specs) { - if (this->log.should_log(phosg::LogLevel::L_INFO)) { - this->log.info_f("Checking spec {:08X} => {} with det={:08X}", spec.probability, spec.data.hex(), det); + uint64_t effective_probability = spec.probability; + if (this->rare_drop_rate_multiplier != 1.0) { + double multiplied_probability = static_cast(spec.probability) * this->rare_drop_rate_multiplier; + effective_probability = (multiplied_probability >= 4294967296.0) + ? 0x100000000ULL + : static_cast(multiplied_probability); } - det -= spec.probability; + if (this->log.should_log(phosg::LogLevel::L_INFO)) { + this->log.info_f("Checking spec {:08X} => effective {:08X} => {} with det={:08X}", + spec.probability, effective_probability, spec.data.hex(), det); + } + det -= effective_probability; if (det < 0) { return this->create_rare_item(spec.data, area); } diff --git a/src/ItemCreator.hh b/src/ItemCreator.hh index 8e77e43a..074c111e 100644 --- a/src/ItemCreator.hh +++ b/src/ItemCreator.hh @@ -40,6 +40,9 @@ public: inline void set_legacy_replay() { this->is_legacy_replay = true; } + inline void set_rare_drop_rate_multiplier(double multiplier) { + this->rare_drop_rate_multiplier = multiplier; + } DropResult on_monster_item_drop(EnemyType enemy_type, uint8_t area, bool force_rare); DropResult on_box_item_drop(uint8_t area, bool force_rare); @@ -75,6 +78,7 @@ private: phosg::PrefixedLogger log; Version logic_version; bool is_legacy_replay; + double rare_drop_rate_multiplier = 1.0; std::shared_ptr stack_limits; GameMode mode; Difficulty difficulty; diff --git a/src/Lobby.cc b/src/Lobby.cc index 3d1417f2..d6aa8be4 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -230,6 +230,11 @@ void Lobby::create_item_creator(Version logic_version) { effective_section_id, rand_crypt, this->quest ? this->quest->meta.battle_rules : nullptr); + if (this->blueballz_tier >= 0) { + double rare_mult = 1.25 + (static_cast(this->blueballz_tier) * 0.25); + this->item_creator->set_rare_drop_rate_multiplier(rare_mult); + this->log.info_f("Blueballz +{} rare drop rate multiplier set to {:g}x", this->blueballz_tier, rare_mult); + } if (s->use_legacy_item_random_behavior) { this->item_creator->set_legacy_replay(); } diff --git a/src/Lobby.hh b/src/Lobby.hh index da3419bf..e29253be 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -81,6 +81,7 @@ struct Lobby : public std::enable_shared_from_this { START_BATTLE_PLAYER_IMMEDIATELY = 0x00010000, CANNOT_CHANGE_CHEAT_MODE = 0x00020000, USE_CREATOR_SECTION_ID = 0x00040000, + BLUEBALLZ_PLUS0 = 0x00080000, // Flags used only for lobbies PUBLIC = 0x01000000, DEFAULT = 0x02000000, @@ -118,6 +119,7 @@ struct Lobby : public std::enable_shared_from_this { Episode episode = Episode::NONE; GameMode mode = GameMode::NORMAL; Difficulty difficulty = Difficulty::NORMAL; + int8_t blueballz_tier = -1; // -1 = disabled; 0..10 = Blueballz +0..+10 float base_exp_multiplier = 1.0f; float exp_share_multiplier = 0.5f; float challenge_exp_multiplier = 1.0f; diff --git a/src/Menu.hh b/src/Menu.hh index f45e122b..e9a4d16d 100644 --- a/src/Menu.hh +++ b/src/Menu.hh @@ -42,6 +42,12 @@ constexpr uint32_t PATCH_SWITCHES = 0x11666611; constexpr uint32_t PROGRAMS = 0x11777711; constexpr uint32_t DISCONNECT = 0x11888811; constexpr uint32_t CLEAR_LICENSE = 0x11999911; +constexpr uint32_t BB_LIVE_SHIP = 0x11AAAA11; +constexpr uint32_t BB_TEST_SHIP = 0x11BBBB11; +constexpr uint32_t BB_VANILLA_SHIP = 0x11CCCC11; +constexpr uint32_t BB_HARDCORE_SHIP = 0x11F00D11; +constexpr uint32_t BB_DEV_SHIP = 0x11EEEE11; +constexpr uint32_t BLUEBALLZ_PLUS0 = 0x11DDDD11; } // namespace MainMenuItemID namespace ClearLicenseConfirmationMenuItemID { diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 70ed72a8..8c55f0b7 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -1024,8 +1024,15 @@ static asio::awaitable S_6x(shared_ptr c, Channel::Messag case 0x17: { const auto& cmd = msg.check_size_t(); if (cmd.header.entity_id == c->lobby_client_id) { - c->log.warning_f("Blocking subcommand 6x17 targeting local client"); - co_return HandlerResult::SUPPRESS; + // Vol Opt phase 1 -> phase 2 uses 6x17 targeting the local client to move + // players into the second arena phase. Allow this only while the proxy-side + // client is already on the Vol Opt floor. + if (c->floor == 0x0D) { + c->log.info_f("Allowing subcommand 6x17 targeting local client on Vol Opt floor"); + } else { + c->log.warning_f("Blocking subcommand 6x17 targeting local client outside Vol Opt floor"); + co_return HandlerResult::SUPPRESS; + } } break; } @@ -2830,7 +2837,12 @@ asio::awaitable handle_proxy_server_commands( if (ec == asio::error::eof || ec == asio::error::connection_reset) { error_str = "Server channel\ndisconnected"; } else if (ec == asio::error::operation_aborted) { - // This happens when the player chooses Change Ship/Change Block, so we don't show an error message + // If this is the currently-active backend channel, treat the abort as a backend disconnect. + // Normal Change Ship/Change Block reconnects replace ses->server_channel first; the old + // aborted channel will not match here, so those expected aborts stay silent. + if ((c->proxy_session == ses) && (ses->server_channel == channel)) { + error_str = "Server channel\ndisconnected"; + } } else { error_str = e.what(); } diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 87cc5c17..8164f667 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -32,6 +33,138 @@ const char* BATTLE_TABLE_DISCONNECT_HOOK_NAME = "battle_table_state"; const char* QUEST_BARRIER_DISCONNECT_HOOK_NAME = "quest_barrier"; const char* ADD_NEXT_CLIENT_DISCONNECT_HOOK_NAME = "add_next_game_client"; +static string bb_test_taint_filename(shared_ptr c) { + return c->character_filename() + ".test-tainted"; +} + +static string bb_test_taint_grandfather_filename(shared_ptr c) { + return c->character_filename() + ".grandfathered-before-test-taint"; +} + +static bool file_exists_for_bb_taint(const string& filename) { + ifstream f(filename); + return f.good(); +} + +static bool bb_character_is_test_tainted(shared_ptr c) { + return file_exists_for_bb_taint(bb_test_taint_filename(c)); +} + +static bool bb_character_is_test_taint_grandfathered(shared_ptr c) { + return file_exists_for_bb_taint(bb_test_taint_grandfather_filename(c)); +} + +static bool mark_bb_character_test_tainted(shared_ptr c) { + string filename = bb_test_taint_filename(c); + ofstream f(filename, ios::out | ios::trunc); + if (!f.good()) { + return false; + } + + f << "status=test-tainted\n"; + f << "reason=entered-test-ship\n"; + f << "account_id=" << c->login->account->account_id << "\n"; + f << "character_file=" << c->character_filename() << "\n"; + return f.good(); +} + + +static string bb_hardcore_filename(shared_ptr c) { + return c->character_filename() + ".hardcore"; +} + +static string bb_hardcore_ineligible_filename(shared_ptr c) { + return c->character_filename() + ".hardcore-ineligible"; +} + +static string bb_hardcore_dead_filename(shared_ptr c) { + return c->character_filename() + ".hardcore-dead"; +} + +static bool bb_character_is_hardcore(shared_ptr c) { + return file_exists_for_bb_taint(bb_hardcore_filename(c)); +} + +static bool bb_character_is_hardcore_ineligible(shared_ptr c) { + return file_exists_for_bb_taint(bb_hardcore_ineligible_filename(c)); +} + +static bool bb_character_is_hardcore_dead(shared_ptr c) { + return file_exists_for_bb_taint(bb_hardcore_dead_filename(c)); +} + +static bool write_bb_hardcore_marker(shared_ptr c, const string& filename, const char* status, const char* reason) { + ofstream f(filename, ios::out | ios::trunc); + if (!f.good()) { + return false; + } + + uint32_t account_id = 0; + if (c->login && c->login->account) { + account_id = c->login->account->account_id; + } + + f << "status=" << status << "\n"; + f << "reason=" << reason << "\n"; + f << "account_id=" << account_id << "\n"; + f << "character_file=" << c->character_filename() << "\n"; + return f.good(); +} + +static bool mark_bb_character_hardcore(shared_ptr c) { + return write_bb_hardcore_marker(c, bb_hardcore_filename(c), "hardcore", "entered-hardcore-ship"); +} + +static bool mark_bb_character_hardcore_ineligible(shared_ptr c) { + return write_bb_hardcore_marker(c, bb_hardcore_ineligible_filename(c), "hardcore-ineligible", "entered-non-hardcore-ship"); +} + +static bool enforce_bb_hardcore_ship_lock(shared_ptr c, bool current_ship_is_hardcore) { + if (bb_character_is_hardcore_dead(c)) { + send_message_box(c, "$C6This Hardcore character is dead.\n\n$C7The character remains in your list, but cannot be loaded."); + return false; + } + + if (current_ship_is_hardcore) { + if (bb_character_is_hardcore_ineligible(c) || + bb_character_is_test_tainted(c) || + bb_character_is_test_taint_grandfathered(c)) { + if (!bb_character_is_hardcore_ineligible(c)) { + mark_bb_character_hardcore_ineligible(c); + } + send_message_box(c, "$C6This BB character has already entered a non-Hardcore ship.\n\n$C7Create a new character and enter Hardcore first."); + return false; + } + + if (!bb_character_is_hardcore(c)) { + if (!mark_bb_character_hardcore(c)) { + send_message_box(c, "$C6Could not mark this character for Hardcore.\n\n$C7Please report this."); + return false; + } + c->log.info_f("Marked BB character as Hardcore: {}", c->character_filename()); + } + + return true; + + } else { + if (bb_character_is_hardcore(c)) { + send_message_box(c, "$C6This BB character is locked to Hardcore.\n\n$C7Hardcore characters cannot enter non-Hardcore ships."); + return false; + } + + if (!bb_character_is_hardcore_ineligible(c)) { + if (!mark_bb_character_hardcore_ineligible(c)) { + send_message_box(c, "$C6Could not mark this character as non-Hardcore.\n\n$C7Please report this."); + return false; + } + c->log.info_f("Marked BB character as Hardcore-ineligible: {}", c->character_filename()); + } + + return true; + } +} + + asio::awaitable on_connect(std::shared_ptr c) { auto s = c->require_server_state(); if (s->default_switch_assist_enabled) { @@ -42,6 +175,16 @@ asio::awaitable on_connect(std::shared_ptr c) { case ServerBehavior::PC_CONSOLE_DETECT: { 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; + + // PSO Peeps: keep GC normal/x5/x10 discs separated after pc_console_detect. + // Without this, all GC discs collapse to gc-us3 and ship select cannot tell + // whether the player entered through normal, x5, or x10. + if (c->listener_port == 19105) { + console_port = s->name_to_port_config.at("gc-us3-x5")->port; + } else if (c->listener_port == 19110) { + console_port = s->name_to_port_config.at("gc-us3-x10")->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 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 Channel where @@ -87,6 +230,21 @@ static void send_main_menu(shared_ptr c) { auto s = c->require_server_state(); auto main_menu = make_shared(MenuID::MAIN, s->name); + + bool is_bb_ship_menu_client = + ((c->version() == Version::BB_V4) || (c->version() == Version::BB_PATCH)); + bool bb_frontdoor_ship_menu = s->enable_bb_ship_selection_menu && is_bb_ship_menu_client; + bool bb_destination_transport_menu = !s->enable_bb_ship_selection_menu && is_bb_ship_menu_client; + bool show_bb_live_test_menu_items = bb_frontdoor_ship_menu || bb_destination_transport_menu; + bool show_bb_restricted_ship_menu_items = bb_frontdoor_ship_menu; + + uint32_t go_to_lobby_menu_item_flags = + (s->proxy_destinations_dc.empty() ? 0 : MenuItem::Flag::INVISIBLE_ON_DC) | + (s->proxy_destinations_pc.empty() ? 0 : MenuItem::Flag::INVISIBLE_ON_PC) | + (s->proxy_destinations_gc.empty() ? 0 : MenuItem::Flag::INVISIBLE_ON_GC) | + (s->proxy_destinations_xb.empty() ? 0 : MenuItem::Flag::INVISIBLE_ON_XB) | + (bb_frontdoor_ship_menu ? MenuItem::Flag::INVISIBLE_ON_BB : 0); + main_menu->items.emplace_back( MainMenuItemID::GO_TO_LOBBY, "Go to lobby", [wc = weak_ptr(c)]() -> string { @@ -117,18 +275,70 @@ static void send_main_menu(shared_ptr c) { "$C6{}$C7 players online\n$C6{}$C7 games\n$C6{}$C7 compatible games", num_players, num_games, num_compatible_games); }, - 0); + go_to_lobby_menu_item_flags); + bool show_blueballz_menu_items = + bb_destination_transport_menu && + s->enable_blueballz && + (s->blueballz_unlocked_tier_v4 >= 0); + + if (show_blueballz_menu_items) { + int64_t max_blueballz_menu_tier = min( + s->blueballz_max_tier, + s->blueballz_unlocked_tier_v4); + + for (int64_t tier = 0; tier <= max_blueballz_menu_tier; tier++) { + main_menu->items.emplace_back( + MainMenuItemID::BLUEBALLZ_PLUS0 + static_cast(tier), + std::format("Blueballz +{}", tier), + std::format("Enter Blueballz\n+{}", tier), + MenuItem::Flag::BB_ONLY); + } + } + main_menu->items.emplace_back(MainMenuItemID::INFORMATION, "Information", "View server\ninformation", MenuItem::Flag::INVISIBLE_ON_DC_PROTOS | MenuItem::Flag::REQUIRES_MESSAGE_BOXES); + // PSO Peeps: BB uses this same cached MAIN menu for the destination + // pre-lobby page and the lobby counter Transport list. Keep the frontdoor + // as the full ship selector, but only expose safe transport choices on + // destination ships. + uint32_t bb_live_test_menu_item_flags = + show_bb_live_test_menu_items + ? MenuItem::Flag::BB_ONLY + : (MenuItem::Flag::INVISIBLE_ON_DC | + MenuItem::Flag::INVISIBLE_ON_PC | + MenuItem::Flag::INVISIBLE_ON_GC | + MenuItem::Flag::INVISIBLE_ON_XB | + MenuItem::Flag::INVISIBLE_ON_BB); + + uint32_t bb_restricted_ship_menu_item_flags = + show_bb_restricted_ship_menu_items + ? MenuItem::Flag::BB_ONLY + : (MenuItem::Flag::INVISIBLE_ON_DC | + MenuItem::Flag::INVISIBLE_ON_PC | + MenuItem::Flag::INVISIBLE_ON_GC | + MenuItem::Flag::INVISIBLE_ON_XB | + MenuItem::Flag::INVISIBLE_ON_BB); + + main_menu->items.emplace_back(MainMenuItemID::BB_LIVE_SHIP, "PSO-Peeps Live", + "Join the live\nPSO-Peeps ship", bb_live_test_menu_item_flags); + main_menu->items.emplace_back(MainMenuItemID::BB_TEST_SHIP, "Test Ship", + "Join the test\nconfiguration ship", bb_live_test_menu_item_flags); + main_menu->items.emplace_back(MainMenuItemID::BB_DEV_SHIP, "Dev Ship", + "Join the dev\nexperimental ship", bb_restricted_ship_menu_item_flags); + main_menu->items.emplace_back(MainMenuItemID::BB_VANILLA_SHIP, "Vanilla Ship", + "Join the vanilla\ndefault-settings ship", bb_restricted_ship_menu_item_flags); + main_menu->items.emplace_back(MainMenuItemID::BB_HARDCORE_SHIP, "Hardcore Ship", + "Join the hardcore\npermadeath ship", bb_restricted_ship_menu_item_flags); + uint32_t proxy_destinations_menu_item_flags = (s->proxy_destinations_dc.empty() ? MenuItem::Flag::INVISIBLE_ON_DC : 0) | (s->proxy_destinations_pc.empty() ? MenuItem::Flag::INVISIBLE_ON_PC : 0) | (s->proxy_destinations_gc.empty() ? MenuItem::Flag::INVISIBLE_ON_GC : 0) | (s->proxy_destinations_xb.empty() ? MenuItem::Flag::INVISIBLE_ON_XB : 0) | MenuItem::Flag::INVISIBLE_ON_BB; - main_menu->items.emplace_back(MainMenuItemID::PROXY_DESTINATIONS, "Proxy server", - "Connect to another\nserver through the\nproxy", proxy_destinations_menu_item_flags); + main_menu->items.emplace_back(MainMenuItemID::PROXY_DESTINATIONS, "Select ship", + "Choose Live,\nVanilla, or Test", proxy_destinations_menu_item_flags); main_menu->items.emplace_back(MainMenuItemID::DOWNLOAD_QUESTS, "Download quests", "Download quests", MenuItem::Flag::INVISIBLE_ON_DC_PROTOS | MenuItem::Flag::INVISIBLE_ON_PC_NTE | MenuItem::Flag::INVISIBLE_ON_BB); @@ -524,6 +734,7 @@ asio::awaitable end_proxy_session(shared_ptr c, const std::string& } bool is_in_game = c->proxy_session->is_in_game; + bool force_client_disconnect = !error_message.empty(); c->proxy_session->server_channel->disconnect(); c->proxy_session.reset(); @@ -532,9 +743,14 @@ asio::awaitable end_proxy_session(shared_ptr c, const std::string& 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); + if (is_in_game || force_client_disconnect) { + string msg; + if (is_in_game) { + msg = std::format("You cannot return\nto $C6{}$C7\nwhile in a game.\n\n{}", + s->name, error_message); + } else { + msg = std::format("Disconnected from\nremote ship.\n\n{}", error_message); + } send_ship_info(c, msg); c->channel->disconnect(); } else { @@ -2518,8 +2734,34 @@ void set_lobby_quest(shared_ptr l, shared_ptr q, bool substi static asio::awaitable on_10_main_menu(shared_ptr c, uint32_t item_id) { auto s = c->require_server_state(); + if ((item_id >= MainMenuItemID::BLUEBALLZ_PLUS0) && + (item_id <= (MainMenuItemID::BLUEBALLZ_PLUS0 + 10))) { + int64_t tier = item_id - MainMenuItemID::BLUEBALLZ_PLUS0; + + if (!s->enable_blueballz || + !is_v4(c->version()) || + (tier < 0) || + (tier > s->blueballz_max_tier) || + (tier > s->blueballz_unlocked_tier_v4)) { + send_message_box(c, std::format("$C6Blueballz +{} is not available.", tier)); + co_return; + } + + c->selected_blueballz_tier = tier; + c->log.info_f("Blueballz +{} selected from BB menu", tier); + + co_await send_auto_patches_if_needed(c); + co_await enable_save_if_needed(c); + send_lobby_list(c); + if (!c->lobby.lock()) { + s->add_client_to_available_lobby(c); + } + co_return; + } + switch (item_id) { case MainMenuItemID::GO_TO_LOBBY: { + c->selected_blueballz_tier = -1; co_await send_auto_patches_if_needed(c); co_await enable_save_if_needed(c); send_lobby_list(c); @@ -2532,6 +2774,21 @@ static asio::awaitable on_10_main_menu(shared_ptr c, uint32_t item break; } + case MainMenuItemID::BLUEBALLZ_PLUS0: { + if (!s->enable_blueballz || (c->version() != Version::DC_V2) || (s->blueballz_unlocked_tier_v2 < 0)) { + send_message_box(c, "$C6Blueballz +0 is not available."); + break; + } + c->selected_blueballz_tier = 0; + co_await send_auto_patches_if_needed(c); + co_await enable_save_if_needed(c); + send_lobby_list(c); + if (!c->lobby.lock()) { + s->add_client_to_available_lobby(c); + } + break; + } + case MainMenuItemID::INFORMATION: { send_menu(c, s->information_menu(c->version())); c->set_flag(Client::Flag::IN_INFORMATION_MENU); @@ -2542,6 +2799,149 @@ static asio::awaitable on_10_main_menu(shared_ptr c, uint32_t item send_proxy_destinations_menu(c); break; + case MainMenuItemID::BB_LIVE_SHIP: { + if (c->version() != Version::BB_V4) { + send_message_box(c, "$C6This ship option is only for Blue Burst."); + break; + } + if ((c->listener_port == 19145) || (c->listener_port == 19146)) { + c->selected_blueballz_tier = -1; + co_await send_auto_patches_if_needed(c); + co_await enable_save_if_needed(c); + send_lobby_list(c); + if (!c->lobby.lock()) { + s->add_client_to_available_lobby(c); + } + break; + } + send_reconnect(c, s->connect_address_for_client(c), 19145); + break; + } + + case MainMenuItemID::BB_TEST_SHIP: { + if (c->version() != Version::BB_V4) { + send_message_box(c, "$C6This ship option is only for Blue Burst."); + break; + } + if ((c->listener_port == 19345) || (c->listener_port == 19346)) { + c->selected_blueballz_tier = -1; + co_await send_auto_patches_if_needed(c); + co_await enable_save_if_needed(c); + send_lobby_list(c); + if (!c->lobby.lock()) { + s->add_client_to_available_lobby(c); + } + break; + } + + // PSO Peeps alpha test: entering Test taints only non-grandfathered BB characters. + // Existing characters were grandfathered before this feature was enabled. + if (!bb_character_is_test_taint_grandfathered(c)) { + if (!mark_bb_character_test_tainted(c)) { + send_message_box(c, "$C6Could not mark this character for Test access.\n\n$C7Please report this."); + break; + } + } + + send_reconnect(c, s->connect_address_for_client(c), 19345); + break; + } + + case MainMenuItemID::BB_DEV_SHIP: { + if (c->version() != Version::BB_V4) { + send_message_box(c, "$C6This ship option is only for Blue Burst."); + break; + } + if ((c->listener_port == 19445) || (c->listener_port == 19446)) { + c->selected_blueballz_tier = -1; + co_await send_auto_patches_if_needed(c); + co_await enable_save_if_needed(c); + send_lobby_list(c); + if (!c->lobby.lock()) { + s->add_client_to_available_lobby(c); + } + break; + } + + // PSO Peeps dev/test isolation: entering Dev taints only non-grandfathered BB characters. + if (!bb_character_is_test_taint_grandfathered(c)) { + if (!mark_bb_character_test_tainted(c)) { + send_message_box(c, "$C6Could not mark this character for Dev access.\n\n$C7Please report this."); + break; + } + } + + send_reconnect(c, s->connect_address_for_client(c), 19445); + break; + } + + case MainMenuItemID::BB_VANILLA_SHIP: { + if (c->version() != Version::BB_V4) { + send_message_box(c, "$C6This ship option is only for Blue Burst."); + break; + } + if ((c->listener_port == 19245) || (c->listener_port == 19246)) { + c->selected_blueballz_tier = -1; + co_await send_auto_patches_if_needed(c); + co_await enable_save_if_needed(c); + send_lobby_list(c); + if (!c->lobby.lock()) { + s->add_client_to_available_lobby(c); + } + break; + } + + if (bb_character_is_test_tainted(c)) { + send_message_box(c, "$C6This character has been used outside of Vanilla and cannot enter Vanilla.\n\n$C7Use Live/Test with this character, or create a fresh Vanilla character."); + c->channel->disconnect(); + break; + } + + send_reconnect(c, s->connect_address_for_client(c), 19245); + break; + } + case MainMenuItemID::BB_HARDCORE_SHIP: { + if (c->version() != Version::BB_V4) { + send_message_box(c, "$C6This ship option is only for Blue Burst."); + break; + } + if ((c->listener_port == 19545) || (c->listener_port == 19546)) { + send_message_box(c, "$C6You are already on Hardcore Ship.\n\n$C7Choose Go to lobby or another ship."); + break; + } + + // Gate Hardcore before reconnecting. The BB ship-menu flow can transfer + // an already-loaded character, so the backend character-select gate alone + // is not enough. + if (bb_character_is_hardcore_dead(c)) { + send_message_box(c, "$C6This Hardcore character is dead.\n\n$C7The character remains in your list, but cannot be loaded."); + c->channel->disconnect(); + break; + } + + if (bb_character_is_hardcore_ineligible(c) || + bb_character_is_test_tainted(c) || + bb_character_is_test_taint_grandfathered(c)) { + if (!bb_character_is_hardcore_ineligible(c)) { + mark_bb_character_hardcore_ineligible(c); + } + send_message_box(c, "$C6This BB character is tainted and cannot enter Hardcore.\n\n$C7Create a new character and enter Hardcore first."); + c->channel->disconnect(); + break; + } + + if (!bb_character_is_hardcore(c)) { + if (!mark_bb_character_hardcore(c)) { + send_message_box(c, "$C6Could not mark this character for Hardcore.\n\n$C7Please report this."); + break; + } + c->log.info_f("Marked BB character as Hardcore from menu: {}", c->character_filename()); + } + + send_reconnect(c, s->connect_address_for_client(c), 19545); + break; + } + case MainMenuItemID::DOWNLOAD_QUESTS: { send_quest_categories_menu(c, QuestMenuType::DOWNLOAD, Episode::NONE); break; @@ -2706,6 +3106,14 @@ static asio::awaitable on_10_proxy_destinations(shared_ptr c, uint send_message_box(c, "$C6No such destination exists."); c->channel->disconnect(); } else { + // PSO Peeps: boosted GC discs enter through separate frontdoor ports after + // pc_console_detect. Do not allow x5/x10 GC discs into Vanilla. + if ((c->listener_port == 9105 || c->listener_port == 9110 || + c->listener_port == 9201 || c->listener_port == 9202) && dest->second == 19203) { + send_message_box(c, "$C6Vanilla Ship is not available from boosted discs.\n\n$C7Use the normal disc for Vanilla."); + co_return; + } + // Clear Check Tactics menu so client won't see newserv tournament state while logically on another server. There // is no such command on Trial Edition though, so only do this on Ep3 final. if (c->version() == Version::GC_EP3) { @@ -3727,6 +4135,13 @@ static asio::awaitable on_E3_BB(shared_ptr c, Channel::Message& ms c->unload_character(false); c->bb_character_index = cmd.character_index; c->bb_bank_character_index = cmd.character_index; + + auto s = c->require_server_state(); + if (!enforce_bb_hardcore_ship_lock(c, s->enable_hardcore_mode)) { + c->unload_character(false); + co_return; + } + send_approve_player_choice_bb(c); } else { @@ -4575,6 +4990,70 @@ shared_ptr create_game_generic( if (creator_c->check_flag(Client::Flag::IS_CLIENT_CUSTOMIZATION)) { game->set_flag(Lobby::Flag::IS_CLIENT_CUSTOMIZATION); } + game->log.info_f("PSO Peeps BBZ debug: created game name=[{}] version={} difficulty={} enable_blueballz={} unlocked_v4={} max_tier={}", + name, + static_cast(creator_c->version()), + name_for_difficulty(difficulty), + s->enable_blueballz, + s->blueballz_unlocked_tier_v4, + s->blueballz_max_tier); + int8_t requested_blueballz_tier = -1; + bool requested_blueballz = false; + string requested_blueballz_source = "none"; + + if (creator_c->selected_blueballz_tier >= 0) { + requested_blueballz = true; + requested_blueballz_tier = creator_c->selected_blueballz_tier; + requested_blueballz_source = "BB menu selection"; + + } else { + size_t bb_prefix_offset = name.find("[BB+"); + if (bb_prefix_offset != string::npos) { + requested_blueballz = true; + requested_blueballz_source = "room prefix"; + size_t tier_start_offset = bb_prefix_offset + 4; + size_t close_offset = name.find(']', tier_start_offset); + if (close_offset != string::npos) { + string tier_str = name.substr(tier_start_offset, close_offset - tier_start_offset); + bool tier_str_valid = !tier_str.empty(); + for (char ch : tier_str) { + if ((ch < '0') || (ch > '9')) { + tier_str_valid = false; + break; + } + } + if (tier_str_valid) { + int64_t parsed_tier = stoll(tier_str); + if ((parsed_tier >= 0) && (parsed_tier <= s->blueballz_max_tier)) { + requested_blueballz_tier = parsed_tier; + } + } + } + } + } + + if (requested_blueballz_tier >= 0) { + if (s->enable_blueballz && + is_v4(creator_c->version()) && + (difficulty == Difficulty::ULTIMATE) && + (requested_blueballz_tier <= s->blueballz_unlocked_tier_v4)) { + game->blueballz_tier = requested_blueballz_tier; + game->set_flag(Lobby::Flag::BLUEBALLZ_PLUS0); + game->log.info_f("Blueballz +{} enabled for BB Ultimate game via {}", + static_cast(game->blueballz_tier), + requested_blueballz_source); + } else { + game->log.info_f("Blueballz +{} room prefix ignored; enable={}, version={}, difficulty={}, unlocked_v4={}, max_tier={}", + static_cast(requested_blueballz_tier), + s->enable_blueballz, + static_cast(creator_c->version()), + name_for_difficulty(difficulty), + s->blueballz_unlocked_tier_v4, + s->blueballz_max_tier); + } + } else if (requested_blueballz) { + game->log.info_f("Blueballz room prefix ignored; invalid prefix in room name: {}", name); + } while (game->floor_item_managers.size() < 0x12) { game->floor_item_managers.emplace_back(game->lobby_id, game->floor_item_managers.size()); @@ -4618,6 +5097,15 @@ shared_ptr create_game_generic( battle_player->set_lobby(game); } game->base_exp_multiplier = s->bb_global_exp_multiplier; + if (game->blueballz_tier >= 0) { + float blueballz_exp_multiplier = 1.0f + (static_cast(game->blueballz_tier) * 0.25f); + game->base_exp_multiplier *= blueballz_exp_multiplier; + game->log.info_f("Blueballz +{} EXP multiplier set to {:g}x total (BBGlobalEXPMultiplier={:g}, blueballz={:g})", + static_cast(game->blueballz_tier), + game->base_exp_multiplier, + s->bb_global_exp_multiplier, + blueballz_exp_multiplier); + } game->exp_share_multiplier = s->exp_share_multiplier; const unordered_map* quest_flag_rewrites; diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 1b428d75..0b90b23e 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -2,7 +2,9 @@ #include #include +#include +#include #include #include #include @@ -76,6 +78,184 @@ using SDF = SubcommandDefinition::Flag; extern const vector subcommand_definitions; + + +static string json_escape_for_hardcore_ledger(const string& text) { + string ret; + ret.reserve(text.size() + 16); + for (char ch : text) { + switch (ch) { + case '\\': + ret += "\\\\"; + break; + case '"': + ret += "\\\""; + break; + case '\n': + ret += "\\n"; + break; + case '\r': + ret += "\\r"; + break; + case '\t': + ret += "\\t"; + break; + default: + ret += ch; + } + } + return ret; +} + +static string hardcore_death_character_name_for_ledger(shared_ptr c) { + try { + auto p = c->character_file(false); + if (p) { + string name = p->disp.name.decode(c->language()); + if (!name.empty()) { + return name; + } + } + } catch (const exception&) { + } + return ""; +} + +static string hardcore_death_ledger_filename(shared_ptr c) { + string char_filename = c->character_filename(); + size_t slash_offset = char_filename.find_last_of('/'); + if (slash_offset == string::npos) { + return "hardcore-deaths.jsonl"; + } + return char_filename.substr(0, slash_offset + 1) + "hardcore-deaths.jsonl"; +} + + +static string hardcore_zone_name(shared_ptr c) { + // Basic Episode 1 floor names. Unknown Episode 2/4 floors will fall back safely for now. + switch (c->floor) { + case 0: + return "Pioneer 2"; + case 1: + return "Forest 1"; + case 2: + return "Forest 2"; + case 3: + return "Caves 1"; + case 4: + return "Caves 2"; + case 5: + return "Caves 3"; + case 6: + return "Mines 1"; + case 7: + return "Mines 2"; + case 8: + return "Ruins 1"; + case 9: + return "Ruins 2"; + case 10: + return "Ruins 3"; + case 11: + return "Dragon"; + case 12: + return "De Rol Le"; + case 13: + return "Vol Opt"; + case 14: + return "Dark Falz"; + default: + return std::format("unknown zone {}", c->floor); + } +} + +static bool append_hardcore_death_ledger(shared_ptr c, const char* reason) { + string filename = hardcore_death_ledger_filename(c); + ofstream f(filename, ios::out | ios::app); + if (!f.good()) { + return false; + } + + uint32_t account_id = 0; + string username; + if (c->login) { + if (c->login->account) { + account_id = c->login->account->account_id; + } + if (c->login->bb_license) { + username = c->login->bb_license->username; + } + } + + string character_file = c->character_filename(); + string character_name = hardcore_death_character_name_for_ledger(c); + string zone = hardcore_zone_name(c); + + f << "{" + << "\"time_epoch\":" << static_cast(time(nullptr)) << "," + << "\"account_id\":" << account_id << "," + << "\"username\":\"" << json_escape_for_hardcore_ledger(username) << "\"," + << "\"character_file\":\"" << json_escape_for_hardcore_ledger(character_file) << "\"," + << "\"character_name\":\"" << json_escape_for_hardcore_ledger(character_name) << "\"," + << "\"zone\":\"" << json_escape_for_hardcore_ledger(zone) << "\"," + << "\"reason\":\"" << json_escape_for_hardcore_ledger(reason ? reason : "") << "\"" + << "}\n"; + + return f.good(); +} + +static string bb_hardcore_dead_filename_for_subcommands(shared_ptr c) { + return c->character_filename() + ".hardcore-dead"; +} + +static bool current_ship_is_hardcore_bb(shared_ptr c) { + try { + auto s = c->require_server_state(); + return s->enable_hardcore_mode && (c->version() == Version::BB_V4); + } catch (const exception&) { + return false; + } +} + +static bool mark_bb_character_hardcore_dead_for_subcommands(shared_ptr c, const char* reason) { + string filename = bb_hardcore_dead_filename_for_subcommands(c); + + bool already_dead = false; + { + ifstream existing_f(filename); + already_dead = existing_f.good(); + } + + ofstream f(filename, ios::out | ios::trunc); + if (!f.good()) { + return false; + } + + uint32_t account_id = 0; + if (c->login && c->login->account) { + account_id = c->login->account->account_id; + } + + f << "status=hardcore-dead\n"; + f << "reason=" << reason << "\n"; + f << "account_id=" << account_id << "\n"; + f << "character_file=" << c->character_filename() << "\n"; + + bool ok = f.good(); + f.close(); + + if (ok && !already_dead) { + if (append_hardcore_death_ledger(c, reason)) { + c->log.info_f("Hardcore permadeath: appended death ledger entry for {}", c->character_filename()); + } else { + c->log.warning_f("Hardcore permadeath: FAILED to append death ledger entry for {}", c->character_filename()); + } + + } + + return ok; +} + const SubcommandDefinition* def_for_subcommand(Version version, uint8_t subcommand) { static bool populated = false; static std::array nte_defs; @@ -1659,7 +1839,21 @@ static asio::awaitable on_player_died(shared_ptr c, SubcommandMess } catch (const out_of_range&) { } + bool hardcore_death = current_ship_is_hardcore_bb(c); + if (hardcore_death) { + if (mark_bb_character_hardcore_dead_for_subcommands(c, "player-death-subcommand")) { + c->log.warning_f("Hardcore permadeath: marked BB character dead: {}", c->character_filename()); + } else { + c->log.error_f("Hardcore permadeath: FAILED to mark BB character dead: {}", c->character_filename()); + } + } + forward_subcommand(c, msg); + + if (hardcore_death) { + send_message_box(c, "DADDY FALZ WINS"); + c->channel->disconnect(); + } } static asio::awaitable on_player_revivable(shared_ptr c, SubcommandMessage& msg) { @@ -1670,8 +1864,23 @@ static asio::awaitable on_player_revivable(shared_ptr c, Subcomman co_return; } + bool hardcore_death = current_ship_is_hardcore_bb(c); + if (hardcore_death) { + if (mark_bb_character_hardcore_dead_for_subcommands(c, "player-revivable-subcommand")) { + c->log.warning_f("Hardcore permadeath: marked BB character dead from revivable state: {}", c->character_filename()); + } else { + c->log.error_f("Hardcore permadeath: FAILED to mark BB character dead from revivable state: {}", c->character_filename()); + } + } + forward_subcommand(c, msg); + if (hardcore_death) { + send_message_box(c, "DADDY FALZ WINS"); + c->channel->disconnect(); + co_return; + } + // Revive if infinite HP is enabled bool player_cheats_enabled = l->check_flag(Lobby::Flag::CHEATS_ENABLED) || (c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)); @@ -1694,6 +1903,17 @@ static asio::awaitable on_player_revived(shared_ptr c, SubcommandM auto l = c->require_lobby(); if (l->is_game()) { + if (current_ship_is_hardcore_bb(c)) { + if (mark_bb_character_hardcore_dead_for_subcommands(c, "player-revived-subcommand")) { + c->log.warning_f("Hardcore permadeath: blocked revive and marked BB character dead: {}", c->character_filename()); + } else { + c->log.error_f("Hardcore permadeath: FAILED to mark BB character dead while blocking revive: {}", c->character_filename()); + } + send_message_box(c, "DADDY FALZ WINS"); + c->channel->disconnect(); + co_return; + } + forward_subcommand(c, msg); if ((l->check_flag(Lobby::Flag::CHEATS_ENABLED) || (c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE))) && c->check_flag(Client::Flag::INFINITE_HP_ENABLED)) { diff --git a/src/SendCommands.cc b/src/SendCommands.cc index c850c239..b2334249 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1,4 +1,5 @@ #include "SendCommands.hh" +#include "BattleParamsIndex.hh" #include #include @@ -690,6 +691,68 @@ void send_guild_card_chunk_bb(shared_ptr c, size_t chunk_index) { send_command(c, 0x02DC, 0x00000000, &cmd, sizeof(cmd) - sizeof(cmd.data) + data_size); } + +static bool is_battle_param_stream_file_for_blueballz(const string& filename) { + return (filename == "BattleParamEntry.dat") || + (filename == "BattleParamEntry_on.dat") || + (filename == "BattleParamEntry_lab.dat") || + (filename == "BattleParamEntry_lab_on.dat") || + (filename == "BattleParamEntry_ep4.dat") || + (filename == "BattleParamEntry_ep4_on.dat"); +} + +static shared_ptr bb_stream_file_data_for_client(shared_ptr c, const string& filename) { + auto s = c->require_server_state(); + + auto raw_data = s->bb_stream_files_cache->get_or_load("system/blueburst/" + filename).file->data; + + int64_t effective_blueballz_hp_scale_tier = (c->selected_blueballz_tier >= 0) + ? c->selected_blueballz_tier + : s->blueballz_enemy_hp_scale_tier; + + if (!is_battle_param_stream_file_for_blueballz(filename) || (effective_blueballz_hp_scale_tier < 0)) { + return raw_data; + } + + effective_blueballz_hp_scale_tier = std::min( + s->blueballz_max_tier, + effective_blueballz_hp_scale_tier); + + string scaled_data = *raw_data; + if (scaled_data.size() < sizeof(BattleParamsIndex::Table)) { + c->log.warning_f("Blueballz enemy HP scaling skipped for {}; file is too small", filename); + return raw_data; + } + + double mult = 1.0 + (static_cast(effective_blueballz_hp_scale_tier) * 0.25); + auto* table = reinterpret_cast(scaled_data.data()); + size_t ultimate_index = static_cast(Difficulty::ULTIMATE); + + auto scale_u16 = [mult](uint32_t v) -> uint16_t { + if (v == 0) { + return 0; + } + uint32_t scaled = static_cast(static_cast(v) * mult); + if (scaled < 1) { + scaled = 1; + } + if (scaled > 0xFFFF) { + scaled = 0xFFFF; + } + return static_cast(scaled); + }; + + for (size_t z = 0; z < 0x60; z++) { + auto& stats = table->stats[ultimate_index][z]; + stats.char_stats.hp = scale_u16(stats.char_stats.hp); + } + + c->log.info_f("Blueballz enemy HP scaling: serving {} with tier {} ({:g}x Ultimate HP)", + filename, effective_blueballz_hp_scale_tier, mult); + + return make_shared(std::move(scaled_data)); +} + static const vector stream_file_entries = { "ItemMagEdit.prs", "ItemPMT.prs", @@ -705,24 +768,15 @@ static const vector stream_file_entries = { void send_stream_file_index_bb(shared_ptr c) { auto s = c->require_server_state(); + c->log.info_f("PSO Peeps BBZ stream debug: send_stream_file_index_bb called"); + vector entries; size_t offset = 0; for (const string& filename : stream_file_entries) { - string key = "system/blueburst/" + filename; - auto cache_res = s->bb_stream_files_cache->get_or_load(key); + auto file_data = bb_stream_file_data_for_client(c, filename); auto& e = entries.emplace_back(); - e.size = cache_res.file->data->size(); - // Computing the checksum can be slow, so we cache it along with the file data. If the cache result was just - // populated, then it may be different, so we always recompute the checksum in that case. - if (cache_res.generate_called) { - e.checksum = crc32(cache_res.file->data->data(), e.size); - s->bb_stream_files_cache->replace_obj(key + ".crc32", e.checksum); - } else { - auto compute_checksum = [&](const string&) -> uint32_t { - return crc32(cache_res.file->data->data(), e.size); - }; - e.checksum = s->bb_stream_files_cache->get_obj(key + ".crc32", compute_checksum).obj; - } + e.size = file_data->size(); + e.checksum = crc32(file_data->data(), e.size); e.offset = offset; e.filename.encode(filename); offset += e.size; @@ -733,30 +787,27 @@ void send_stream_file_index_bb(shared_ptr c) { void send_stream_file_chunk_bb(shared_ptr c, uint32_t chunk_index) { auto s = c->require_server_state(); - auto cache_result = s->bb_stream_files_cache->get( - "", [&](const string&) -> string { - size_t bytes = 0; - for (const auto& name : stream_file_entries) { - bytes += s->bb_stream_files_cache->get_or_load("system/blueburst/" + name).file->data->size(); - } + c->log.info_f("PSO Peeps BBZ stream debug: send_stream_file_chunk_bb called for chunk {}", chunk_index); - string ret; - ret.reserve(bytes); - for (const auto& name : stream_file_entries) { - ret += *s->bb_stream_files_cache->get_or_load("system/blueburst/" + name).file->data; - } - return ret; - }); - const auto& contents = cache_result.file->data; + size_t total_bytes = 0; + for (const auto& name : stream_file_entries) { + total_bytes += bb_stream_file_data_for_client(c, name)->size(); + } + + string contents; + contents.reserve(total_bytes); + for (const auto& name : stream_file_entries) { + contents += *bb_stream_file_data_for_client(c, name); + } S_StreamFileChunk_BB_02EB chunk_cmd; chunk_cmd.chunk_index = chunk_index; size_t offset = sizeof(chunk_cmd.data) * chunk_index; - if (offset > contents->size()) { + if (offset > contents.size()) { throw runtime_error("client requested chunk beyond end of stream file"); } - size_t bytes = min(contents->size() - offset, sizeof(chunk_cmd.data)); - chunk_cmd.data.assign_range(reinterpret_cast(contents->data() + offset), bytes, 0); + size_t bytes = min(contents.size() - offset, sizeof(chunk_cmd.data)); + chunk_cmd.data.assign_range(reinterpret_cast(contents.data() + offset), bytes, 0); size_t cmd_size = offsetof(S_StreamFileChunk_BB_02EB, data) + bytes; cmd_size = (cmd_size + 3) & ~3; diff --git a/src/ServerState.cc b/src/ServerState.cc index 3d36273a..2a454c28 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -915,6 +915,22 @@ void ServerState::load_config_early() { this->cheat_mode_behavior = parse_behavior_switch("CheatModeBehavior", BehaviorSwitch::OFF_BY_DEFAULT); this->default_switch_assist_enabled = this->config_json->get_bool("EnableSwitchAssistByDefault", false); this->use_game_creator_section_id = this->config_json->get_bool("UseGameCreatorSectionID", false); + this->enable_bb_ship_selection_menu = this->config_json->get_bool("EnableBBShipSelectionMenu", false); + this->enable_blueballz = this->config_json->get_bool("EnableBlueballz", false); + this->enable_hardcore_mode = this->config_json->get_bool("EnableHardcoreMode", false); + this->blueballz_max_tier = std::min(10, std::max(0, this->config_json->get_int("BlueballzMaxTier", 10))); + this->blueballz_unlocked_tier_v2 = std::min( + this->blueballz_max_tier, + std::max(-1, this->config_json->get_int("BlueballzUnlockedTierV2", -1))); + this->blueballz_unlocked_tier_v3 = std::min( + this->blueballz_max_tier, + std::max(-1, this->config_json->get_int("BlueballzUnlockedTierV3", -1))); + this->blueballz_unlocked_tier_v4 = std::min( + this->blueballz_max_tier, + std::max(-1, this->config_json->get_int("BlueballzUnlockedTierV4", -1))); + this->blueballz_enemy_hp_scale_tier = std::min( + this->blueballz_max_tier, + std::max(-1, this->config_json->get_int("BlueballzEnemyHPScaleTier", -1))); this->rare_notifs_enabled_for_client_drops = this->config_json->get_bool("RareNotificationsEnabledForClientDrops", false); this->default_rare_notifs_enabled_v1_v2 = this->config_json->get_bool("RareNotificationsEnabledByDefault", false); this->default_rare_notifs_enabled_v3_v4 = this->default_rare_notifs_enabled_v1_v2; diff --git a/src/ServerState.hh b/src/ServerState.hh index f49fedf0..5f0bf1d2 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -156,7 +156,15 @@ struct ServerState : public std::enable_shared_from_this { BehaviorSwitch cheat_mode_behavior = BehaviorSwitch::OFF_BY_DEFAULT; bool default_switch_assist_enabled = false; bool use_game_creator_section_id = false; + bool enable_bb_ship_selection_menu = false; bool use_psov2_rand_crypt = false; // Used in some tests + bool enable_blueballz = false; + int64_t blueballz_enemy_hp_scale_tier = -1; // -1 = disabled; 0..10 = scale BB enemy HP in stream files + bool enable_hardcore_mode = false; + int8_t blueballz_max_tier = 10; + int8_t blueballz_unlocked_tier_v2 = 0; + int8_t blueballz_unlocked_tier_v3 = 0; + int8_t blueballz_unlocked_tier_v4 = 0; bool use_legacy_item_random_behavior = false; // Used in some tests bool rare_notifs_enabled_for_client_drops = false; bool default_rare_notifs_enabled_v1_v2 = false; diff --git a/system/quests/extermination/q101-bb-e.bin b/system/quests/extermination/q101-bb-e.bin index 219de489980bdea28c07325991b8939313bb76a2..ca7ba49b6d641a442823ad14522741104871fdb4 100644 GIT binary patch literal 8464 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 literal 2576 zcmZ`)SyWSp7QG1$Bt#7Jq%}chP@oVb45A<)lQIZK&?*`s2$C=)66$-UAhLu~C?Fv+ zSPM!(SWY-lCR;#e(NY8yK^q1^F@caka=E!T_vQV(zqi)eYwh8jpYyXlY;*yDXHS6h z_k$@Y$^Z!H1_TWPl7M8u7FbDzNI;0@>U;(ppaYkH`26`qQPNJp0RT~xAi_J2N>!?I z1MomN;E>kNSa`%*DNyhRd*(zhk4)_K{fAmDVi$O z7SXqTChgQbCNk5Xm6M`X0V8P+a@BBLe`~T_ZYmM8YoIcupygE1rKeX1C< zxT`D7Nr{i+D&wsxsjTb#)o~7&BOQ8lvF|cq7Z_HyFO736#@wSzl^?V%i*c1gr;wzc z;~*&qz$u_84os1J0n<_b{&#k+Uch4-oxn#5zNn@~`lc-IL<9U9sjkVafAnTq^CIBUI%kmU%NjX(%YToKfY zZIRQ;9H8-UB)wLmG6*+=uK=L=I07EdRe{1PTrhGn{E{WW1sDz8sqADdH;s#7z=aZb zAbOm|EQupkGY-1LPJq3!${y9We>!iUx+VO=sgkuFw5Wioji$1tPI{)QnoEX30kSwoXt2hwz!5JO^QeAq{9fK|GDIM*jkcP#OkT9Jlg9{G)fH z$Q;?JiR?niyAg|Qqzb97%@&~+u0^2A3v{Uj@StPN;s)Q@}8S_JIwbO#EPwo!iTOMV`ylE;@fpT z@=YYNsN|BO=uwJ1Qj*a09eeT4VrX!|TZE>f`1Ek*PPt!>IsPUjoJ|8O;(Bs4n;p zhgzJNN;`H#RhjIgYjZ=Dc!X_9S@o(}q9}Z1Q?>C6YsUlmM~H{CsQvN|pIc1RO(Mq$ z`;u6e^zc$A=kXJN-)>RUt2m*85c94jejKkcea~%9BvBe(_k(ZeNUqSI4vqhQm@dyR zW4_|(Nn`j7cFxxdVgp}P*`BZ__WOA;CTs4*^Jri6G!eM>@uBqx07II+PntzfsNrHi zTV_9tveZzIll`nDteh=9I&kf2>T=rS)K#K$RRKQwF>8p%47*MFS_K-lWq&%o{rWAe zyTJCh1A;0yff;DfH2H|Crf4?rv zbo+--R}O`xG1C0+4Up3plTgP+zxSl5om{44GZ?EwNtGf;wDgDvqP)}PE@r) zwL|th2@pGE&r<=lYSn`xhQBz2T^QVKC|`m#iw|9K48>E6zHXO%m8@uaNOPqUTj|;t z^WkMPdu*WC`A)=g(Xzj-%V^CD38Yutq=4`eNNZ=WyxM{7!czu=Vt-p#_Vex{rQUT6 z_7^2kx2Q+m2A>jdVNf9J0geZs}8;-&r_gUqh__m@w| zI%9dc$;&`CN5^kLx}nu7zTgZ;|`gN}nX zu+`Y3fMb5yr&;~N0KmLx$m5un zn!7PdB)NHyQCUS6<|?PMe46umnlpWwGtEhfBl{E9{VRGr{Ab9+TJB?VZvzc)z0WE% z1jTt^S7$w+cNMr3h2}>y{PYc>2@hy9nzwK0fPRROfOgOt)CKC$-Yq5VecoWDuJd4Z zTeqswbD>lNaCU<;(QALIv5YdhA;Rv&=7@f4L5>w85^VS>I1a(_$)JYCxCPG{KZZ(nlnz zZoyrl*sx+WMK**pn-`)9&8z=56V9_7wA}Ca!tO4E%`TCqr1#GX=B5{6xFGz>p9!$T z1GtyEio<*&_rfvo4+Tb~_3GU|4w*iay*|{?V!LyYp`-D%kP3$$l78gE%xbZLj5e7~-zw49iS7`xkj$u!fBy;q{{ioV BO>qDK diff --git a/system/quests/extermination/q101-bb.dat b/system/quests/extermination/q101-bb.dat index 05b729f58e13903ecd3e32230b3abb6e0063d22b..990d65783e0aac9ffbfc52bf3a847751d2ce61ff 100644 GIT binary patch literal 17460 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 gL7sSZK$T@!(B&fX(3#kGD!wQVu@Bn1btjl2isd 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 diff --git a/system/quests/solo-story/q001.json b/system/quests/solo-story/q001.json index 5c2a07f4..2c63c085 100644 --- a/system/quests/solo-story/q001.json +++ b/system/quests/solo-story/q001.json @@ -1,3 +1,2 @@ { - "EnabledIf": "!F_0065 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q002.json b/system/quests/solo-story/q002.json index 6842f452..2c63c085 100644 --- a/system/quests/solo-story/q002.json +++ b/system/quests/solo-story/q002.json @@ -1,3 +1,2 @@ { - "EnabledIf": "!F_0067 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q003.json b/system/quests/solo-story/q003.json index 35710c6d..2c63c085 100644 --- a/system/quests/solo-story/q003.json +++ b/system/quests/solo-story/q003.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B && F_01F9", - "EnabledIf": "!F_0069 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q004.json b/system/quests/solo-story/q004.json index 369516d6..2c63c085 100644 --- a/system/quests/solo-story/q004.json +++ b/system/quests/solo-story/q004.json @@ -1,3 +1,2 @@ { - "EnabledIf": "!F_006B || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q005.json b/system/quests/solo-story/q005.json index 26c72a09..2c63c085 100644 --- a/system/quests/solo-story/q005.json +++ b/system/quests/solo-story/q005.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B", - "EnabledIf": "!F_006D || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q006.json b/system/quests/solo-story/q006.json index 33b510e6..2c63c085 100644 --- a/system/quests/solo-story/q006.json +++ b/system/quests/solo-story/q006.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B", - "EnabledIf": "!F_006F || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q007.json b/system/quests/solo-story/q007.json index 72a1c8f8..2c63c085 100644 --- a/system/quests/solo-story/q007.json +++ b/system/quests/solo-story/q007.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B", - "EnabledIf": "!F_0071 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q008.json b/system/quests/solo-story/q008.json index ecbb4f3c..2c63c085 100644 --- a/system/quests/solo-story/q008.json +++ b/system/quests/solo-story/q008.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0071", - "EnabledIf": "!F_0073 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q009.json b/system/quests/solo-story/q009.json index 3b0040c6..2c63c085 100644 --- a/system/quests/solo-story/q009.json +++ b/system/quests/solo-story/q009.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B", - "EnabledIf": "!F_0075 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q010.json b/system/quests/solo-story/q010.json index 47c9a375..2c63c085 100644 --- a/system/quests/solo-story/q010.json +++ b/system/quests/solo-story/q010.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B && F_01F9", - "EnabledIf": "!F_0077 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q011.json b/system/quests/solo-story/q011.json index 32ffc500..2c63c085 100644 --- a/system/quests/solo-story/q011.json +++ b/system/quests/solo-story/q011.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B && F_01F9", - "EnabledIf": "!F_0079 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q012.json b/system/quests/solo-story/q012.json index 3739b481..2c63c085 100644 --- a/system/quests/solo-story/q012.json +++ b/system/quests/solo-story/q012.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0077 && F_0079 && F_007F && F_0085", - "EnabledIf": "!F_007B || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q013.json b/system/quests/solo-story/q013.json index eaa190fb..2c63c085 100644 --- a/system/quests/solo-story/q013.json +++ b/system/quests/solo-story/q013.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_007B", - "EnabledIf": "!F_007D || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q014.json b/system/quests/solo-story/q014.json index 5742ee81..2c63c085 100644 --- a/system/quests/solo-story/q014.json +++ b/system/quests/solo-story/q014.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B && F_01F9", - "EnabledIf": "!F_007F || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q015.json b/system/quests/solo-story/q015.json index 94d235b2..2c63c085 100644 --- a/system/quests/solo-story/q015.json +++ b/system/quests/solo-story/q015.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0077 && F_0079 && F_007F && F_0085", - "EnabledIf": "!F_0081 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q016.json b/system/quests/solo-story/q016.json index 33c59a00..2c63c085 100644 --- a/system/quests/solo-story/q016.json +++ b/system/quests/solo-story/q016.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B && F_0201", - "EnabledIf": "!F_0083 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q017.json b/system/quests/solo-story/q017.json index c661338a..2c63c085 100644 --- a/system/quests/solo-story/q017.json +++ b/system/quests/solo-story/q017.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B && F_01F9", - "EnabledIf": "!F_0085 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q018.json b/system/quests/solo-story/q018.json index 891008e6..2c63c085 100644 --- a/system/quests/solo-story/q018.json +++ b/system/quests/solo-story/q018.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B && F_0201", - "EnabledIf": "!F_0087 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q019.json b/system/quests/solo-story/q019.json index 9302db2e..2c63c085 100644 --- a/system/quests/solo-story/q019.json +++ b/system/quests/solo-story/q019.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B && F_0209", - "EnabledIf": "!F_0089 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q020.json b/system/quests/solo-story/q020.json index 7c133854..2c63c085 100644 --- a/system/quests/solo-story/q020.json +++ b/system/quests/solo-story/q020.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B && F_0201", - "EnabledIf": "!F_008B || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q021.json b/system/quests/solo-story/q021.json index 555adb1c..2c63c085 100644 --- a/system/quests/solo-story/q021.json +++ b/system/quests/solo-story/q021.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_008B", - "EnabledIf": "!F_008D || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q022.json b/system/quests/solo-story/q022.json index 9c5ca148..2c63c085 100644 --- a/system/quests/solo-story/q022.json +++ b/system/quests/solo-story/q022.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B && F_008D && F_0209", - "EnabledIf": "!F_008F || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q023.json b/system/quests/solo-story/q023.json index 3a55cac3..2c63c085 100644 --- a/system/quests/solo-story/q023.json +++ b/system/quests/solo-story/q023.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B && F_006F && F_0209", - "EnabledIf": "!F_0091 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q024.json b/system/quests/solo-story/q024.json index 5ddf23ee..2c63c085 100644 --- a/system/quests/solo-story/q024.json +++ b/system/quests/solo-story/q024.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B && F_0209", - "EnabledIf": "!F_0093 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q025.json b/system/quests/solo-story/q025.json index b2b4010c..2c63c085 100644 --- a/system/quests/solo-story/q025.json +++ b/system/quests/solo-story/q025.json @@ -1,4 +1,2 @@ { - "AvailableIf": "F_0065 && F_0067 && F_006B && F_008D && F_0209", - "EnabledIf": "!F_0095 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)", } diff --git a/system/quests/solo-story/q026.json b/system/quests/solo-story/q026.json index 45683a0e..2c63c085 100644 --- a/system/quests/solo-story/q026.json +++ b/system/quests/solo-story/q026.json @@ -1,3 +1,2 @@ { - "AvailableIf": "F_0073 && (V_NumPlayers == 1)", }