initial implementation of BB teams (WIP)
This commit is contained in:
@@ -105,6 +105,7 @@ add_executable(newserv
|
||||
src/ServerState.cc
|
||||
src/Shell.cc
|
||||
src/StaticGameData.cc
|
||||
src/TeamIndex.cc
|
||||
src/Text.cc
|
||||
src/TextArchive.cc
|
||||
src/UnicodeTextSet.cc
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
- Fix some edge cases on the BB proxy server (e.g. Change Ship)
|
||||
- Implement less-common subcommands
|
||||
- 6xD8: Add S-rank weapon special
|
||||
- Implement teams
|
||||
- All EA subcommands
|
||||
- 6xC1, 6xC2, 6xCD, 6xCE: Team invites/administration
|
||||
- 6xCC: Exchange item for team points
|
||||
- Test team commands
|
||||
- Test all EA subcommands (a few are still not implemented)
|
||||
- 6xC1, 6xC2, 6xCD, 6xCE: Team invites/administration (not implemented)
|
||||
- 6xCC: Exchange item for team points (not implemented)
|
||||
- Implement story progress flags for unlocking quests
|
||||
|
||||
@@ -263,6 +263,46 @@ shared_ptr<Lobby> Client::require_lobby() const {
|
||||
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) {
|
||||
reinterpret_cast<Client*>(ctx)->save_game_data();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "PatchFileIndex.hh"
|
||||
#include "Player.hh"
|
||||
#include "QuestScript.hh"
|
||||
#include "TeamIndex.hh"
|
||||
#include "Text.hh"
|
||||
|
||||
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,
|
||||
USE_OVERRIDE_RANDOM_SEED = 0x0000000020000000,
|
||||
HAS_GUILD_CARD_NUMBER = 0x0000000040000000,
|
||||
ACCEPTED_TEAM_INVITATION = 0x0000000080000000,
|
||||
|
||||
// Cheat mode flags
|
||||
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<Lobby> require_lobby() const;
|
||||
|
||||
std::shared_ptr<TeamIndex::Team> team();
|
||||
|
||||
static void dispatch_save_game_data(evutil_socket_t, short, void* ctx);
|
||||
void save_game_data();
|
||||
static void dispatch_send_ping(evutil_socket_t, short, void* ctx);
|
||||
|
||||
+81
-52
@@ -2901,7 +2901,7 @@ struct S_TournamentEntryList_GC_Ep3_E2 {
|
||||
} __packed__;
|
||||
|
||||
// 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)
|
||||
// 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 {
|
||||
/* 0000 */ PSOBBCharacterFile char_file;
|
||||
/* 2EA4 */ PSOBBSystemFile system_file;
|
||||
/* 2EA4 */ PSOBBFullSystemFile system_file;
|
||||
/* 3994 */
|
||||
} __packed__;
|
||||
|
||||
@@ -3223,9 +3223,16 @@ struct C_CreateTeam_BB_01EA {
|
||||
pstring<TextEncoding::UTF16, 0x10> name;
|
||||
} __packed__;
|
||||
|
||||
// 02EA (S->C): Unknown
|
||||
// header.flag must be in the range [0, 6]. If it isn't, the command is ignored.
|
||||
// No other arguments.
|
||||
// 02EA (S->C): Create team result
|
||||
// No arguments except header.flag, which specifies the error code. Values:
|
||||
// 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
|
||||
|
||||
@@ -3233,14 +3240,19 @@ struct C_AddOrRemoveTeamMember_BB_03EA_05EA {
|
||||
le_uint32_t guild_card_number = 0;
|
||||
} __packed__;
|
||||
|
||||
// 04EA (S->C): Unknown
|
||||
// No arguments except header.flag.
|
||||
// 04EA (S->C): Add team member result
|
||||
// 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
|
||||
// Same format as 03EA.
|
||||
|
||||
// 06EA (S->C): Delete team?
|
||||
// This command behaves exactly like 10EA.
|
||||
// 06EA (S->C): Remove team member result
|
||||
// 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
|
||||
|
||||
@@ -3251,20 +3263,19 @@ struct SC_TeamChat_BB_07EA {
|
||||
// Text follows here
|
||||
} __packed__;
|
||||
|
||||
// 08EA (C->S): Team admin
|
||||
// 08EA (C->S): Get team member list
|
||||
// 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;
|
||||
parray<uint8_t, 4> unknown_a2;
|
||||
struct Entry {
|
||||
// This is displayed as "<%04d> %s" % (value, message)
|
||||
le_uint32_t value = 0;
|
||||
le_uint32_t color = 0; // 0x10 or 0x20 = green, 0x30 = blue, 0x40 = red, anything else = white
|
||||
le_uint32_t unknown_a1 = 0;
|
||||
pstring<TextEncoding::UTF16, 0x10> message;
|
||||
le_uint32_t index = 0;
|
||||
le_uint32_t privilege_level = 0; // 0x10 or 0x20 = green, 0x30 = blue, 0x40 = red, anything else = white
|
||||
le_uint32_t guild_card_number = 0;
|
||||
pstring<TextEncoding::UTF16, 0x10> name;
|
||||
} __packed__;
|
||||
// Variable-length field:
|
||||
// Entry entries[entry_count];
|
||||
@@ -3291,32 +3302,31 @@ struct S_Unknown_BB_0EEA {
|
||||
// The client also accepts this command but completely ignores it.
|
||||
|
||||
struct C_SetTeamFlag_BB_0FEA {
|
||||
parray<uint8_t, 0x800> data;
|
||||
parray<le_uint16_t, 0x20 * 0x20> flag_data;
|
||||
} __packed__;
|
||||
|
||||
// 10EA: Delete team
|
||||
// 10EA: Delete team result
|
||||
// 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
|
||||
// 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 {
|
||||
le_uint32_t unknown_a1 = 0;
|
||||
struct C_ChangeTeamMemberPrivilegeLevel_BB_11EA {
|
||||
le_uint32_t guild_card_number = 0;
|
||||
} __packed__;
|
||||
|
||||
// 12EA (S->C): Team membership information
|
||||
// If the client is not in a team, all fields except guild_card_number should
|
||||
// be zero.
|
||||
// If the client is not in a team, all fields should be zero.
|
||||
|
||||
struct S_TeamMembershipInformation_BB_12EA {
|
||||
le_uint32_t unknown_a1 = 0; // Command is ignored unless this is 0
|
||||
le_uint32_t guild_card_number = 0; // Team membership ID?
|
||||
le_uint32_t unknown_a1 = 0;
|
||||
le_uint32_t guild_card_number = 0;
|
||||
le_uint32_t team_id = 0;
|
||||
le_uint32_t unknown_a4 = 0;
|
||||
le_uint32_t privilege_level = 0;
|
||||
uint8_t unknown_a6 = 0;
|
||||
le_uint32_t unknown_a6 = 0;
|
||||
uint8_t privilege_level = 0;
|
||||
uint8_t unknown_a7 = 0;
|
||||
uint8_t unknown_a8 = 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 lobby_client_id = 0;
|
||||
pstring<TextEncoding::UTF16, 0x10> player_name;
|
||||
parray<uint8_t, 0x800> team_flag;
|
||||
parray<le_uint16_t, 0x20 * 0x20> flag_data;
|
||||
} __packed__;
|
||||
|
||||
// 14EA (C->S): Unknown
|
||||
@@ -3349,32 +3359,51 @@ struct S_TeamInfoForPlayer_BB_13EA_15EA_Entry {
|
||||
// 16EA (S->C): Unknown
|
||||
// No arguments except header.flag.
|
||||
|
||||
// 18EA: Membership 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
|
||||
// 18EA: Team ranking information
|
||||
// No arguments (C->S)
|
||||
|
||||
struct S_TeamPrivilegeList_BB_19EA {
|
||||
le_uint32_t unknown_a1 = 0;
|
||||
struct S_TeamRankingInformation_BB_18EA {
|
||||
/* 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__;
|
||||
|
||||
// 1AEA: Unknown
|
||||
// 19EA: Team reward list
|
||||
// No arguments (C->S)
|
||||
|
||||
// 1BEA (C->S): Unknown
|
||||
// header.flag is used, but no other arguments
|
||||
struct S_TeamRewardList_BB_19EA {
|
||||
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
|
||||
// No arguments when sent by the client.
|
||||
@@ -3389,7 +3418,7 @@ struct C_Unknown_BB_1EEA {
|
||||
pstring<TextEncoding::UTF16, 0x10> unknown_a1;
|
||||
} __packed__;
|
||||
|
||||
// 1FEA (S->C): Unknown
|
||||
// 1FEA (S->C): Action result
|
||||
// This command behaves exactly like 02EA.
|
||||
|
||||
// 20EA: Unknown
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ constexpr uint16_t encode_xrgb8888_to_xrgb1555(uint32_t xrgb8888) {
|
||||
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
|
||||
// Out: -rrrrrgggggbbbbb
|
||||
return ((rgbx8888 >> 17) & 0x7C00) | ((rgbx8888 >> 14) & 0x03E0) | ((rgbx8888 >> 11) & 0x001F);
|
||||
|
||||
+4
-1
@@ -14,7 +14,8 @@ License::License(const JSON& json)
|
||||
flags(0),
|
||||
ban_end_time(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->access_key = json.get_string("AccessKey", "");
|
||||
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->ep3_current_meseta = json.get_int("Ep3CurrentMeseta", 0);
|
||||
this->ep3_total_meseta_earned = json.get_int("Ep3TotalMesetaEarned", 0);
|
||||
this->bb_team_id = json.get_int("BBTeamID", 0);
|
||||
}
|
||||
|
||||
JSON License::json() const {
|
||||
@@ -43,6 +45,7 @@ JSON License::json() const {
|
||||
{"BanEndTime", this->ban_end_time},
|
||||
{"Ep3CurrentMeseta", this->ep3_current_meseta},
|
||||
{"Ep3TotalMesetaEarned", this->ep3_total_meseta_earned},
|
||||
{"BBTeamID", this->bb_team_id},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ struct License {
|
||||
uint32_t ep3_current_meseta = 0;
|
||||
uint32_t ep3_total_meseta_earned = 0;
|
||||
|
||||
uint32_t bb_team_id = 0;
|
||||
|
||||
License() = default;
|
||||
explicit License(const JSON& json);
|
||||
|
||||
|
||||
+17
@@ -76,6 +76,23 @@ constexpr uint32_t EP3_INFINITE_MESETA = 0xAA0F0FAA;
|
||||
constexpr uint32_t EP3_INFINITE_TIME = 0xAA1010AA;
|
||||
} // 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 {
|
||||
enum Flag {
|
||||
// For menu items to be visible on DCNTE, they must not have either of the
|
||||
|
||||
+20
-9
@@ -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) {
|
||||
this->load_all_files();
|
||||
}
|
||||
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) {
|
||||
throw runtime_error("system data is not loaded");
|
||||
}
|
||||
@@ -236,7 +236,7 @@ void ClientGameData::create_character_file(
|
||||
|
||||
void ClientGameData::load_all_files() {
|
||||
if (this->bb_username.empty()) {
|
||||
this->system_data.reset(new PSOBBSystemFile());
|
||||
this->system_data.reset(new PSOBBBaseSystemFile());
|
||||
this->character_data.reset(new PSOBBCharacterFile());
|
||||
this->guild_card_data.reset(new PSOBBGuildCardFile());
|
||||
return;
|
||||
@@ -248,7 +248,7 @@ void ClientGameData::load_all_files() {
|
||||
|
||||
string sys_filename = this->system_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());
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ void ClientGameData::load_all_files() {
|
||||
// If there was no .psosys file, load the system file from the .psochar
|
||||
// file instead
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -294,7 +294,7 @@ void ClientGameData::load_all_files() {
|
||||
throw runtime_error("account data header is incorrect");
|
||||
}
|
||||
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());
|
||||
}
|
||||
if (!this->guild_card_data) {
|
||||
@@ -304,7 +304,7 @@ void ClientGameData::load_all_files() {
|
||||
}
|
||||
|
||||
if (!this->system_data) {
|
||||
this->system_data.reset(new PSOBBSystemFile());
|
||||
this->system_data.reset(new PSOBBBaseSystemFile());
|
||||
player_data_log.info("Created new system 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->guild_card.guild_card_number = this->guild_card_number;
|
||||
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.present = 1;
|
||||
this->character_data->guild_card.language = nsc_data.inventory.language;
|
||||
@@ -399,10 +398,22 @@ void ClientGameData::save_character_file() {
|
||||
|
||||
string filename = this->character_filename();
|
||||
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(), *this->character_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());
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -64,8 +64,8 @@ public:
|
||||
return this->overlay_character_data.get() != nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<PSOBBSystemFile> system(bool allow_load = true);
|
||||
std::shared_ptr<const PSOBBSystemFile> system(bool allow_load = true) const;
|
||||
std::shared_ptr<PSOBBBaseSystemFile> system(bool allow_load = true);
|
||||
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<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
|
||||
// character data is temporarily replaced in-game. In other play modes and in
|
||||
// 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> character_data;
|
||||
std::shared_ptr<PSOBBGuildCardFile> guild_card_data;
|
||||
|
||||
+320
-12
@@ -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->battle_records = cmd.char_file.battle_records;
|
||||
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) {
|
||||
auto& cmd = check_size_t<PSOBBSystemFile>(data);
|
||||
auto& cmd = check_size_t<PSOBBFullSystemFile>(data);
|
||||
auto sys = c->game_data.system();
|
||||
*sys = cmd;
|
||||
*sys = cmd.base;
|
||||
c->game_data.save_system_file();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
static void on_EA_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string&) {
|
||||
// TODO: Implement teams. This command has a very large number of subcommands
|
||||
// (up to 20EA!).
|
||||
if (command == 0x01EA) {
|
||||
send_lobby_message_box(c, "$C6Teams are not supported.");
|
||||
} else if (command == 0x14EA) {
|
||||
// Do nothing (for now)
|
||||
} else {
|
||||
throw invalid_argument("unimplemented team command");
|
||||
static void on_EA_BB(shared_ptr<Client> c, uint16_t command, uint32_t flag, string& data) {
|
||||
auto s = c->require_server_state();
|
||||
|
||||
switch (command >> 8) {
|
||||
case 0x01: { // Create team
|
||||
const auto& cmd = check_size_t<C_CreateTeam_BB_01EA>(data);
|
||||
string team_name = cmd.name.decode(c->language());
|
||||
if (s->team_index->get_by_name(team_name)) {
|
||||
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
@@ -197,20 +197,14 @@ uint32_t PSOBBGuildCardFile::checksum() const {
|
||||
return crc32(this, sizeof(*this));
|
||||
}
|
||||
|
||||
PSOBBSystemFile::PSOBBSystemFile()
|
||||
: guild_card_number(0),
|
||||
team_id(0),
|
||||
team_info(0),
|
||||
team_privilege_level(0),
|
||||
reserved(0),
|
||||
team_rewards(0) {
|
||||
PSOBBBaseSystemFile::PSOBBBaseSystemFile() {
|
||||
// This field is based on 1/1/2000, not 1/1/1970, so adjust appropriately
|
||||
this->base.creation_timestamp = (now() - 946684800000000ULL) / 1000000;
|
||||
for (size_t z = 0; z < PSOBBSystemFile::DEFAULT_KEY_CONFIG.size(); z++) {
|
||||
this->key_config[z] = PSOBBSystemFile::DEFAULT_KEY_CONFIG[z];
|
||||
for (size_t z = 0; z < PSOBBBaseSystemFile::DEFAULT_KEY_CONFIG.size(); z++) {
|
||||
this->key_config[z] = PSOBBBaseSystemFile::DEFAULT_KEY_CONFIG[z];
|
||||
}
|
||||
for (size_t z = 0; z < PSOBBSystemFile::DEFAULT_JOYSTICK_CONFIG.size(); z++) {
|
||||
this->joystick_config[z] = PSOBBSystemFile::DEFAULT_JOYSTICK_CONFIG[z];
|
||||
for (size_t z = 0; z < PSOBBBaseSystemFile::DEFAULT_JOYSTICK_CONFIG.size(); 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,
|
||||
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, 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,
|
||||
@@ -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, 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, 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,
|
||||
|
||||
+32
-15
@@ -119,7 +119,7 @@ struct PSOGCEp3SystemFile {
|
||||
/* 012C */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PSOBBSystemFileBase {
|
||||
struct PSOBBMinimalSystemFile {
|
||||
/* 0000 */ be_uint32_t checksum = 0;
|
||||
/* 0004 */ be_int16_t music_volume = 0;
|
||||
/* 0006 */ int8_t sound_volume = 0;
|
||||
@@ -132,24 +132,41 @@ struct PSOBBSystemFileBase {
|
||||
/* 0114 */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PSOBBSystemFile {
|
||||
/* 0000 */ PSOBBSystemFileBase base;
|
||||
struct PSOBBTeamMembership {
|
||||
/* 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;
|
||||
/* 0280 */ parray<uint8_t, 0x0038> joystick_config;
|
||||
/* 02B8 */ le_uint32_t guild_card_number = 0;
|
||||
/* 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 */
|
||||
/* 02B8 */
|
||||
|
||||
static const std::array<uint8_t, 0x016C> DEFAULT_KEY_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));
|
||||
|
||||
struct PSOBBCharacterFile {
|
||||
@@ -242,7 +259,7 @@ struct PSOBBGuildCardFile {
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
/* 0000 */ PSOBBSystemFileBase system_file;
|
||||
/* 0000 */ PSOBBMinimalSystemFile system_file;
|
||||
/* 0114 */ parray<GuildCardBB, 0x1C> blocked;
|
||||
/* 1DF4 */ parray<uint8_t, 0x180> unknown_a2;
|
||||
/* 1F74 */ parray<Entry, 0x69> entries;
|
||||
@@ -758,7 +775,7 @@ struct LegacySavedAccountDataBB { // .nsa file format
|
||||
/* 0000 */ pstring<TextEncoding::ASCII, 0x40> signature;
|
||||
/* 0040 */ parray<le_uint32_t, 0x001E> blocked_senders;
|
||||
/* 00B8 */ PSOBBGuildCardFile guild_card_file;
|
||||
/* D648 */ PSOBBSystemFile system_file;
|
||||
/* D648 */ PSOBBFullSystemFile system_file;
|
||||
/* E138 */ le_uint32_t unused;
|
||||
/* E13C */ le_uint32_t option_flags;
|
||||
/* E140 */ parray<uint8_t, 0x0A40> shortcuts;
|
||||
|
||||
+246
-10
@@ -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) {
|
||||
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) {
|
||||
@@ -596,12 +603,19 @@ void send_approve_player_choice_bb(shared_ptr<Client> c) {
|
||||
void send_complete_player_bb(shared_ptr<Client> c) {
|
||||
auto p = c->game_data.character(true, false);
|
||||
auto sys = c->game_data.system(true);
|
||||
auto team = c->team();
|
||||
if (c->config.check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB)) {
|
||||
p->inventory.language = 1;
|
||||
p->guild_card.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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
uint32_t guild_card_number = source->license->serial_number;
|
||||
auto source_team = source->team();
|
||||
|
||||
uint64_t xb_user_id = source->license->xb_user_id
|
||||
? source->license->xb_user_id
|
||||
: (0xAE00000000000000 | guild_card_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;
|
||||
: (0xAE00000000000000ULL | source->license->serial_number);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (c->version() == GameVersion::BB) {
|
||||
send_all_nearby_team_metadatas_to_client(c, false);
|
||||
}
|
||||
|
||||
uint8_t lobby_type;
|
||||
if (c->config.override_lobby_number != 0x80) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -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<Lobby> l, 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
|
||||
|
||||
@@ -288,6 +288,8 @@ Proxy session commands:\n\
|
||||
for (const string& type : types) {
|
||||
if (type == "licenses") {
|
||||
this->state->load_licenses();
|
||||
} else if (type == "teams") {
|
||||
this->state->load_teams();
|
||||
} else if (type == "patches") {
|
||||
this->state->load_patch_indexes();
|
||||
} else if (type == "battle-params") {
|
||||
|
||||
+7
-3
@@ -98,6 +98,7 @@ void ServerState::init() {
|
||||
this->parse_config(config, false);
|
||||
this->load_bb_private_keys();
|
||||
this->load_licenses();
|
||||
this->load_teams();
|
||||
this->load_patch_indexes();
|
||||
this->load_battle_params();
|
||||
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) {
|
||||
try {
|
||||
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()) {
|
||||
if (l == other_l) {
|
||||
continue; // don't bother looking again
|
||||
@@ -887,10 +886,15 @@ void ServerState::load_bb_private_keys() {
|
||||
}
|
||||
|
||||
void ServerState::load_licenses() {
|
||||
config_log.info("Loading license list");
|
||||
config_log.info("Indexing licenses");
|
||||
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() {
|
||||
if (isdir("system/patch-pc")) {
|
||||
config_log.info("Indexing PSO PC patch files");
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "Lobby.hh"
|
||||
#include "Menu.hh"
|
||||
#include "Quest.hh"
|
||||
#include "TeamIndex.hh"
|
||||
#include "WordSelectTable.hh"
|
||||
|
||||
// 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::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_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 load_bb_private_keys();
|
||||
void load_licenses();
|
||||
void load_teams();
|
||||
void load_patch_indexes();
|
||||
void load_battle_params();
|
||||
void load_level_table();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user