PSO Peeps Start
Docker / Build (push) Has been cancelled

This commit is contained in:
2026-05-01 23:14:17 -04:00
parent 7f68d41bac
commit f05e68492d
42 changed files with 1019 additions and 92 deletions
+2
View File
@@ -140,6 +140,7 @@ public:
std::shared_ptr<Channel> channel;
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> bb_detector_crypt;
ServerBehavior server_behavior;
uint16_t listener_port = 0;
std::unordered_map<std::string, std::function<void()>> 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<Variations> override_variations;
VectorXYZF pos;
uint32_t floor = 0x0F;
+2
View File
@@ -44,6 +44,7 @@ void GameServer::listen(
shared_ptr<Client> GameServer::connect_channel(shared_ptr<Channel> ch, uint16_t port, ServerBehavior initial_state) {
auto c = make_shared<Client>(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<Client> GameServer::create_client(
phosg::TerminalFormat::FG_YELLOW,
phosg::TerminalFormat::FG_GREEN);
auto c = make_shared<Client>(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;
+151
View File
@@ -5,6 +5,7 @@
#include <phosg/Network.hh>
#include <string>
#include <map>
#include <vector>
#include "GameServer.hh"
@@ -54,6 +55,156 @@ HTTPServer::HTTPServer(shared_ptr<ServerState> 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<string, uint64_t> connected_by_version;
map<string, uint64_t> lobby_players_by_version;
map<string, uint64_t> 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>(phosg::JSON::list());
for (const auto& c : this->state->game_server->all_clients()) {
+12 -4
View File
@@ -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<double>(spec.probability) * this->rare_drop_rate_multiplier;
effective_probability = (multiplied_probability >= 4294967296.0)
? 0x100000000ULL
: static_cast<uint64_t>(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);
}
+4
View File
@@ -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<const ItemData::StackLimits> stack_limits;
GameMode mode;
Difficulty difficulty;
+5
View File
@@ -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<double>(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();
}
+2
View File
@@ -81,6 +81,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
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<Lobby> {
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;
+6
View File
@@ -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 {
+15 -3
View File
@@ -1024,8 +1024,15 @@ static asio::awaitable<HandlerResult> S_6x(shared_ptr<Client> c, Channel::Messag
case 0x17: {
const auto& cmd = msg.check_size_t<G_SetEntityPositionAndAngle_6x17>();
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<void> 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();
}
+494 -6
View File
@@ -3,6 +3,7 @@
#include <inttypes.h>
#include <string.h>
#include <fstream>
#include <memory>
#include <phosg/Filesystem.hh>
#include <phosg/Hash.hh>
@@ -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<Client> c) {
return c->character_filename() + ".test-tainted";
}
static string bb_test_taint_grandfather_filename(shared_ptr<Client> 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<Client> c) {
return file_exists_for_bb_taint(bb_test_taint_filename(c));
}
static bool bb_character_is_test_taint_grandfathered(shared_ptr<Client> c) {
return file_exists_for_bb_taint(bb_test_taint_grandfather_filename(c));
}
static bool mark_bb_character_test_tainted(shared_ptr<Client> 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<Client> c) {
return c->character_filename() + ".hardcore";
}
static string bb_hardcore_ineligible_filename(shared_ptr<Client> c) {
return c->character_filename() + ".hardcore-ineligible";
}
static string bb_hardcore_dead_filename(shared_ptr<Client> c) {
return c->character_filename() + ".hardcore-dead";
}
static bool bb_character_is_hardcore(shared_ptr<Client> c) {
return file_exists_for_bb_taint(bb_hardcore_filename(c));
}
static bool bb_character_is_hardcore_ineligible(shared_ptr<Client> c) {
return file_exists_for_bb_taint(bb_hardcore_ineligible_filename(c));
}
static bool bb_character_is_hardcore_dead(shared_ptr<Client> c) {
return file_exists_for_bb_taint(bb_hardcore_dead_filename(c));
}
static bool write_bb_hardcore_marker(shared_ptr<Client> 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<Client> c) {
return write_bb_hardcore_marker(c, bb_hardcore_filename(c), "hardcore", "entered-hardcore-ship");
}
static bool mark_bb_character_hardcore_ineligible(shared_ptr<Client> 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<Client> 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<void> on_connect(std::shared_ptr<Client> c) {
auto s = c->require_server_state();
if (s->default_switch_assist_enabled) {
@@ -42,6 +175,16 @@ asio::awaitable<void> on_connect(std::shared_ptr<Client> 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<Client> c) {
auto s = c->require_server_state();
auto main_menu = make_shared<Menu>(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<Client>(c)]() -> string {
@@ -117,18 +275,70 @@ static void send_main_menu(shared_ptr<Client> 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<int64_t>(
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<uint32_t>(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<void> end_proxy_session(shared_ptr<Client> 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<void> end_proxy_session(shared_ptr<Client> 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<Lobby> l, shared_ptr<const Quest> q, bool substi
static asio::awaitable<void> on_10_main_menu(shared_ptr<Client> 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<void> on_10_main_menu(shared_ptr<Client> 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<void> on_10_main_menu(shared_ptr<Client> 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<void> on_10_proxy_destinations(shared_ptr<Client> 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<void> on_E3_BB(shared_ptr<Client> 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<Lobby> 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<size_t>(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<int>(game->blueballz_tier),
requested_blueballz_source);
} else {
game->log.info_f("Blueballz +{} room prefix ignored; enable={}, version={}, difficulty={}, unlocked_v4={}, max_tier={}",
static_cast<int>(requested_blueballz_tier),
s->enable_blueballz,
static_cast<size_t>(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<Lobby> 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<float>(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<int>(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<uint16_t, IntegralExpression>* quest_flag_rewrites;
+220
View File
@@ -2,7 +2,9 @@
#include <math.h>
#include <string.h>
#include <time.h>
#include <fstream>
#include <memory>
#include <phosg/Random.hh>
#include <phosg/Strings.hh>
@@ -76,6 +78,184 @@ using SDF = SubcommandDefinition::Flag;
extern const vector<SubcommandDefinition> 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<Client> 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<Client> 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<Client> 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<Client> 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<long long>(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<Client> c) {
return c->character_filename() + ".hardcore-dead";
}
static bool current_ship_is_hardcore_bb(shared_ptr<Client> 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<Client> 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<const SubcommandDefinition*, 0x100> nte_defs;
@@ -1659,7 +1839,21 @@ static asio::awaitable<void> on_player_died(shared_ptr<Client> 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<void> on_player_revivable(shared_ptr<Client> c, SubcommandMessage& msg) {
@@ -1670,8 +1864,23 @@ static asio::awaitable<void> on_player_revivable(shared_ptr<Client> 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<void> on_player_revived(shared_ptr<Client> 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)) {
+82 -31
View File
@@ -1,4 +1,5 @@
#include "SendCommands.hh"
#include "BattleParamsIndex.hh"
#include <inttypes.h>
#include <stdarg.h>
@@ -690,6 +691,68 @@ void send_guild_card_chunk_bb(shared_ptr<Client> 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<const string> bb_stream_file_data_for_client(shared_ptr<Client> 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<int64_t>(
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<double>(effective_blueballz_hp_scale_tier) * 0.25);
auto* table = reinterpret_cast<BattleParamsIndex::Table*>(scaled_data.data());
size_t ultimate_index = static_cast<size_t>(Difficulty::ULTIMATE);
auto scale_u16 = [mult](uint32_t v) -> uint16_t {
if (v == 0) {
return 0;
}
uint32_t scaled = static_cast<uint32_t>(static_cast<double>(v) * mult);
if (scaled < 1) {
scaled = 1;
}
if (scaled > 0xFFFF) {
scaled = 0xFFFF;
}
return static_cast<uint16_t>(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<string>(std::move(scaled_data));
}
static const vector<string> stream_file_entries = {
"ItemMagEdit.prs",
"ItemPMT.prs",
@@ -705,24 +768,15 @@ static const vector<string> stream_file_entries = {
void send_stream_file_index_bb(shared_ptr<Client> c) {
auto s = c->require_server_state();
c->log.info_f("PSO Peeps BBZ stream debug: send_stream_file_index_bb called");
vector<S_StreamFileIndexEntry_BB_01EB> 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<uint32_t>(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<uint32_t>(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<Client> c) {
void send_stream_file_chunk_bb(shared_ptr<Client> c, uint32_t chunk_index) {
auto s = c->require_server_state();
auto cache_result = s->bb_stream_files_cache->get(
"<BB stream file>", [&](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<size_t>(contents->size() - offset, sizeof(chunk_cmd.data));
chunk_cmd.data.assign_range(reinterpret_cast<const uint8_t*>(contents->data() + offset), bytes, 0);
size_t bytes = min<size_t>(contents.size() - offset, sizeof(chunk_cmd.data));
chunk_cmd.data.assign_range(reinterpret_cast<const uint8_t*>(contents.data() + offset), bytes, 0);
size_t cmd_size = offsetof(S_StreamFileChunk_BB_02EB, data) + bytes;
cmd_size = (cmd_size + 3) & ~3;
+16
View File
@@ -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<int64_t>(10, std::max<int64_t>(0, this->config_json->get_int("BlueballzMaxTier", 10)));
this->blueballz_unlocked_tier_v2 = std::min<int64_t>(
this->blueballz_max_tier,
std::max<int64_t>(-1, this->config_json->get_int("BlueballzUnlockedTierV2", -1)));
this->blueballz_unlocked_tier_v3 = std::min<int64_t>(
this->blueballz_max_tier,
std::max<int64_t>(-1, this->config_json->get_int("BlueballzUnlockedTierV3", -1)));
this->blueballz_unlocked_tier_v4 = std::min<int64_t>(
this->blueballz_max_tier,
std::max<int64_t>(-1, this->config_json->get_int("BlueballzUnlockedTierV4", -1)));
this->blueballz_enemy_hp_scale_tier = std::min<int64_t>(
this->blueballz_max_tier,
std::max<int64_t>(-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;
+8
View File
@@ -156,7 +156,15 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
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;
Binary file not shown.
Binary file not shown.
-1
View File
@@ -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)",
}
-1
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-1
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-2
View File
@@ -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)",
}
-1
View File
@@ -1,3 +1,2 @@
{
"AvailableIf": "F_0073 && (V_NumPlayers == 1)",
}