initial implementation of BB teams (WIP)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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");
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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