initial implementation of BB teams (WIP)

This commit is contained in:
Martin Michelsen
2023-11-18 20:49:18 -08:00
parent 99d1a3272a
commit c1e225847e
21 changed files with 1249 additions and 123 deletions
+1
View File
@@ -105,6 +105,7 @@ add_executable(newserv
src/ServerState.cc src/ServerState.cc
src/Shell.cc src/Shell.cc
src/StaticGameData.cc src/StaticGameData.cc
src/TeamIndex.cc
src/Text.cc src/Text.cc
src/TextArchive.cc src/TextArchive.cc
src/UnicodeTextSet.cc src/UnicodeTextSet.cc
+4 -4
View File
@@ -36,8 +36,8 @@
- Fix some edge cases on the BB proxy server (e.g. Change Ship) - Fix some edge cases on the BB proxy server (e.g. Change Ship)
- Implement less-common subcommands - Implement less-common subcommands
- 6xD8: Add S-rank weapon special - 6xD8: Add S-rank weapon special
- Implement teams - Test team commands
- All EA subcommands - Test all EA subcommands (a few are still not implemented)
- 6xC1, 6xC2, 6xCD, 6xCE: Team invites/administration - 6xC1, 6xC2, 6xCD, 6xCE: Team invites/administration (not implemented)
- 6xCC: Exchange item for team points - 6xCC: Exchange item for team points (not implemented)
- Implement story progress flags for unlocking quests - Implement story progress flags for unlocking quests
+40
View File
@@ -263,6 +263,46 @@ shared_ptr<Lobby> Client::require_lobby() const {
return l; return l;
} }
shared_ptr<TeamIndex::Team> Client::team() {
if (!this->license) {
throw logic_error("Client::team called on client with no license");
}
if (this->license->bb_team_id == 0) {
return nullptr;
}
auto p = this->game_data.character(false);
auto s = this->require_server_state();
auto team = s->team_index->get_by_id(this->license->bb_team_id);
if (!team) {
this->license->bb_team_id = 0;
this->license->save();
return nullptr;
}
auto member_it = team->members.find(this->license->serial_number);
if (member_it == team->members.end()) {
this->license->bb_team_id = 0;
this->license->save();
return nullptr;
}
// The team membership is valid, but the player name may be different; update
// the team membership if needed
if (p) {
auto& m = member_it->second;
string name = p->disp.name.decode(this->language());
if (m.name != name) {
this->log.info("Updating player name in team config");
m.name = name;
team->save_config();
}
}
return team;
}
void Client::dispatch_save_game_data(evutil_socket_t, short, void* ctx) { void Client::dispatch_save_game_data(evutil_socket_t, short, void* ctx) {
reinterpret_cast<Client*>(ctx)->save_game_data(); reinterpret_cast<Client*>(ctx)->save_game_data();
} }
+4
View File
@@ -17,6 +17,7 @@
#include "PatchFileIndex.hh" #include "PatchFileIndex.hh"
#include "Player.hh" #include "Player.hh"
#include "QuestScript.hh" #include "QuestScript.hh"
#include "TeamIndex.hh"
#include "Text.hh" #include "Text.hh"
extern const uint64_t CLIENT_CONFIG_MAGIC; extern const uint64_t CLIENT_CONFIG_MAGIC;
@@ -60,6 +61,7 @@ struct Client : public std::enable_shared_from_this<Client> {
HAS_EP3_MEDIA_UPDATES = 0x0000000010000000, HAS_EP3_MEDIA_UPDATES = 0x0000000010000000,
USE_OVERRIDE_RANDOM_SEED = 0x0000000020000000, USE_OVERRIDE_RANDOM_SEED = 0x0000000020000000,
HAS_GUILD_CARD_NUMBER = 0x0000000040000000, HAS_GUILD_CARD_NUMBER = 0x0000000040000000,
ACCEPTED_TEAM_INVITATION = 0x0000000080000000,
// Cheat mode flags // Cheat mode flags
SWITCH_ASSIST_ENABLED = 0x0000000100000000, SWITCH_ASSIST_ENABLED = 0x0000000100000000,
@@ -238,6 +240,8 @@ struct Client : public std::enable_shared_from_this<Client> {
std::shared_ptr<ServerState> require_server_state() const; std::shared_ptr<ServerState> require_server_state() const;
std::shared_ptr<Lobby> require_lobby() const; std::shared_ptr<Lobby> require_lobby() const;
std::shared_ptr<TeamIndex::Team> team();
static void dispatch_save_game_data(evutil_socket_t, short, void* ctx); static void dispatch_save_game_data(evutil_socket_t, short, void* ctx);
void save_game_data(); void save_game_data();
static void dispatch_send_ping(evutil_socket_t, short, void* ctx); static void dispatch_send_ping(evutil_socket_t, short, void* ctx);
+81 -52
View File
@@ -2901,7 +2901,7 @@ struct S_TournamentEntryList_GC_Ep3_E2 {
} __packed__; } __packed__;
// E2 (S->C): Set system file contents (BB) // E2 (S->C): Set system file contents (BB)
// See PSOBBSystemFile in SaveFileFormats.hh for format // See PSOBBFullSystemFile in SaveFileFormats.hh for format
// E3 (S->C): Game or tournament info (Episode 3) // E3 (S->C): Game or tournament info (Episode 3)
// The header.flag argument determines which fields are valid (and which panes // The header.flag argument determines which fields are valid (and which panes
@@ -3077,7 +3077,7 @@ struct C_CreateSpectatorTeam_GC_Ep3_E7 {
struct SC_SyncSaveFiles_BB_E7 { struct SC_SyncSaveFiles_BB_E7 {
/* 0000 */ PSOBBCharacterFile char_file; /* 0000 */ PSOBBCharacterFile char_file;
/* 2EA4 */ PSOBBSystemFile system_file; /* 2EA4 */ PSOBBFullSystemFile system_file;
/* 3994 */ /* 3994 */
} __packed__; } __packed__;
@@ -3223,9 +3223,16 @@ struct C_CreateTeam_BB_01EA {
pstring<TextEncoding::UTF16, 0x10> name; pstring<TextEncoding::UTF16, 0x10> name;
} __packed__; } __packed__;
// 02EA (S->C): Unknown // 02EA (S->C): Create team result
// header.flag must be in the range [0, 6]. If it isn't, the command is ignored. // No arguments except header.flag, which specifies the error code. Values:
// No other arguments. // 0 = success
// 1 = generic error
// 2 = name already registered
// 3 = generic error
// 4 = generic error
// 5 = generic error
// 6 = generic error
// Anything else = command is ignored
// 03EA (C->S): Add team member // 03EA (C->S): Add team member
@@ -3233,14 +3240,19 @@ struct C_AddOrRemoveTeamMember_BB_03EA_05EA {
le_uint32_t guild_card_number = 0; le_uint32_t guild_card_number = 0;
} __packed__; } __packed__;
// 04EA (S->C): Unknown // 04EA (S->C): Add team member result
// No arguments except header.flag. // No arguments except header.flag, which specifies the error code. Values:
// 0 = success
// 5 = team is full
// Anything else = generic error
// 05EA (C->S): Remove team member // 05EA (C->S): Remove team member
// Same format as 03EA. // Same format as 03EA.
// 06EA (S->C): Delete team? // 06EA (S->C): Remove team member result
// This command behaves exactly like 10EA. // No arguments except header.flag, which specifies the error code. 0 means
// success, but it's not known what any other values mean. The client expects
// the error code to be less than 7.
// 07EA: Team chat // 07EA: Team chat
@@ -3251,20 +3263,19 @@ struct SC_TeamChat_BB_07EA {
// Text follows here // Text follows here
} __packed__; } __packed__;
// 08EA (C->S): Team admin // 08EA (C->S): Get team member list
// No arguments // No arguments
// 09EA (S->C): Unknown // 09EA (S->C): Team member list
struct S_Unknown_BB_09EA { struct S_TeamMemberList_BB_09EA {
le_uint32_t entry_count = 0; le_uint32_t entry_count = 0;
parray<uint8_t, 4> unknown_a2;
struct Entry { struct Entry {
// This is displayed as "<%04d> %s" % (value, message) // This is displayed as "<%04d> %s" % (value, message)
le_uint32_t value = 0; le_uint32_t index = 0;
le_uint32_t color = 0; // 0x10 or 0x20 = green, 0x30 = blue, 0x40 = red, anything else = white le_uint32_t privilege_level = 0; // 0x10 or 0x20 = green, 0x30 = blue, 0x40 = red, anything else = white
le_uint32_t unknown_a1 = 0; le_uint32_t guild_card_number = 0;
pstring<TextEncoding::UTF16, 0x10> message; pstring<TextEncoding::UTF16, 0x10> name;
} __packed__; } __packed__;
// Variable-length field: // Variable-length field:
// Entry entries[entry_count]; // Entry entries[entry_count];
@@ -3291,32 +3302,31 @@ struct S_Unknown_BB_0EEA {
// The client also accepts this command but completely ignores it. // The client also accepts this command but completely ignores it.
struct C_SetTeamFlag_BB_0FEA { struct C_SetTeamFlag_BB_0FEA {
parray<uint8_t, 0x800> data; parray<le_uint16_t, 0x20 * 0x20> flag_data;
} __packed__; } __packed__;
// 10EA: Delete team // 10EA: Delete team result
// No arguments except header.flag // No arguments except header.flag
// 11EA: Promote team member // 11EA: Change team member privilege level
// The format below is used only when the client sends this command; when the // The format below is used only when the client sends this command; when the
// server sends it, only header.flag is used. // server sends it, only header.flag is used.
// TODO: header.flag is used for this command. Figure out what it's for. // header.flag specifies the new privilege level for the specified team member.
struct C_PromoteTeamMember_BB_11EA { struct C_ChangeTeamMemberPrivilegeLevel_BB_11EA {
le_uint32_t unknown_a1 = 0; le_uint32_t guild_card_number = 0;
} __packed__; } __packed__;
// 12EA (S->C): Team membership information // 12EA (S->C): Team membership information
// If the client is not in a team, all fields except guild_card_number should // If the client is not in a team, all fields should be zero.
// be zero.
struct S_TeamMembershipInformation_BB_12EA { struct S_TeamMembershipInformation_BB_12EA {
le_uint32_t unknown_a1 = 0; // Command is ignored unless this is 0 le_uint32_t unknown_a1 = 0;
le_uint32_t guild_card_number = 0; // Team membership ID? le_uint32_t guild_card_number = 0;
le_uint32_t team_id = 0; le_uint32_t team_id = 0;
le_uint32_t unknown_a4 = 0; le_uint32_t unknown_a4 = 0;
le_uint32_t privilege_level = 0; le_uint32_t unknown_a6 = 0;
uint8_t unknown_a6 = 0; uint8_t privilege_level = 0;
uint8_t unknown_a7 = 0; uint8_t unknown_a7 = 0;
uint8_t unknown_a8 = 0; uint8_t unknown_a8 = 0;
uint8_t unknown_a9 = 0; uint8_t unknown_a9 = 0;
@@ -3336,7 +3346,7 @@ struct S_TeamInfoForPlayer_BB_13EA_15EA_Entry {
le_uint32_t guild_card_number2 = 0; le_uint32_t guild_card_number2 = 0;
le_uint32_t lobby_client_id = 0; le_uint32_t lobby_client_id = 0;
pstring<TextEncoding::UTF16, 0x10> player_name; pstring<TextEncoding::UTF16, 0x10> player_name;
parray<uint8_t, 0x800> team_flag; parray<le_uint16_t, 0x20 * 0x20> flag_data;
} __packed__; } __packed__;
// 14EA (C->S): Unknown // 14EA (C->S): Unknown
@@ -3349,32 +3359,51 @@ struct S_TeamInfoForPlayer_BB_13EA_15EA_Entry {
// 16EA (S->C): Unknown // 16EA (S->C): Unknown
// No arguments except header.flag. // No arguments except header.flag.
// 18EA: Membership information // 18EA: Team ranking information
// No arguments (C->S)
// TODO: Document S->C format
struct S_TeamMembershipInformation_BB_18EA {
parray<uint8_t, 0x0C> unknown_a1;
le_uint32_t unknown_a2 = 1;
le_uint32_t unknown_a3 = 1;
le_uint32_t privilege_level = 0;
le_uint32_t guild_card_number = 0;
pstring<TextEncoding::UTF16, 0x10> player_name;
le_uint32_t unknown_a4 = 0;
le_uint32_t unknown_a5 = 2;
} __packed__;
// 19EA: Privilege list
// No arguments (C->S) // No arguments (C->S)
struct S_TeamPrivilegeList_BB_19EA { struct S_TeamRankingInformation_BB_18EA {
le_uint32_t unknown_a1 = 0; /* 0000 */ le_uint32_t unknown_a1 = 0;
/* 0004 */ le_uint32_t unknown_a2 = 0;
/* 0008 */ le_uint32_t unknown_a3 = 0;
/* 000C */ le_uint32_t num_entries = 1;
struct Entry {
/* 00 */ le_uint32_t unknown_a1 = 0;
/* 04 */ le_uint32_t privilege_level = 0;
/* 08 */ le_uint32_t guild_card_number = 0;
/* 0C */ pstring<TextEncoding::UTF16, 0x10> player_name;
/* 2C */ le_uint32_t unknown_a2 = 0;
/* 30 */
} __packed__;
// Variable-length field:
/* 0010 */ // Entry entries[num_entries];
} __packed__; } __packed__;
// 1AEA: Unknown // 19EA: Team reward list
// No arguments (C->S)
// 1BEA (C->S): Unknown struct S_TeamRewardList_BB_19EA {
// header.flag is used, but no other arguments le_uint32_t num_rewards_unlocked = 0;
} __packed__;
// 1AEA: Team rewards available for purchase
struct S_TeamRewardsAvailableForPurchase_BB_1AEA {
le_uint32_t num_entries;
struct Entry {
/* 0000 */ pstring<TextEncoding::UTF16, 0x40> name;
/* 0080 */ pstring<TextEncoding::UTF16, 0x80> description;
/* 0180 */ le_uint32_t team_points = 0;
/* 0184 */ le_uint32_t reward_id = 0;
/* 0188 */
} __packed__;
// Variable length field:
// Entry entries[num_entries];
} __packed__;
// 1BEA (C->S): Buy team reward
// No arguments except header.flag, which specifies a reward_id from a preceding
// 1AEA command.
// 1CEA: Ranking information // 1CEA: Ranking information
// No arguments when sent by the client. // No arguments when sent by the client.
@@ -3389,7 +3418,7 @@ struct C_Unknown_BB_1EEA {
pstring<TextEncoding::UTF16, 0x10> unknown_a1; pstring<TextEncoding::UTF16, 0x10> unknown_a1;
} __packed__; } __packed__;
// 1FEA (S->C): Unknown // 1FEA (S->C): Action result
// This command behaves exactly like 02EA. // This command behaves exactly like 02EA.
// 20EA: Unknown // 20EA: Unknown
+1 -1
View File
@@ -43,7 +43,7 @@ constexpr uint16_t encode_xrgb8888_to_xrgb1555(uint32_t xrgb8888) {
return ((xrgb8888 >> 9) & 0x7C00) | ((xrgb8888 >> 6) & 0x03E0) | ((xrgb8888 >> 3) & 0x001F); return ((xrgb8888 >> 9) & 0x7C00) | ((xrgb8888 >> 6) & 0x03E0) | ((xrgb8888 >> 3) & 0x001F);
} }
constexpr uint32_t encode_rgbx8888_to_xrgb1555(uint16_t rgbx8888) { constexpr uint16_t encode_rgbx8888_to_xrgb1555(uint32_t rgbx8888) {
// In: rrrrrrrrggggggggbbbbbbbbxxxxxxxx // In: rrrrrrrrggggggggbbbbbbbbxxxxxxxx
// Out: -rrrrrgggggbbbbb // Out: -rrrrrgggggbbbbb
return ((rgbx8888 >> 17) & 0x7C00) | ((rgbx8888 >> 14) & 0x03E0) | ((rgbx8888 >> 11) & 0x001F); return ((rgbx8888 >> 17) & 0x7C00) | ((rgbx8888 >> 14) & 0x03E0) | ((rgbx8888 >> 11) & 0x001F);
+4 -1
View File
@@ -14,7 +14,8 @@ License::License(const JSON& json)
flags(0), flags(0),
ban_end_time(0), ban_end_time(0),
ep3_current_meseta(0), ep3_current_meseta(0),
ep3_total_meseta_earned(0) { ep3_total_meseta_earned(0),
bb_team_id(0) {
this->serial_number = json.get_int("SerialNumber"); this->serial_number = json.get_int("SerialNumber");
this->access_key = json.get_string("AccessKey", ""); this->access_key = json.get_string("AccessKey", "");
this->gc_password = json.get_string("GCPassword", ""); this->gc_password = json.get_string("GCPassword", "");
@@ -27,6 +28,7 @@ License::License(const JSON& json)
this->ban_end_time = json.get_int("BanEndTime", 0); this->ban_end_time = json.get_int("BanEndTime", 0);
this->ep3_current_meseta = json.get_int("Ep3CurrentMeseta", 0); this->ep3_current_meseta = json.get_int("Ep3CurrentMeseta", 0);
this->ep3_total_meseta_earned = json.get_int("Ep3TotalMesetaEarned", 0); this->ep3_total_meseta_earned = json.get_int("Ep3TotalMesetaEarned", 0);
this->bb_team_id = json.get_int("BBTeamID", 0);
} }
JSON License::json() const { JSON License::json() const {
@@ -43,6 +45,7 @@ JSON License::json() const {
{"BanEndTime", this->ban_end_time}, {"BanEndTime", this->ban_end_time},
{"Ep3CurrentMeseta", this->ep3_current_meseta}, {"Ep3CurrentMeseta", this->ep3_current_meseta},
{"Ep3TotalMesetaEarned", this->ep3_total_meseta_earned}, {"Ep3TotalMesetaEarned", this->ep3_total_meseta_earned},
{"BBTeamID", this->bb_team_id},
}); });
} }
+2
View File
@@ -45,6 +45,8 @@ struct License {
uint32_t ep3_current_meseta = 0; uint32_t ep3_current_meseta = 0;
uint32_t ep3_total_meseta_earned = 0; uint32_t ep3_total_meseta_earned = 0;
uint32_t bb_team_id = 0;
License() = default; License() = default;
explicit License(const JSON& json); explicit License(const JSON& json);
+17
View File
@@ -76,6 +76,23 @@ constexpr uint32_t EP3_INFINITE_MESETA = 0xAA0F0FAA;
constexpr uint32_t EP3_INFINITE_TIME = 0xAA1010AA; constexpr uint32_t EP3_INFINITE_TIME = 0xAA1010AA;
} // namespace ProxyOptionsMenuItemID } // namespace ProxyOptionsMenuItemID
namespace TeamRewardMenuItemID {
constexpr uint32_t TEAM_FLAG = 0x01010101;
constexpr uint32_t DRESSING_ROOM = 0x02020202;
constexpr uint32_t MEMBERS_20_LEADERS_3 = 0x03030303;
constexpr uint32_t MEMBERS_40_LEADERS_5 = 0x04040404;
constexpr uint32_t MEMBERS_70_LEADERS_8 = 0x05050505;
constexpr uint32_t MEMBERS_100_LEADERS_10 = 0x06060606;
// constexpr uint32_t POINT_OF_DISASTER = ...;
// constexpr uint32_t TOYS_TWILIGHT = ...;
// constexpr uint32_t COMMANDER_BLADE = ...;
// constexpr uint32_t UNION_GUARD = ...;
// constexpr uint32_t TEAM_POINTS_500 = ...;
// constexpr uint32_t TEAM_POINTS_1000 = ...;
// constexpr uint32_t TEAM_POINTS_5000 = ...;
// constexpr uint32_t TEAM_POINTS_10000 = ...;
} // namespace TeamRewardMenuItemID
struct MenuItem { struct MenuItem {
enum Flag { enum Flag {
// For menu items to be visible on DCNTE, they must not have either of the // For menu items to be visible on DCNTE, they must not have either of the
+20 -9
View File
@@ -130,14 +130,14 @@ void ClientGameData::create_challenge_overlay(GameVersion version, size_t templa
} }
} }
shared_ptr<PSOBBSystemFile> ClientGameData::system(bool allow_load) { shared_ptr<PSOBBBaseSystemFile> ClientGameData::system(bool allow_load) {
if (!this->system_data && allow_load) { if (!this->system_data && allow_load) {
this->load_all_files(); this->load_all_files();
} }
return this->system_data; return this->system_data;
} }
shared_ptr<const PSOBBSystemFile> ClientGameData::system(bool allow_load) const { shared_ptr<const PSOBBBaseSystemFile> ClientGameData::system(bool allow_load) const {
if (!this->system_data.get() && allow_load) { if (!this->system_data.get() && allow_load) {
throw runtime_error("system data is not loaded"); throw runtime_error("system data is not loaded");
} }
@@ -236,7 +236,7 @@ void ClientGameData::create_character_file(
void ClientGameData::load_all_files() { void ClientGameData::load_all_files() {
if (this->bb_username.empty()) { if (this->bb_username.empty()) {
this->system_data.reset(new PSOBBSystemFile()); this->system_data.reset(new PSOBBBaseSystemFile());
this->character_data.reset(new PSOBBCharacterFile()); this->character_data.reset(new PSOBBCharacterFile());
this->guild_card_data.reset(new PSOBBGuildCardFile()); this->guild_card_data.reset(new PSOBBGuildCardFile());
return; return;
@@ -248,7 +248,7 @@ void ClientGameData::load_all_files() {
string sys_filename = this->system_filename(); string sys_filename = this->system_filename();
if (isfile(sys_filename)) { if (isfile(sys_filename)) {
this->system_data.reset(new PSOBBSystemFile(load_object_file<PSOBBSystemFile>(sys_filename))); this->system_data.reset(new PSOBBBaseSystemFile(load_object_file<PSOBBBaseSystemFile>(sys_filename, true)));
player_data_log.info("Loaded system data from %s", sys_filename.c_str()); player_data_log.info("Loaded system data from %s", sys_filename.c_str());
} }
@@ -272,7 +272,7 @@ void ClientGameData::load_all_files() {
// If there was no .psosys file, load the system file from the .psochar // If there was no .psosys file, load the system file from the .psochar
// file instead // file instead
if (!this->system_data) { if (!this->system_data) {
this->system_data.reset(new PSOBBSystemFile(freadx<PSOBBSystemFile>(f.get()))); this->system_data.reset(new PSOBBBaseSystemFile(freadx<PSOBBBaseSystemFile>(f.get())));
player_data_log.info("Loaded system data from %s", char_filename.c_str()); player_data_log.info("Loaded system data from %s", char_filename.c_str());
} }
} }
@@ -294,7 +294,7 @@ void ClientGameData::load_all_files() {
throw runtime_error("account data header is incorrect"); throw runtime_error("account data header is incorrect");
} }
if (!this->system_data) { if (!this->system_data) {
this->system_data.reset(new PSOBBSystemFile(nsa_data->system_file)); this->system_data.reset(new PSOBBBaseSystemFile(nsa_data->system_file.base));
player_data_log.info("Loaded legacy system data from %s", nsa_filename.c_str()); player_data_log.info("Loaded legacy system data from %s", nsa_filename.c_str());
} }
if (!this->guild_card_data) { if (!this->guild_card_data) {
@@ -304,7 +304,7 @@ void ClientGameData::load_all_files() {
} }
if (!this->system_data) { if (!this->system_data) {
this->system_data.reset(new PSOBBSystemFile()); this->system_data.reset(new PSOBBBaseSystemFile());
player_data_log.info("Created new system data"); player_data_log.info("Created new system data");
} }
if (!this->guild_card_data) { if (!this->guild_card_data) {
@@ -335,7 +335,6 @@ void ClientGameData::load_all_files() {
this->character_data->bank = nsc_data.bank; this->character_data->bank = nsc_data.bank;
this->character_data->guild_card.guild_card_number = this->guild_card_number; this->character_data->guild_card.guild_card_number = this->guild_card_number;
this->character_data->guild_card.name = nsc_data.disp.name; this->character_data->guild_card.name = nsc_data.disp.name;
this->character_data->guild_card.team_name = this->system_data->team_name;
this->character_data->guild_card.description = nsc_data.guild_card_description; this->character_data->guild_card.description = nsc_data.guild_card_description;
this->character_data->guild_card.present = 1; this->character_data->guild_card.present = 1;
this->character_data->guild_card.language = nsc_data.inventory.language; this->character_data->guild_card.language = nsc_data.inventory.language;
@@ -399,10 +398,22 @@ void ClientGameData::save_character_file() {
string filename = this->character_filename(); string filename = this->character_filename();
auto f = fopen_unique(filename, "wb"); auto f = fopen_unique(filename, "wb");
PSOCommandHeaderBB header = {sizeof(PSOCommandHeaderBB) + sizeof(PSOBBCharacterFile) + sizeof(PSOBBSystemFile), 0x00E7, 0x00000000}; PSOCommandHeaderBB header = {sizeof(PSOCommandHeaderBB) + sizeof(PSOBBCharacterFile) + sizeof(PSOBBBaseSystemFile) + sizeof(PSOBBTeamMembership), 0x00E7, 0x00000000};
fwritex(f.get(), header); fwritex(f.get(), header);
fwritex(f.get(), *this->character_data); fwritex(f.get(), *this->character_data);
fwritex(f.get(), *this->system_data); fwritex(f.get(), *this->system_data);
// TODO: Technically, we should write the actual team membership struct to the
// file here, but that would cause ClientGameData to depend on License, which
// it currently does not. This data doesn't matter at all for correctness
// within newserv, since it ignores this data entirely and instead generates
// the membership struct from the team ID in the License and the team's state.
// So, writing correct data here would mostly be for compatibility with other
// PSO servers. But if the other server is newserv, then this data would be
// used anyway, and if it's not, then it would presumably have a different set
// of teams with a different set of team IDs anyway, so the membership struct
// here would be useless either way.
static const PSOBBTeamMembership empty_membership;
fwritex(f.get(), empty_membership);
player_data_log.info("Saved character file %s", filename.c_str()); player_data_log.info("Saved character file %s", filename.c_str());
} }
+3 -3
View File
@@ -64,8 +64,8 @@ public:
return this->overlay_character_data.get() != nullptr; return this->overlay_character_data.get() != nullptr;
} }
std::shared_ptr<PSOBBSystemFile> system(bool allow_load = true); std::shared_ptr<PSOBBBaseSystemFile> system(bool allow_load = true);
std::shared_ptr<const PSOBBSystemFile> system(bool allow_load = true) const; std::shared_ptr<const PSOBBBaseSystemFile> system(bool allow_load = true) const;
std::shared_ptr<PSOBBCharacterFile> character(bool allow_load = true, bool allow_overlay = true); std::shared_ptr<PSOBBCharacterFile> character(bool allow_load = true, bool allow_overlay = true);
std::shared_ptr<const PSOBBCharacterFile> character(bool allow_load = true, bool allow_overlay = true) const; std::shared_ptr<const PSOBBCharacterFile> character(bool allow_load = true, bool allow_overlay = true) const;
@@ -88,7 +88,7 @@ private:
// The overlay character data is used in battle and challenge modes, when // The overlay character data is used in battle and challenge modes, when
// character data is temporarily replaced in-game. In other play modes and in // character data is temporarily replaced in-game. In other play modes and in
// lobbies, overlay_character_data is null. // lobbies, overlay_character_data is null.
std::shared_ptr<PSOBBSystemFile> system_data; std::shared_ptr<PSOBBBaseSystemFile> system_data;
std::shared_ptr<PSOBBCharacterFile> overlay_character_data; std::shared_ptr<PSOBBCharacterFile> overlay_character_data;
std::shared_ptr<PSOBBCharacterFile> character_data; std::shared_ptr<PSOBBCharacterFile> character_data;
std::shared_ptr<PSOBBGuildCardFile> guild_card_data; std::shared_ptr<PSOBBGuildCardFile> guild_card_data;
+320 -12
View File
@@ -3312,13 +3312,13 @@ static void on_E7_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
p->challenge_records = cmd.char_file.challenge_records; p->challenge_records = cmd.char_file.challenge_records;
p->battle_records = cmd.char_file.battle_records; p->battle_records = cmd.char_file.battle_records;
p->death_count = cmd.char_file.death_count; p->death_count = cmd.char_file.death_count;
*c->game_data.system() = cmd.system_file; *c->game_data.system() = cmd.system_file.base;
} }
static void on_E2_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) { static void on_E2_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
auto& cmd = check_size_t<PSOBBSystemFile>(data); auto& cmd = check_size_t<PSOBBFullSystemFile>(data);
auto sys = c->game_data.system(); auto sys = c->game_data.system();
*sys = cmd; *sys = cmd.base;
c->game_data.save_system_file(); c->game_data.save_system_file();
S_SystemFileCreated_00E1_BB out_cmd = {1}; S_SystemFileCreated_00E1_BB out_cmd = {1};
@@ -4309,15 +4309,323 @@ static void on_EF_Ep3(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
send_ep3_card_auction(l); send_ep3_card_auction(l);
} }
static void on_EA_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string&) { static void on_EA_BB(shared_ptr<Client> c, uint16_t command, uint32_t flag, string& data) {
// TODO: Implement teams. This command has a very large number of subcommands auto s = c->require_server_state();
// (up to 20EA!).
if (command == 0x01EA) { switch (command >> 8) {
send_lobby_message_box(c, "$C6Teams are not supported."); case 0x01: { // Create team
} else if (command == 0x14EA) { const auto& cmd = check_size_t<C_CreateTeam_BB_01EA>(data);
// Do nothing (for now) string team_name = cmd.name.decode(c->language());
} else { if (s->team_index->get_by_name(team_name)) {
throw invalid_argument("unimplemented team command"); send_command(c, 0x02EA, 0x00000002);
} else if (c->license->bb_team_id != 0) {
// TODO: What's the right error code to use here?
send_command(c, 0x02EA, 0x00000001);
} else {
string player_name = c->game_data.character()->disp.name.decode(c->language());
auto team = s->team_index->create(team_name, c->license->serial_number, player_name);
c->license->bb_team_id = team->team_id;
c->license->save();
send_command(c, 0x02EA, 0x00000000);
send_update_team_metadata_for_client(c);
send_team_membership_info(c);
send_update_team_reward_flags(c);
}
break;
}
case 0x03: { // Add team member
auto team = c->team();
if (team && team->members.at(c->license->serial_number).privilege_level() >= 0x30) {
const auto& cmd = check_size_t<C_AddOrRemoveTeamMember_BB_03EA_05EA>(data);
auto s = c->require_server_state();
shared_ptr<Client> added_c;
try {
added_c = s->find_client(nullptr, cmd.guild_card_number);
} catch (const out_of_range&) {
send_lobby_message_box(c, "Player is offline");
}
if (added_c) {
auto added_c_team = added_c->team();
if (added_c_team) {
send_lobby_message_box(c, "Player is already\nin another team");
} else if (!added_c->config.check_flag(Client::Flag::ACCEPTED_TEAM_INVITATION)) {
send_lobby_message_box(c, "Player did not accept\nteam invitation");
} else if (!team->can_add_member()) {
// Send "team is full" error
send_command(c, 0x04EA, 0x00000005);
send_command(added_c, 0x04EA, 0x00000005);
} else {
added_c->license->bb_team_id = team->team_id;
added_c->license->save();
added_c->config.clear_flag(Client::Flag::ACCEPTED_TEAM_INVITATION);
send_update_team_metadata_for_client(added_c);
send_team_membership_info(added_c);
send_command(c, 0x04EA, 0x00000000);
send_command(added_c, 0x04EA, 0x00000000);
}
}
}
break;
}
case 0x05: { // Remove team member
auto team = c->team();
if (team) {
const auto& cmd = check_size_t<C_AddOrRemoveTeamMember_BB_03EA_05EA>(data);
bool is_removing_self = (cmd.guild_card_number == c->license->serial_number);
if (is_removing_self ||
(team->members.at(c->license->serial_number).privilege_level() >= 0x30)) {
s->team_index->remove_member(cmd.guild_card_number);
auto removed_license = s->license_index->get(cmd.guild_card_number);
removed_license->bb_team_id = 0;
removed_license->save();
send_command(c, 0x06EA, 0x00000000);
shared_ptr<Client> removed_c;
if (is_removing_self) {
removed_c = c;
} else {
try {
removed_c = s->find_client(nullptr, cmd.guild_card_number);
} catch (const out_of_range&) {
}
}
if (removed_c) {
send_update_team_metadata_for_client(removed_c);
send_team_membership_info(removed_c);
}
} else {
// TODO: Figure out the right error code to use here.
send_command(c, 0x06EA, 0x00000001);
}
}
break;
}
case 0x07: { // Team chat
auto team = c->team();
if (team) {
check_size_v(data.size(), sizeof(SC_TeamChat_BB_07EA) + 4);
static const string required_end("\0\0", 2);
if (ends_with(data, required_end)) {
for (const auto& it : team->members) {
try {
auto target_c = s->find_client(nullptr, it.second.serial_number);
send_command(target_c, 0x07EA, 0x00000000, data);
} catch (const out_of_range&) {
}
}
}
}
break;
}
case 0x08:
send_team_member_list(c);
break;
case 0x0D: {
auto team = c->team();
if (team) {
S_Unknown_BB_0EEA cmd;
cmd.team_name.encode(team->name, c->language());
send_command_t(c, 0x0EEA, 0x00000000, cmd);
} else {
throw runtime_error("client is not in a team");
}
break;
}
case 0x0F: { // Set team flag
auto team = c->team();
if (team && team->members.at(c->license->serial_number).check_flag(TeamIndex::Team::Member::Flag::IS_MASTER)) {
const auto& cmd = check_size_t<C_SetTeamFlag_BB_0FEA>(data);
team->flag_data.reset(new parray<le_uint16_t, 0x20 * 0x20>(cmd.flag_data));
team->save_flag();
for (const auto& it : team->members) {
try {
auto member_c = s->find_client(nullptr, it.second.serial_number);
send_update_team_metadata_for_client(member_c);
} catch (const out_of_range&) {
}
}
}
break;
}
case 0x10: { // Disband team
auto team = c->team();
if (team && team->members.at(c->license->serial_number).check_flag(TeamIndex::Team::Member::Flag::IS_MASTER)) {
s->team_index->disband(team->team_id);
send_command(c, 0x10EA, 0x00000000);
for (const auto& it : team->members) {
try {
auto member_c = s->find_client(nullptr, it.second.serial_number);
send_update_team_metadata_for_client(member_c);
send_team_membership_info(member_c);
} catch (const out_of_range&) {
}
}
}
break;
}
case 0x11: { // Change member privilege level
auto team = c->team();
if (team) {
auto& cmd = check_size_t<C_ChangeTeamMemberPrivilegeLevel_BB_11EA>(data);
if (cmd.guild_card_number == c->license->serial_number) {
throw runtime_error("this command cannot be used to modify your own permissions");
}
auto& this_m = team->members.at(c->license->serial_number);
if (!this_m.check_flag(TeamIndex::Team::Member::Flag::IS_MASTER)) {
break;
}
auto& other_m = team->members.at(cmd.guild_card_number);
// The client only sends this command with flag = 0x00, 0x30, or 0x40
bool send_updates_for_this_m = false;
bool send_updates_for_other_m = false;
switch (flag) {
case 0x00: // Demote member
if (other_m.check_flag(TeamIndex::Team::Member::Flag::IS_LEADER)) {
other_m.clear_flag(TeamIndex::Team::Member::Flag::IS_LEADER);
send_updates_for_other_m = true;
} else {
send_command(c, 0x11EA, 0x00000005);
}
break;
case 0x30: // Promote member
if (!other_m.check_flag(TeamIndex::Team::Member::Flag::IS_LEADER) && team->can_promote_leader()) {
other_m.set_flag(TeamIndex::Team::Member::Flag::IS_LEADER);
send_updates_for_other_m = true;
} else {
send_command(c, 0x11EA, 0x00000005);
}
break;
case 0x40: // Transfer master
this_m.clear_flag(TeamIndex::Team::Member::Flag::IS_MASTER);
this_m.set_flag(TeamIndex::Team::Member::Flag::IS_LEADER);
other_m.clear_flag(TeamIndex::Team::Member::Flag::IS_LEADER);
other_m.set_flag(TeamIndex::Team::Member::Flag::IS_MASTER);
send_updates_for_this_m = true;
send_updates_for_other_m = true;
break;
default:
throw runtime_error("invalid privilege level");
}
if (send_updates_for_this_m) {
send_update_team_metadata_for_client(c);
send_team_membership_info(c);
}
if (send_updates_for_other_m) {
try {
auto other_c = s->find_client(nullptr, cmd.guild_card_number);
send_update_team_metadata_for_client(c);
send_team_membership_info(c);
} catch (const out_of_range&) {
}
}
}
break;
}
case 0x13:
send_all_nearby_team_metadatas_to_client(c, true);
break;
case 0x14:
break;
case 0x18: // Ranking information
send_team_rank_info(c);
break;
case 0x19: {
S_TeamRewardList_BB_19EA cmd = {0};
send_command_t(c, 0x19EA, 0x00000000, cmd);
break;
}
case 0x1A: // Get team rewards available for purchase
send_team_rewards_available_for_purchase(c);
break;
case 0x1B: { // Buy team reward
auto team = c->team();
if (team) {
check_size_v(data.size(), 0); // No data should be sent
bool should_send_update_reward_flags = false;
switch (flag) {
case TeamRewardMenuItemID::TEAM_FLAG:
team->set_reward_flag(TeamIndex::Team::RewardFlag::TEAM_FLAG);
should_send_update_reward_flags = true;
break;
case TeamRewardMenuItemID::DRESSING_ROOM:
team->set_reward_flag(TeamIndex::Team::RewardFlag::DRESSING_ROOM);
should_send_update_reward_flags = true;
break;
case TeamRewardMenuItemID::MEMBERS_20_LEADERS_3:
if (team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_20_LEADERS_3) ||
team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_40_LEADERS_5) ||
team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_70_LEADERS_8) ||
team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_100_LEADERS_10)) {
throw runtime_error("team size upgrades purchased out of order");
}
team->set_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_20_LEADERS_3);
should_send_update_reward_flags = true;
break;
case TeamRewardMenuItemID::MEMBERS_40_LEADERS_5:
if (!team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_20_LEADERS_3) ||
team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_40_LEADERS_5) ||
team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_70_LEADERS_8) ||
team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_100_LEADERS_10)) {
throw runtime_error("team size upgrades purchased out of order");
}
team->set_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_40_LEADERS_5);
should_send_update_reward_flags = true;
break;
case TeamRewardMenuItemID::MEMBERS_70_LEADERS_8:
if (!team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_20_LEADERS_3) ||
!team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_40_LEADERS_5) ||
team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_70_LEADERS_8) ||
team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_100_LEADERS_10)) {
throw runtime_error("team size upgrades purchased out of order");
}
team->set_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_70_LEADERS_8);
should_send_update_reward_flags = true;
break;
case TeamRewardMenuItemID::MEMBERS_100_LEADERS_10:
if (!team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_20_LEADERS_3) ||
!team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_40_LEADERS_5) ||
!team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_70_LEADERS_8) ||
team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_100_LEADERS_10)) {
throw runtime_error("team size upgrades purchased out of order");
}
team->set_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_100_LEADERS_10);
should_send_update_reward_flags = true;
break;
// TODO: Implement all the following cases
// case POINT_OF_DISASTER:
// case TOYS_TWILIGHT:
// case COMMANDER_BLADE:
// case UNION_GUARD:
// case TEAM_POINTS_500:
// case TEAM_POINTS_1000:
// case TEAM_POINTS_5000:
// case TEAM_POINTS_10000:
default:
throw runtime_error("incorrect team reward ID");
}
if (should_send_update_reward_flags) {
for (const auto& it : team->members) {
try {
auto member_c = s->find_client(nullptr, it.second.serial_number);
send_update_team_reward_flags(member_c);
} catch (const out_of_range&) {
}
}
}
}
break;
}
case 0x1C:
throw runtime_error("team subcommand is not yet implemented");
default:
throw runtime_error("invalid team command");
} }
} }
+7 -13
View File
@@ -197,20 +197,14 @@ uint32_t PSOBBGuildCardFile::checksum() const {
return crc32(this, sizeof(*this)); return crc32(this, sizeof(*this));
} }
PSOBBSystemFile::PSOBBSystemFile() PSOBBBaseSystemFile::PSOBBBaseSystemFile() {
: guild_card_number(0),
team_id(0),
team_info(0),
team_privilege_level(0),
reserved(0),
team_rewards(0) {
// This field is based on 1/1/2000, not 1/1/1970, so adjust appropriately // This field is based on 1/1/2000, not 1/1/1970, so adjust appropriately
this->base.creation_timestamp = (now() - 946684800000000ULL) / 1000000; this->base.creation_timestamp = (now() - 946684800000000ULL) / 1000000;
for (size_t z = 0; z < PSOBBSystemFile::DEFAULT_KEY_CONFIG.size(); z++) { for (size_t z = 0; z < PSOBBBaseSystemFile::DEFAULT_KEY_CONFIG.size(); z++) {
this->key_config[z] = PSOBBSystemFile::DEFAULT_KEY_CONFIG[z]; this->key_config[z] = PSOBBBaseSystemFile::DEFAULT_KEY_CONFIG[z];
} }
for (size_t z = 0; z < PSOBBSystemFile::DEFAULT_JOYSTICK_CONFIG.size(); z++) { for (size_t z = 0; z < PSOBBBaseSystemFile::DEFAULT_JOYSTICK_CONFIG.size(); z++) {
this->joystick_config[z] = PSOBBSystemFile::DEFAULT_JOYSTICK_CONFIG[z]; this->joystick_config[z] = PSOBBBaseSystemFile::DEFAULT_JOYSTICK_CONFIG[z];
} }
} }
@@ -448,7 +442,7 @@ const std::array<uint16_t, 20> PSOBBCharacterFile::DEFAULT_TECH_MENU_CONFIG = {
0x0000, 0x0006, 0x0003, 0x0001, 0x0007, 0x0004, 0x0002, 0x0008, 0x0005, 0x0009, 0x0000, 0x0006, 0x0003, 0x0001, 0x0007, 0x0004, 0x0002, 0x0008, 0x0005, 0x0009,
0x0012, 0x000F, 0x0010, 0x0011, 0x000D, 0x000A, 0x000B, 0x000C, 0x000E, 0x0000}; 0x0012, 0x000F, 0x0010, 0x0011, 0x000D, 0x000A, 0x000B, 0x000C, 0x000E, 0x0000};
const std::array<uint8_t, 0x016C> PSOBBSystemFile::DEFAULT_KEY_CONFIG = { const std::array<uint8_t, 0x016C> PSOBBBaseSystemFile::DEFAULT_KEY_CONFIG = {
0x00, 0x00, 0x00, 0x00, 0x5E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@@ -473,7 +467,7 @@ const std::array<uint8_t, 0x016C> PSOBBSystemFile::DEFAULT_KEY_CONFIG = {
0x00, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00}; 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00};
const std::array<uint8_t, 0x0038> PSOBBSystemFile::DEFAULT_JOYSTICK_CONFIG = { const std::array<uint8_t, 0x0038> PSOBBBaseSystemFile::DEFAULT_JOYSTICK_CONFIG = {
0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00,
0x00, 0x00, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
0x08, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
+32 -15
View File
@@ -119,7 +119,7 @@ struct PSOGCEp3SystemFile {
/* 012C */ /* 012C */
} __attribute__((packed)); } __attribute__((packed));
struct PSOBBSystemFileBase { struct PSOBBMinimalSystemFile {
/* 0000 */ be_uint32_t checksum = 0; /* 0000 */ be_uint32_t checksum = 0;
/* 0004 */ be_int16_t music_volume = 0; /* 0004 */ be_int16_t music_volume = 0;
/* 0006 */ int8_t sound_volume = 0; /* 0006 */ int8_t sound_volume = 0;
@@ -132,24 +132,41 @@ struct PSOBBSystemFileBase {
/* 0114 */ /* 0114 */
} __attribute__((packed)); } __attribute__((packed));
struct PSOBBSystemFile { struct PSOBBTeamMembership {
/* 0000 */ PSOBBSystemFileBase base; /* 0000 */ le_uint32_t guild_card_number = 0;
/* 0004 */ le_uint32_t team_id = 0;
/* 0008 */ le_uint32_t unknown_a4 = 0;
/* 000C */ le_uint32_t unknown_a6 = 0;
/* 0010 */ uint8_t privilege_level = 0;
/* 0011 */ uint8_t unknown_a7 = 0;
/* 0012 */ uint8_t unknown_a8 = 0;
/* 0013 */ uint8_t unknown_a9 = 0;
/* 0014 */ pstring<TextEncoding::UTF16, 0x0010> team_name;
/* 0034 */ parray<le_uint16_t, 0x20 * 0x20> flag_data;
/* 0834 */ le_uint32_t reward_flags = 0;
/* 0838 */
PSOBBTeamMembership() = default;
} __attribute__((packed));
struct PSOBBBaseSystemFile {
/* 0000 */ PSOBBMinimalSystemFile base;
/* 0114 */ parray<uint8_t, 0x016C> key_config; /* 0114 */ parray<uint8_t, 0x016C> key_config;
/* 0280 */ parray<uint8_t, 0x0038> joystick_config; /* 0280 */ parray<uint8_t, 0x0038> joystick_config;
/* 02B8 */ le_uint32_t guild_card_number = 0; /* 02B8 */
/* 02BC */ le_uint32_t team_id = 0;
/* 02C0 */ le_uint64_t team_info = 0;
/* 02C8 */ le_uint16_t team_privilege_level = 0;
/* 02CA */ le_uint16_t reserved = 0;
/* 02CC */ pstring<TextEncoding::UTF16, 0x0010> team_name;
/* 02EC */ parray<uint8_t, 0x0800> team_flag;
/* 0AEC */ le_uint32_t team_rewards = 0;
/* 0AF0 */
static const std::array<uint8_t, 0x016C> DEFAULT_KEY_CONFIG; static const std::array<uint8_t, 0x016C> DEFAULT_KEY_CONFIG;
static const std::array<uint8_t, 0x0038> DEFAULT_JOYSTICK_CONFIG; static const std::array<uint8_t, 0x0038> DEFAULT_JOYSTICK_CONFIG;
PSOBBSystemFile(); PSOBBBaseSystemFile();
} __attribute__((packed));
struct PSOBBFullSystemFile {
/* 0000 */ PSOBBBaseSystemFile base;
/* 02B8 */ PSOBBTeamMembership team_membership;
/* 0AF0 */
PSOBBFullSystemFile() = default;
} __attribute__((packed)); } __attribute__((packed));
struct PSOBBCharacterFile { struct PSOBBCharacterFile {
@@ -242,7 +259,7 @@ struct PSOBBGuildCardFile {
void clear(); void clear();
} __attribute__((packed)); } __attribute__((packed));
/* 0000 */ PSOBBSystemFileBase system_file; /* 0000 */ PSOBBMinimalSystemFile system_file;
/* 0114 */ parray<GuildCardBB, 0x1C> blocked; /* 0114 */ parray<GuildCardBB, 0x1C> blocked;
/* 1DF4 */ parray<uint8_t, 0x180> unknown_a2; /* 1DF4 */ parray<uint8_t, 0x180> unknown_a2;
/* 1F74 */ parray<Entry, 0x69> entries; /* 1F74 */ parray<Entry, 0x69> entries;
@@ -758,7 +775,7 @@ struct LegacySavedAccountDataBB { // .nsa file format
/* 0000 */ pstring<TextEncoding::ASCII, 0x40> signature; /* 0000 */ pstring<TextEncoding::ASCII, 0x40> signature;
/* 0040 */ parray<le_uint32_t, 0x001E> blocked_senders; /* 0040 */ parray<le_uint32_t, 0x001E> blocked_senders;
/* 00B8 */ PSOBBGuildCardFile guild_card_file; /* 00B8 */ PSOBBGuildCardFile guild_card_file;
/* D648 */ PSOBBSystemFile system_file; /* D648 */ PSOBBFullSystemFile system_file;
/* E138 */ le_uint32_t unused; /* E138 */ le_uint32_t unused;
/* E13C */ le_uint32_t option_flags; /* E13C */ le_uint32_t option_flags;
/* E140 */ parray<uint8_t, 0x0A40> shortcuts; /* E140 */ parray<uint8_t, 0x0A40> shortcuts;
+246 -10
View File
@@ -470,7 +470,14 @@ void send_client_init_bb(shared_ptr<Client> c, uint32_t error_code) {
} }
void send_system_file_bb(shared_ptr<Client> c) { void send_system_file_bb(shared_ptr<Client> c) {
send_command_t(c, 0x00E2, 0x00000000, *c->game_data.system()); auto team = c->team();
PSOBBFullSystemFile cmd;
cmd.base = *c->game_data.system();
if (team) {
cmd.team_membership = team->membership_for_member(c->license->serial_number);
}
send_command_t(c, 0x00E2, 0x00000000, cmd);
} }
void send_player_preview_bb(shared_ptr<Client> c, int8_t character_index, const PlayerDispDataBBPreview* preview) { void send_player_preview_bb(shared_ptr<Client> c, int8_t character_index, const PlayerDispDataBBPreview* preview) {
@@ -596,12 +603,19 @@ void send_approve_player_choice_bb(shared_ptr<Client> c) {
void send_complete_player_bb(shared_ptr<Client> c) { void send_complete_player_bb(shared_ptr<Client> c) {
auto p = c->game_data.character(true, false); auto p = c->game_data.character(true, false);
auto sys = c->game_data.system(true); auto sys = c->game_data.system(true);
auto team = c->team();
if (c->config.check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB)) { if (c->config.check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB)) {
p->inventory.language = 1; p->inventory.language = 1;
p->guild_card.language = 1; p->guild_card.language = 1;
sys->base.language = 1; sys->base.language = 1;
} }
SC_SyncSaveFiles_BB_E7 cmd = {*p, *sys};
SC_SyncSaveFiles_BB_E7 cmd;
cmd.char_file = *p;
cmd.system_file.base = *sys;
if (team) {
cmd.system_file.team_membership = team->membership_for_member(c->license->serial_number);
}
send_command_t(c, 0x00E7, 0x00000000, cmd); send_command_t(c, 0x00E7, 0x00000000, cmd);
} }
@@ -1103,17 +1117,22 @@ void send_guild_card(shared_ptr<Client> c, shared_ptr<Client> source) {
} }
auto source_p = source->game_data.character(true, false); auto source_p = source->game_data.character(true, false);
uint32_t guild_card_number = source->license->serial_number; auto source_team = source->team();
uint64_t xb_user_id = source->license->xb_user_id uint64_t xb_user_id = source->license->xb_user_id
? source->license->xb_user_id ? source->license->xb_user_id
: (0xAE00000000000000 | guild_card_number); : (0xAE00000000000000ULL | source->license->serial_number);
uint8_t language = source_p->inventory.language;
string name = source_p->disp.name.decode(language);
string description = source_p->guild_card.description.decode(language);
uint8_t section_id = source_p->disp.visual.section_id;
uint8_t char_class = source_p->disp.visual.char_class;
send_guild_card(c->channel, guild_card_number, xb_user_id, name, "", description, language, section_id, char_class); send_guild_card(
c->channel,
source->license->serial_number,
xb_user_id,
source_p->disp.name.decode(source->language()),
source_team ? source_team->name : "",
source_p->guild_card.description.decode(source->language()),
source->language(),
source_p->disp.visual.section_id,
source_p->disp.visual.char_class);
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@@ -1719,6 +1738,10 @@ void send_join_lobby_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cli
send_player_records_t<RecordsT>(c, l, joining_client); send_player_records_t<RecordsT>(c, l, joining_client);
} }
if (c->version() == GameVersion::BB) {
send_all_nearby_team_metadatas_to_client(c, false);
}
uint8_t lobby_type; uint8_t lobby_type;
if (c->config.override_lobby_number != 0x80) { if (c->config.override_lobby_number != 0x80) {
lobby_type = c->config.override_lobby_number; lobby_type = c->config.override_lobby_number;
@@ -3158,3 +3181,216 @@ void send_change_event(shared_ptr<ServerState> s, uint8_t new_event) {
send_change_event(l, new_event); send_change_event(l, new_event);
} }
} }
////////////////////////////////////////////////////////////////////////////////
// BB teams
void send_team_membership_info(shared_ptr<Client> c) {
auto team = c->team();
S_TeamMembershipInformation_BB_12EA cmd;
if (team) {
cmd.guild_card_number = c->license->serial_number;
cmd.team_id = team->team_id;
cmd.privilege_level = team->members.at(c->license->serial_number).privilege_level();
cmd.team_name.encode(team->name);
}
send_command_t(c, 0x12EA, 0x00000000, cmd);
}
static S_TeamInfoForPlayer_BB_13EA_15EA_Entry team_metadata_for_client(shared_ptr<Client> c) {
auto team = c->team();
S_TeamInfoForPlayer_BB_13EA_15EA_Entry cmd;
cmd.lobby_client_id = c->lobby_client_id;
if (team) {
cmd.guild_card_number = c->license->serial_number;
cmd.team_id = team->team_id;
cmd.privilege_level = team->members.at(c->license->serial_number).privilege_level();
cmd.team_name.encode(team->name);
cmd.guild_card_number2 = c->license->serial_number;
cmd.player_name = c->game_data.character()->disp.name;
if (team->flag_data) {
cmd.flag_data = *team->flag_data;
}
}
return cmd;
}
void send_update_team_metadata_for_client(shared_ptr<Client> c) {
auto l = c->require_lobby();
send_command_t(l, 0x15EA, 0x00000001, team_metadata_for_client(c));
}
void send_all_nearby_team_metadatas_to_client(shared_ptr<Client> c, bool is_13EA) {
auto l = c->require_lobby();
vector<S_TeamInfoForPlayer_BB_13EA_15EA_Entry> entries;
entries.reserve(l->count_clients());
for (auto lc : l->clients) {
if (lc) {
entries.emplace_back(team_metadata_for_client(lc));
}
}
send_command_vt(l, is_13EA ? 0x13EA : 0x15EA, entries.size(), entries);
}
void send_update_team_reward_flags(std::shared_ptr<Client> c) {
auto team = c->team();
send_command(c, 0x1DEA, team ? team->reward_flags : 0x00000000);
}
void send_team_member_list(shared_ptr<Client> c) {
auto team = c->team();
if (!team) {
throw runtime_error("client is not in a team");
}
S_TeamMemberList_BB_09EA header;
header.entry_count = team->members.size();
vector<S_TeamMemberList_BB_09EA::Entry> entries;
entries.reserve(header.entry_count);
for (auto& it : team->members) {
auto& m = it.second;
auto& e = entries.emplace_back();
e.index = entries.size();
e.privilege_level = m.privilege_level();
e.guild_card_number = m.serial_number;
e.name.encode(m.name, c->language());
}
send_command_t_vt(c, 0x09EA, 0x00000000, header, entries);
}
void send_team_rank_info(std::shared_ptr<Client> c) {
auto team = c->team();
if (!team) {
throw runtime_error("client is not in a team");
}
S_TeamRankingInformation_BB_18EA cmd;
cmd.num_entries = team->num_members();
vector<S_TeamRankingInformation_BB_18EA::Entry> entries;
auto& e1 = entries.emplace_back();
e1.unknown_a1 = 1;
e1.privilege_level = 0x00;
e1.guild_card_number = 0x55555555;
e1.player_name.encode("TeamRappy");
auto& e2 = entries.emplace_back();
e2.unknown_a1 = 2;
e2.privilege_level = 0x30;
e2.guild_card_number = 0x66666666;
e2.player_name.encode("TeamRappy");
auto& e3 = entries.emplace_back();
e3.unknown_a1 = 3;
e3.privilege_level = 0x40;
e3.guild_card_number = 0x77777777;
e3.player_name.encode("TeamRappy");
// TODO NOCOMMIT: write this function for realz
send_command_t_vt(c, 0x18EA, 0x00000000, cmd, entries);
}
void send_team_rewards_available_for_purchase(std::shared_ptr<Client> c) {
auto team = c->team();
if (!team) {
throw runtime_error("user is not in a team");
}
vector<S_TeamRewardsAvailableForPurchase_BB_1AEA::Entry> entries;
if (!team->check_reward_flag(TeamIndex::Team::RewardFlag::TEAM_FLAG)) {
auto& e = entries.emplace_back();
e.name.encode("Team flag");
e.description.encode("Show a custom banner\nabove your team\'s\nplayers in the lobby");
e.reward_id = TeamRewardMenuItemID::TEAM_FLAG;
e.team_points = 2500;
}
if (!team->check_reward_flag(TeamIndex::Team::RewardFlag::DRESSING_ROOM)) {
auto& e = entries.emplace_back();
e.name.encode("Dressing room");
e.description.encode("Unlock the ability to\nchange your character\'s\nappearance");
e.reward_id = TeamRewardMenuItemID::DRESSING_ROOM;
e.team_points = 3000;
}
if (!team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_20_LEADERS_3)) {
auto& e = entries.emplace_back();
e.name.encode("20 team members");
e.description.encode("Increase your team\'s\nsize limit to 30 members\nand 3 leaders");
e.reward_id = TeamRewardMenuItemID::MEMBERS_20_LEADERS_3;
e.team_points = 1500;
} else if (!team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_40_LEADERS_5)) {
auto& e = entries.emplace_back();
e.name.encode("40 team members");
e.description.encode("Increase your team\'s\nsize limit to 40 members\nand 3 leaders");
e.reward_id = TeamRewardMenuItemID::MEMBERS_40_LEADERS_5;
e.team_points = 4000;
} else if (!team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_70_LEADERS_8)) {
auto& e = entries.emplace_back();
e.name.encode("70 team members");
e.description.encode("Increase your team\'s\nsize limit to 70 members\nand 8 leaders");
e.reward_id = TeamRewardMenuItemID::MEMBERS_70_LEADERS_8;
e.team_points = 9000;
} else if (!team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_100_LEADERS_10)) {
auto& e = entries.emplace_back();
e.name.encode("100 team members");
e.description.encode("Increase your team\'s\nsize limit to 100 members\nand 10 leaders");
e.reward_id = TeamRewardMenuItemID::MEMBERS_100_LEADERS_10;
e.team_points = 18000;
}
// TODO: Implement these. Currently we don't have a good way to conditionally
// unlock quests, and especially not from a team reward flag.
// if (!point_of_disaster_unlocked) {
// auto& e = entries.emplace_back();
// e.name.encode("Quest: Point of Disaster");
// e.description.encode("Unlock the quest\nPoint of Disaster\nfor your team");
// e.team_points = 1000;
// e.reward_id = TeamRewardMenuItemID::POINT_OF_DISASTER;
// }
// if (!toys_twilight_unlocked) {
// auto& e = entries.emplace_back();
// e.name.encode("Quest: Toys Twilight");
// e.description.encode("Unlock the quest\nToys Twilight\nfor your team");
// e.team_points = 1000;
// e.reward_id = TeamRewardMenuItemID::TOYS_TWILIGHT;
// }
// TODO: How should these be implemented? There has to be a way to create
// items in the lobby, presumably...
// auto& e = entries.emplace_back();
// e.name.encode("Commander Blade");
// e.description.encode("Create a Commander\nBlade weapon");
// e.team_points = 8000;
// e.reward_id = TeamRewardMenuItemID::COMMANDER_BLADE;
// auto& e = entries.emplace_back();
// e.name.encode("Union Guard");
// e.description.encode("Create a Union Guard\nshield");
// e.team_points = 100;
// e.reward_id = TeamRewardMenuItemID::UNION_GUARD;
// auto& e = entries.emplace_back();
// e.name.encode("Team Points Ticket 500");
// e.description.encode("Create a 500-point ticket");
// e.team_points = 500;
// e.reward_id = TeamRewardMenuItemID::TEAM_POINTS_500;
// auto& e = entries.emplace_back();
// e.name.encode("Team Points Ticket 1000");
// e.description.encode("Create a 1000-point ticket");
// e.team_points = 1000;
// e.reward_id = TeamRewardMenuItemID::TEAM_POINTS_1000;
// auto& e = entries.emplace_back();
// e.name.encode("Team Points Ticket 5000");
// e.description.encode("Create a 5000-point ticket");
// e.team_points = 5000;
// e.reward_id = TeamRewardMenuItemID::TEAM_POINTS_5000;
// auto& e = entries.emplace_back();
// e.name.encode("Team Points Ticket 10000");
// e.description.encode("Create a 10000-point ticket");
// e.team_points = 10000;
// e.reward_id = TeamRewardMenuItemID::TEAM_POINTS_10000;
S_TeamRewardsAvailableForPurchase_BB_1AEA cmd;
cmd.num_entries = entries.size();
send_command_t_vt(c, 0x1AEA, 0x00000000, cmd, entries);
}
+8
View File
@@ -383,3 +383,11 @@ void send_server_time(std::shared_ptr<Client> c);
void send_change_event(std::shared_ptr<Client> c, uint8_t new_event); void send_change_event(std::shared_ptr<Client> c, uint8_t new_event);
void send_change_event(std::shared_ptr<Lobby> l, uint8_t new_event); void send_change_event(std::shared_ptr<Lobby> l, uint8_t new_event);
void send_change_event(std::shared_ptr<ServerState> s, uint8_t new_event); void send_change_event(std::shared_ptr<ServerState> s, uint8_t new_event);
void send_team_membership_info(std::shared_ptr<Client> c); // 12EA
void send_update_team_metadata_for_client(std::shared_ptr<Client> c); // 15EA (to all clients in lobby, with only c's data)
void send_all_nearby_team_metadatas_to_client(std::shared_ptr<Client> c, bool is_13EA); // 13EA/15EA (to only c, with all lobby clients' data)
void send_update_team_reward_flags(std::shared_ptr<Client> c); // 1DEA
void send_team_member_list(std::shared_ptr<Client> c); // 09EA
void send_team_rank_info(std::shared_ptr<Client> c); // 18EA
void send_team_rewards_available_for_purchase(std::shared_ptr<Client> c); // 1AEA
+2
View File
@@ -288,6 +288,8 @@ Proxy session commands:\n\
for (const string& type : types) { for (const string& type : types) {
if (type == "licenses") { if (type == "licenses") {
this->state->load_licenses(); this->state->load_licenses();
} else if (type == "teams") {
this->state->load_teams();
} else if (type == "patches") { } else if (type == "patches") {
this->state->load_patch_indexes(); this->state->load_patch_indexes();
} else if (type == "battle-params") { } else if (type == "battle-params") {
+7 -3
View File
@@ -98,6 +98,7 @@ void ServerState::init() {
this->parse_config(config, false); this->parse_config(config, false);
this->load_bb_private_keys(); this->load_bb_private_keys();
this->load_licenses(); this->load_licenses();
this->load_teams();
this->load_patch_indexes(); this->load_patch_indexes();
this->load_battle_params(); this->load_battle_params();
this->load_level_table(); this->load_level_table();
@@ -285,7 +286,6 @@ shared_ptr<Client> ServerState::find_client(const std::string* identifier, uint6
} }
} }
// look in the current lobby first
if (l) { if (l) {
try { try {
return l->find_client(identifier, serial_number); return l->find_client(identifier, serial_number);
@@ -293,7 +293,6 @@ shared_ptr<Client> ServerState::find_client(const std::string* identifier, uint6
} }
} }
// look in all lobbies if not found
for (auto& other_l : this->all_lobbies()) { for (auto& other_l : this->all_lobbies()) {
if (l == other_l) { if (l == other_l) {
continue; // don't bother looking again continue; // don't bother looking again
@@ -887,10 +886,15 @@ void ServerState::load_bb_private_keys() {
} }
void ServerState::load_licenses() { void ServerState::load_licenses() {
config_log.info("Loading license list"); config_log.info("Indexing licenses");
this->license_index.reset(new LicenseIndex()); this->license_index.reset(new LicenseIndex());
} }
void ServerState::load_teams() {
config_log.info("Indexing teams");
this->team_index.reset(new TeamIndex("system/teams"));
}
void ServerState::load_patch_indexes() { void ServerState::load_patch_indexes() {
if (isdir("system/patch-pc")) { if (isdir("system/patch-pc")) {
config_log.info("Indexing PSO PC patch files"); config_log.info("Indexing PSO PC patch files");
+3
View File
@@ -22,6 +22,7 @@
#include "Lobby.hh" #include "Lobby.hh"
#include "Menu.hh" #include "Menu.hh"
#include "Quest.hh" #include "Quest.hh"
#include "TeamIndex.hh"
#include "WordSelectTable.hh" #include "WordSelectTable.hh"
// Forward declarations due to reference cycles // Forward declarations due to reference cycles
@@ -141,6 +142,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::vector<Ep3LobbyBannerEntry> ep3_lobby_banners; std::vector<Ep3LobbyBannerEntry> ep3_lobby_banners;
std::shared_ptr<LicenseIndex> license_index; std::shared_ptr<LicenseIndex> license_index;
std::shared_ptr<TeamIndex> team_index;
std::shared_ptr<const Menu> information_menu_v2; std::shared_ptr<const Menu> information_menu_v2;
std::shared_ptr<const Menu> information_menu_v3; std::shared_ptr<const Menu> information_menu_v3;
@@ -232,6 +234,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
void parse_config(const JSON& config_json, bool is_reload); void parse_config(const JSON& config_json, bool is_reload);
void load_bb_private_keys(); void load_bb_private_keys();
void load_licenses(); void load_licenses();
void load_teams();
void load_patch_indexes(); void load_patch_indexes();
void load_battle_params(); void load_battle_params();
void load_level_table(); void load_level_table();
+326
View File
@@ -0,0 +1,326 @@
#include "TeamIndex.hh"
#include <phosg/Filesystem.hh>
#include <phosg/Image.hh>
#include <phosg/Random.hh>
#include "BattleParamsIndex.hh"
#include "GVMEncoder.hh"
#include "ItemData.hh"
#include "Loggers.hh"
#include "StaticGameData.hh"
using namespace std;
TeamIndex::Team::Member::Member(const JSON& json)
: serial_number(json.get_int("SerialNumber")),
flags(json.get_int("Flags", 0)),
points(json.get_int("Points", 0)),
name(json.get_string("Name", "")) {}
JSON TeamIndex::Team::Member::json() const {
return JSON::dict({
{"SerialNumber", this->serial_number},
{"Flags", this->flags},
{"Points", this->points},
{"Name", this->name},
});
}
uint32_t TeamIndex::Team::Member::privilege_level() const {
if (this->check_flag(Member::Flag::IS_MASTER)) {
return 0x40;
} else if (this->check_flag(Member::Flag::IS_LEADER)) {
return 0x30;
} else {
return 0x00;
}
}
TeamIndex::Team::Team(uint32_t team_id) : Team() {
this->team_id = team_id;
}
string TeamIndex::Team::json_filename() const {
return string_printf("system/teams/%08" PRIX32 ".json", this->team_id);
}
string TeamIndex::Team::flag_filename() const {
return string_printf("system/teams/%08" PRIX32 ".bmp", this->team_id);
}
void TeamIndex::Team::load_config() {
auto json = JSON::parse(load_file(this->json_filename()));
this->name = json.get_string("Name");
for (const auto& member_it : json.get_list("Members")) {
Member m(*member_it);
uint32_t serial_number = m.serial_number;
this->members.emplace(serial_number, std::move(m));
}
this->reward_flags = json.get_int("RewardFlags");
}
void TeamIndex::Team::save_config() const {
JSON members_json = JSON::list();
for (const auto& it : this->members) {
members_json.emplace_back(it.second.json());
}
JSON root = JSON::dict({
{"Name", this->name},
{"Members", std::move(members_json)},
{"RewardFlags", this->reward_flags},
});
save_file(this->json_filename(), root.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS));
}
void TeamIndex::Team::load_flag() {
Image img(this->flag_filename());
if (img.get_width() != 32 || img.get_height() != 32) {
throw runtime_error("incorrect flag image dimensions");
}
this->flag_data.reset(new parray<le_uint16_t, 0x20 * 0x20>());
for (size_t y = 0; y < 32; y++) {
for (size_t x = 0; x < 32; x++) {
this->flag_data->at(y * 0x20 + x) = encode_rgbx8888_to_xrgb1555(img.read_pixel(x, y));
}
}
}
void TeamIndex::Team::save_flag() const {
if (!this->flag_data) {
return;
}
Image img(32, 32, false);
for (size_t y = 0; y < 32; y++) {
for (size_t x = 0; x < 32; x++) {
img.write_pixel(x, y, decode_xrgb1555_to_rgba8888(this->flag_data->at(y * 0x20 + x)));
}
}
img.save(this->flag_filename(), Image::Format::WINDOWS_BITMAP);
}
void TeamIndex::Team::delete_files() const {
string json_filename = this->json_filename();
string flag_filename = this->flag_filename();
remove(json_filename.c_str());
remove(flag_filename.c_str());
}
PSOBBTeamMembership TeamIndex::Team::membership_for_member(uint32_t serial_number) const {
const auto& m = this->members.at(serial_number);
PSOBBTeamMembership ret;
ret.guild_card_number = serial_number;
ret.team_id = this->team_id;
ret.unknown_a4 = 0;
ret.privilege_level = m.privilege_level();
ret.unknown_a6 = 0;
ret.unknown_a7 = 0;
ret.unknown_a8 = 0;
ret.unknown_a9 = 0;
ret.team_name.encode("\tE" + this->name);
if (this->flag_data) {
ret.flag_data = *this->flag_data;
} else {
ret.flag_data.clear();
}
ret.reward_flags = this->reward_flags;
return ret;
}
size_t TeamIndex::Team::num_members() const {
return this->members.size();
}
size_t TeamIndex::Team::num_leaders() const {
size_t count = 0;
for (const auto& it : this->members) {
if (it.second.check_flag(Member::Flag::IS_LEADER)) {
count++;
}
}
return count;
}
size_t TeamIndex::Team::max_members() const {
if (this->check_reward_flag(RewardFlag::MEMBERS_100_LEADERS_10)) {
return 100;
} else if (this->check_reward_flag(RewardFlag::MEMBERS_70_LEADERS_8)) {
return 70;
} else if (this->check_reward_flag(RewardFlag::MEMBERS_40_LEADERS_5)) {
return 40;
} else if (this->check_reward_flag(RewardFlag::MEMBERS_20_LEADERS_3)) {
return 20;
} else {
return 10;
}
}
size_t TeamIndex::Team::max_leaders() const {
if (this->check_reward_flag(RewardFlag::MEMBERS_100_LEADERS_10)) {
return 10;
} else if (this->check_reward_flag(RewardFlag::MEMBERS_70_LEADERS_8)) {
return 8;
} else if (this->check_reward_flag(RewardFlag::MEMBERS_40_LEADERS_5)) {
return 5;
} else if (this->check_reward_flag(RewardFlag::MEMBERS_20_LEADERS_3)) {
return 3;
} else {
return 2;
}
}
bool TeamIndex::Team::can_add_member() const {
return this->num_members() < this->max_members();
}
bool TeamIndex::Team::can_promote_leader() const {
return this->num_leaders() < this->max_leaders();
}
TeamIndex::TeamIndex(const string& directory)
: directory(directory),
next_team_id(1) {
if (!isdir(this->directory)) {
mkdir(this->directory.c_str(), 0755);
return;
}
for (const auto& filename : list_directory(this->directory)) {
string file_path = this->directory + "/" + filename;
if (filename == "base.json") {
auto json = JSON::parse(load_file(file_path));
this->next_team_id = json.get_int("NextTeamID");
}
if (ends_with(filename, ".json")) {
try {
uint32_t team_id = stoul(filename.substr(0, filename.size() - 5), nullptr, 16);
shared_ptr<Team> team(new Team(team_id));
team->load_config();
try {
team->load_flag();
} catch (const exception& e) {
static_game_data_log.warning("Failed to load flag for team %08" PRIX32 ": %s", team_id, e.what());
}
this->add_to_indexes(team);
static_game_data_log.info("Indexed team %08" PRIX32 " (%s) (%zu members)", team_id, team->name.c_str(), team->num_members());
} catch (const exception& e) {
static_game_data_log.warning("Failed to index team from %s: %s", filename.c_str(), e.what());
}
}
}
}
size_t TeamIndex::count() const {
return this->id_to_team.size();
}
shared_ptr<TeamIndex::Team> TeamIndex::get_by_id(uint32_t team_id) {
try {
return this->id_to_team.at(team_id);
} catch (const out_of_range&) {
return nullptr;
}
}
shared_ptr<TeamIndex::Team> TeamIndex::get_by_name(const string& name) {
try {
return this->name_to_team.at(name);
} catch (const out_of_range&) {
return nullptr;
}
}
shared_ptr<TeamIndex::Team> TeamIndex::get_by_serial_number(uint32_t serial_number) {
try {
return this->serial_number_to_team.at(serial_number);
} catch (const out_of_range&) {
return nullptr;
}
}
vector<shared_ptr<TeamIndex::Team>> TeamIndex::all() {
vector<shared_ptr<Team>> ret;
for (const auto& it : this->id_to_team) {
ret.emplace_back(it.second);
}
return ret;
}
shared_ptr<TeamIndex::Team> TeamIndex::create(string& name, uint32_t master_serial_number, const string& master_name) {
shared_ptr<Team> team(new Team(this->next_team_id++));
save_file(this->directory + "/base.json", JSON::dict({{"NextTeamID", this->next_team_id}}).serialize());
Team::Member m;
m.serial_number = master_serial_number;
m.flags = 0;
m.points = 0;
m.name = master_name;
m.set_flag(Team::Member::Flag::IS_MASTER);
team->members.emplace(master_serial_number, std::move(m));
team->name = name;
team->save_config();
this->add_to_indexes(team);
return team;
}
void TeamIndex::disband(uint32_t team_id) {
auto team = this->id_to_team.at(team_id);
this->remove_from_indexes(team);
team->delete_files();
}
void TeamIndex::add_member(uint32_t team_id, uint32_t serial_number, const string& name) {
auto team = this->id_to_team.at(team_id);
if (!this->serial_number_to_team.emplace(serial_number, team).second) {
throw runtime_error("user is already in a different team");
}
Team::Member m;
m.serial_number = serial_number;
m.flags = 0;
m.points = 0;
m.name = name;
team->members.emplace(serial_number, std::move(m));
team->save_config();
}
void TeamIndex::remove_member(uint32_t serial_number) {
auto team_it = this->serial_number_to_team.find(serial_number);
if (team_it == this->serial_number_to_team.end()) {
throw runtime_error("client is not in any team");
}
auto team = std::move(team_it->second);
this->serial_number_to_team.erase(team_it);
team->members.erase(serial_number);
if (team->members.empty()) {
this->disband(team->team_id);
} else {
team->save_config();
}
}
void TeamIndex::add_to_indexes(shared_ptr<Team> team) {
if (!this->id_to_team.emplace(team->team_id, team).second) {
throw runtime_error("team ID is already in use");
}
if (!this->name_to_team.emplace(team->name, team).second) {
this->id_to_team.erase(team->team_id);
throw runtime_error("team name is already in use");
}
for (const auto& it : team->members) {
if (!this->serial_number_to_team.emplace(it.second.serial_number, team).second) {
static_game_data_log.warning("Serial number %08" PRIX32 " (%010" PRIu32 ") exists in multiple teams",
it.second.serial_number, it.second.serial_number);
}
}
}
void TeamIndex::remove_from_indexes(shared_ptr<Team> team) {
this->id_to_team.erase(team->team_id);
this->name_to_team.erase(team->name);
for (const auto& it : team->members) {
this->serial_number_to_team.erase(it.second.serial_number);
}
}
+121
View File
@@ -0,0 +1,121 @@
#pragma once
#include <stdint.h>
#include <array>
#include <memory>
#include <phosg/JSON.hh>
#include <random>
#include <string>
#include "ItemNameIndex.hh"
#include "SaveFileFormats.hh"
#include "StaticGameData.hh"
#include "Text.hh"
#include "Version.hh"
class TeamIndex {
public:
struct Team {
struct Member {
enum class Flag {
IS_MASTER = 0x01,
IS_LEADER = 0x02,
};
uint32_t serial_number = 0;
uint8_t flags = 0;
uint64_t points = 0;
std::string name;
Member() = default;
explicit Member(const JSON& json);
JSON json() const;
[[nodiscard]] inline bool check_flag(Flag flag) const {
return !!(static_cast<uint8_t>(flag) & this->flags);
}
inline void set_flag(Flag flag) {
this->flags |= static_cast<uint8_t>(flag);
}
inline void clear_flag(Flag flag) {
this->flags &= (~static_cast<uint8_t>(flag));
}
uint32_t privilege_level() const;
};
enum class RewardFlag {
// Only 0x00000001 and 0x00000002 are used by the client; the rest are
// free to be used however the server chooses.
TEAM_FLAG = 0x00000001,
DRESSING_ROOM = 0x00000002,
MEMBERS_20_LEADERS_3 = 0x00000004,
MEMBERS_40_LEADERS_5 = 0x00000008,
MEMBERS_70_LEADERS_8 = 0x00000010,
MEMBERS_100_LEADERS_10 = 0x00000020,
};
uint32_t team_id = 0;
std::string name;
std::unordered_map<uint32_t, Member> members;
uint32_t reward_flags = 0;
std::shared_ptr<parray<le_uint16_t, 0x20 * 0x20>> flag_data;
Team() = default;
explicit Team(uint32_t team_id);
JSON json() const;
std::string json_filename() const;
std::string flag_filename() const;
void load_config();
void save_config() const;
void load_flag();
void save_flag() const;
void delete_files() const;
PSOBBTeamMembership membership_for_member(uint32_t serial_number) const;
[[nodiscard]] inline bool check_reward_flag(RewardFlag flag) const {
return !!(static_cast<uint8_t>(flag) & this->reward_flags);
}
inline void set_reward_flag(RewardFlag flag) {
this->reward_flags |= static_cast<uint8_t>(flag);
}
inline void clear_reward_flag(RewardFlag flag) {
this->reward_flags &= (~static_cast<uint8_t>(flag));
}
size_t num_members() const;
size_t num_leaders() const;
size_t max_members() const;
size_t max_leaders() const;
bool can_add_member() const;
bool can_promote_leader() const;
};
explicit TeamIndex(const std::string& directory);
~TeamIndex() = default;
size_t count() const;
std::shared_ptr<Team> get_by_id(uint32_t team_id);
std::shared_ptr<Team> get_by_name(const std::string& name);
std::shared_ptr<Team> get_by_serial_number(uint32_t serial_number);
std::vector<std::shared_ptr<Team>> all();
std::shared_ptr<Team> create(std::string& name, uint32_t master_serial_number, const std::string& master_name);
void disband(uint32_t team_id);
void add_member(uint32_t team_id, uint32_t serial_number, const std::string& name);
void remove_member(uint32_t serial_number);
protected:
std::string directory;
uint32_t next_team_id;
std::unordered_map<uint32_t, std::shared_ptr<Team>> id_to_team;
std::unordered_map<std::string, std::shared_ptr<Team>> name_to_team;
std::unordered_map<uint32_t, std::shared_ptr<Team>> serial_number_to_team;
void add_to_indexes(std::shared_ptr<Team> team);
void remove_from_indexes(std::shared_ptr<Team> team);
};