implement most remaining BB team functions

This commit is contained in:
Martin Michelsen
2023-11-29 11:35:15 -08:00
parent 556360c993
commit 9ea84d7101
14 changed files with 557 additions and 263 deletions
+4 -3
View File
@@ -245,7 +245,7 @@ shared_ptr<Lobby> Client::require_lobby() const {
return l;
}
shared_ptr<TeamIndex::Team> Client::team() {
shared_ptr<const TeamIndex::Team> Client::team() {
if (!this->license) {
throw logic_error("Client::team called on client with no license");
}
@@ -258,6 +258,7 @@ shared_ptr<TeamIndex::Team> Client::team() {
auto s = this->require_server_state();
auto team = s->team_index->get_by_id(this->license->bb_team_id);
if (!team) {
this->log.info("License contains a team ID, but the team does not exist; clearing team ID from license");
this->license->bb_team_id = 0;
this->license->save();
return nullptr;
@@ -265,6 +266,7 @@ shared_ptr<TeamIndex::Team> Client::team() {
auto member_it = team->members.find(this->license->serial_number);
if (member_it == team->members.end()) {
this->log.info("License contains a team ID, but the team does not contain this member; clearing team ID from license");
this->license->bb_team_id = 0;
this->license->save();
return nullptr;
@@ -277,8 +279,7 @@ shared_ptr<TeamIndex::Team> Client::team() {
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();
s->team_index->update_member_name(this->license->serial_number, name);
}
}
+1 -2
View File
@@ -54,7 +54,6 @@ 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,
@@ -232,7 +231,7 @@ 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();
std::shared_ptr<const TeamIndex::Team> team();
static void dispatch_save_game_data(evutil_socket_t, short, void* ctx);
void save_game_data();
+35 -21
View File
@@ -3254,9 +3254,8 @@ struct C_AddOrRemoveTeamMember_BB_03EA_05EA {
struct SC_TeamChat_BB_07EA {
pstring<TextEncoding::UTF16, 0x10> sender_name;
// It seems there are no real limits on the message length, other than the
// overall command length limit of 0x7C00 bytes.
// Text follows here
// Text follows here. The message is truncated by the client if it is longer
// than 0x8F wchar_ts.
} __packed__;
// 08EA (C->S): Get team member list
@@ -3267,8 +3266,8 @@ struct SC_TeamChat_BB_07EA {
struct S_TeamMemberList_BB_09EA {
le_uint32_t entry_count = 0;
struct Entry {
// This is displayed as "<%04d> %s" % (value, message)
le_uint32_t index = 0;
// This is displayed as "<%04d> %s" % (rank, name)
le_uint32_t rank = 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;
@@ -3278,16 +3277,17 @@ struct S_TeamMemberList_BB_09EA {
} __packed__;
// 0CEA (S->C): Unknown
// The client appears to ignore this command.
struct S_Unknown_BB_0CEA {
parray<uint8_t, 0x20> unknown_a1;
// Text follows here
} __packed__;
// 0DEA (C->S): Unknown
// 0DEA (C->S): Get team name
// No arguments
// 0EEA (S->C): Unknown
// 0EEA (S->C): Team name
struct S_Unknown_BB_0EEA {
parray<uint8_t, 0x10> unused;
@@ -3306,8 +3306,10 @@ struct C_SetTeamFlag_BB_0FEA {
// 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.
// server sends it, only header.flag is used. As with various other team
// commands, header.flag specifies the error code in this case.
// header.flag specifies the new privilege level for the specified team member.
// Known values: 0 = normal, 0x30 = leader, 0x40 = master
struct C_ChangeTeamMemberPrivilegeLevel_BB_11EA {
le_uint32_t guild_card_number = 0;
@@ -3333,6 +3335,8 @@ struct S_TeamMembershipInformation_BB_12EA {
// header.flag specifies the number of entries.
struct S_TeamInfoForPlayer_BB_13EA_15EA_Entry {
// The client uses the first four of these to determine if the player is in a
// team or not - if they are all zero, the player is not in a team.
le_uint32_t guild_card_number = 0;
le_uint32_t team_id = 0;
le_uint32_t unknown_a3 = 0;
@@ -3345,20 +3349,20 @@ struct S_TeamInfoForPlayer_BB_13EA_15EA_Entry {
parray<le_uint16_t, 0x20 * 0x20> flag_data;
} __packed__;
// 14EA (C->S): Unknown
// 14EA (C->S): Get team info for lobby players
// No arguments. Client always sends 1 in the header.flag field.
// 15EA (S->C): Unknown
// 15EA (S->C): Team info for lobby players
// header.flag specifies the number of entries. The entry format appears to be
// the same as for the 13EA command.
// 16EA (S->C): Unknown
// No arguments except header.flag.
// 18EA: Team ranking information
// 18EA: Intra-team ranking information
// No arguments (C->S)
struct S_TeamRankingInformation_BB_18EA {
struct S_IntraTeamRanking_BB_18EA {
/* 0000 */ le_uint32_t ranking_points = 0;
/* 0004 */ le_uint32_t unknown_a2 = 0;
/* 0008 */ le_uint32_t points_remaining = 0;
@@ -3378,13 +3382,7 @@ struct S_TeamRankingInformation_BB_18EA {
// 19EA: Team reward list
// No arguments (C->S)
struct S_TeamRewardList_BB_19EA {
le_uint32_t num_rewards_unlocked = 0;
} __packed__;
// 1AEA: Team rewards available for purchase
struct S_TeamRewardsAvailableForPurchase_BB_1AEA {
struct S_TeamRewardList_BB_19EA_1AEA {
le_uint32_t num_entries;
struct Entry {
/* 0000 */ pstring<TextEncoding::UTF16, 0x40> name;
@@ -3397,13 +3395,28 @@ struct S_TeamRewardsAvailableForPurchase_BB_1AEA {
// Entry entries[num_entries];
} __packed__;
// 1AEA: Team rewards available for purchase
// Same format as 19EA.
// 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: Cross-team ranking information
// No arguments when sent by the client.
struct S_CrossTeamRanking_BB_1CEA {
le_uint32_t num_entries;
struct Entry {
/* 00 */ pstring<TextEncoding::UTF16, 0x10> team_name;
/* 20 */ le_uint32_t team_points = 0;
/* 24 */ le_uint32_t unknown_a1 = 0;
/* 28 */
} __packed__;
// Variable length field:
// Entry entries[num_entries];
} __packed__;
// 1DEA (S->C): Update team rewards bitmask
// header.flag specifies the new rewards bitmask.
@@ -3415,7 +3428,8 @@ struct C_Unknown_BB_1EEA {
} __packed__;
// 1FEA (S->C): Action result
// This command behaves exactly like 02EA.
// This command behaves exactly like 02EA. This command is presumably the
// response to whatever 1EEA does.
// 20EA: Unknown
// header.flag is used, but no other arguments
+44 -94
View File
@@ -4528,23 +4528,27 @@ static void on_EA_BB(shared_ptr<Client> c, uint16_t command, uint32_t flag, stri
try {
added_c = s->find_client(nullptr, cmd.guild_card_number);
} catch (const out_of_range&) {
send_lobby_message_box(c, "Player is offline");
send_command(c, 0x04EA, 0x00000006);
}
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");
send_command(c, 0x04EA, 0x00000001);
send_command(added_c, 0x04EA, 0x00000001);
} 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);
s->team_index->add_member(
team->team_id,
added_c->license->serial_number,
added_c->game_data.character()->disp.name.decode(added_c->language()));
send_update_team_metadata_for_client(added_c);
send_team_membership_info(added_c);
@@ -4623,8 +4627,7 @@ static void on_EA_BB(shared_ptr<Client> c, uint16_t command, uint32_t flag, stri
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();
s->team_index->set_flag_data(team->team_id, cmd.flag_data);
for (const auto& it : team->members) {
try {
auto member_c = s->find_client(nullptr, it.second.serial_number);
@@ -4660,37 +4663,29 @@ static void on_EA_BB(shared_ptr<Client> c, uint16_t command, uint32_t flag, stri
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);
if (s->team_index->demote_leader(c->license->serial_number, cmd.guild_card_number)) {
send_command(c, 0x11EA, 0x00000000);
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);
if (s->team_index->promote_leader(c->license->serial_number, cmd.guild_card_number)) {
send_command(c, 0x11EA, 0x00000000);
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);
s->team_index->change_master(c->license->serial_number, cmd.guild_card_number);
send_command(c, 0x11EA, 0x00000000);
send_updates_for_this_m = true;
send_updates_for_other_m = true;
break;
@@ -4705,8 +4700,8 @@ static void on_EA_BB(shared_ptr<Client> c, uint16_t command, uint32_t flag, stri
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);
send_update_team_metadata_for_client(other_c);
send_team_membership_info(other_c);
} catch (const out_of_range&) {
}
}
@@ -4720,83 +4715,37 @@ static void on_EA_BB(shared_ptr<Client> c, uint16_t command, uint32_t flag, stri
send_all_nearby_team_metadatas_to_client(c, false);
break;
case 0x18EA: // Ranking information
send_team_rank_info(c);
send_intra_team_ranking(c);
break;
case 0x19EA: {
S_TeamRewardList_BB_19EA cmd = {0};
send_command_t(c, 0x19EA, 0x00000000, cmd);
break;
}
case 0x1AEA: // Get team rewards available for purchase
send_team_rewards_available_for_purchase(c);
case 0x19EA: // List purchased team rewards
case 0x1AEA: // List team rewards available for purchase
send_team_reward_list(c, (command == 0x19EA));
break;
case 0x1BEA: { // 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");
const auto& reward = s->team_index->reward_definitions().at(flag);
for (const auto& key : reward.prerequisite_keys) {
if (!team->has_reward(key)) {
throw runtime_error("not all prerequisite rewards have been purchased");
}
}
if (should_send_update_reward_flags) {
if (reward.is_unique && team->has_reward(reward.key)) {
throw runtime_error("team reward already purchased");
}
if (!reward.reward_item.empty()) {
// TODO: How do we do this? Do we just send a 6xBE in the lobby?
// (Once this is figured out, don't forget to move this block to after
// the reward is actually purchased)
throw runtime_error("team reward items are not implemented");
}
s->team_index->buy_reward(team->team_id, reward.key, reward.team_points, reward.reward_flag);
if (reward.reward_flag != TeamIndex::Team::RewardFlag::NONE) {
for (const auto& it : team->members) {
try {
auto member_c = s->find_client(nullptr, it.second.serial_number);
@@ -4809,7 +4758,8 @@ static void on_EA_BB(shared_ptr<Client> c, uint16_t command, uint32_t flag, stri
break;
}
case 0x1CEA:
throw runtime_error("team subcommand is not yet implemented");
send_cross_team_ranking(c);
break;
default:
throw runtime_error("invalid team command");
}
+5 -6
View File
@@ -2137,8 +2137,7 @@ void on_exchange_item_for_team_points_bb(shared_ptr<Client> c, uint8_t command,
auto item = p->remove_item(cmd.item_id, cmd.amount, c->version() != Version::BB_V4);
size_t points = s->item_parameter_table_v4->get_item_team_points(item);
team->members.at(c->license->serial_number).points += points;
team->save_config();
s->team_index->add_member_points(c->license->serial_number, points);
auto name = s->describe_item(c->version(), item, false);
l->log.info("Player %hhu exchanged inventory item %hu:%08" PRIX32 " (%s) for %zu team points",
@@ -2917,8 +2916,8 @@ SubcommandDefinition subcommand_definitions[0x100] = {
/* 6xBE */ {0x00, 0x00, 0xBE, on_ep3_sound_chat},
/* 6xBF */ {0x00, 0x00, 0xBF, on_forward_check_size_ep3_lobby},
/* 6xC0 */ {0x00, 0x00, 0xC0, on_sell_item_at_shop_bb},
/* 6xC1 */ {0x00, 0x00, 0xC1, nullptr},
/* 6xC2 */ {0x00, 0x00, 0xC2, nullptr},
/* 6xC1 */ {0x00, 0x00, 0xC1, on_forward_check_size},
/* 6xC2 */ {0x00, 0x00, 0xC2, on_forward_check_size},
/* 6xC3 */ {0x00, 0x00, 0xC3, on_drop_partial_stack_bb},
/* 6xC4 */ {0x00, 0x00, 0xC4, on_sort_inventory_bb},
/* 6xC5 */ {0x00, 0x00, 0xC5, on_medical_center_bb},
@@ -2929,8 +2928,8 @@ SubcommandDefinition subcommand_definitions[0x100] = {
/* 6xCA */ {0x00, 0x00, 0xCA, on_item_reward_request_bb},
/* 6xCB */ {0x00, 0x00, 0xCB, nullptr},
/* 6xCC */ {0x00, 0x00, 0xCC, on_exchange_item_for_team_points_bb},
/* 6xCD */ {0x00, 0x00, 0xCD, nullptr},
/* 6xCE */ {0x00, 0x00, 0xCE, nullptr},
/* 6xCD */ {0x00, 0x00, 0xCD, on_forward_check_size},
/* 6xCE */ {0x00, 0x00, 0xCE, on_forward_check_size},
/* 6xCF */ {0x00, 0x00, 0xCF, on_battle_restart_bb},
/* 6xD0 */ {0x00, 0x00, 0xD0, on_battle_level_up_bb},
/* 6xD1 */ {0x00, 0x00, 0xD1, on_request_challenge_grave_recovery_item_bb},
+90 -120
View File
@@ -3313,10 +3313,10 @@ static S_TeamInfoForPlayer_BB_13EA_15EA_Entry team_metadata_for_client(shared_pt
auto team = c->team();
S_TeamInfoForPlayer_BB_13EA_15EA_Entry cmd;
cmd.lobby_client_id = c->lobby_client_id;
cmd.guild_card_number = c->license->serial_number;
cmd.guild_card_number2 = c->license->serial_number;
cmd.player_name = c->game_data.character()->disp.name;
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);
@@ -3356,29 +3356,6 @@ void send_team_member_list(shared_ptr<Client> c) {
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");
}
vector<const TeamIndex::Team::Member*> members;
for (const auto& it : team->members) {
members.emplace_back(&it.second);
@@ -3388,10 +3365,45 @@ void send_team_rank_info(std::shared_ptr<Client> c) {
};
sort(members.begin(), members.end(), rank_fn);
S_TeamRankingInformation_BB_18EA cmd;
cmd.points_remaining = 0;
S_TeamMemberList_BB_09EA header;
header.entry_count = members.size();
vector<S_TeamRankingInformation_BB_18EA::Entry> entries;
vector<S_TeamMemberList_BB_09EA::Entry> entries;
entries.reserve(header.entry_count);
for (size_t z = 0; z < members.size(); z++) {
const auto* m = members[z];
auto& e = entries.emplace_back();
e.rank = z + 1;
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_intra_team_ranking(std::shared_ptr<Client> c) {
auto team = c->team();
if (!team) {
throw runtime_error("client is not in a team");
}
// TODO: At some point we should maintain a sorted index instead of sorting
// these on-demand.
vector<const TeamIndex::Team::Member*> members;
for (const auto& it : team->members) {
members.emplace_back(&it.second);
}
auto rank_fn = +[](const TeamIndex::Team::Member* a, const TeamIndex::Team::Member* b) {
return a->points > b->points;
};
sort(members.begin(), members.end(), rank_fn);
S_IntraTeamRanking_BB_18EA cmd;
cmd.points_remaining = team->points - team->spent_points;
cmd.num_entries = members.size();
vector<S_IntraTeamRanking_BB_18EA::Entry> entries;
for (size_t z = 0; z < members.size(); z++) {
const auto* m = members[z];
cmd.ranking_points += m->points;
@@ -3402,111 +3414,69 @@ void send_team_rank_info(std::shared_ptr<Client> c) {
e.player_name.encode(m->name);
e.points = m->points;
}
cmd.num_entries = entries.size();
send_command_t_vt(c, 0x18EA, 0x00000000, cmd, entries);
}
void send_team_rewards_available_for_purchase(std::shared_ptr<Client> c) {
void send_cross_team_ranking(std::shared_ptr<Client> c) {
auto s = c->require_server_state();
// TODO: At some point we should maintain a sorted index instead of sorting
// these on-demand.
auto teams = s->team_index->all();
auto rank_fn = +[](const shared_ptr<const TeamIndex::Team>& a, const shared_ptr<const TeamIndex::Team>& b) {
return a->points > b->points;
};
sort(teams.begin(), teams.end(), rank_fn);
size_t num_to_send = min<size_t>(teams.size(), 0x300);
S_CrossTeamRanking_BB_1CEA cmd;
cmd.num_entries = num_to_send;
vector<S_CrossTeamRanking_BB_1CEA::Entry> entries;
for (size_t z = 0; z < num_to_send; z++) {
auto t = teams[z];
auto& e = entries.emplace_back();
e.team_name.encode(t->name, c->language());
e.team_points = t->points;
e.unknown_a1 = 0x01020304;
}
send_command_t_vt(c, 0x1CEA, 0x00000000, cmd, entries);
}
void send_team_reward_list(std::shared_ptr<Client> c, bool show_purchased) {
auto team = c->team();
if (!team) {
throw runtime_error("user is not in a team");
}
auto s = c->require_server_state();
vector<S_TeamRewardsAvailableForPurchase_BB_1AEA::Entry> entries;
if (!team->check_reward_flag(TeamIndex::Team::RewardFlag::TEAM_FLAG)) {
vector<S_TeamRewardList_BB_19EA_1AEA::Entry> entries;
for (const auto& reward : s->team_index->reward_definitions()) {
if (team->has_reward(reward.key) != show_purchased) {
continue;
}
bool has_all_prerequisites = true;
for (const auto& key : reward.prerequisite_keys) {
if (!team->has_reward(key)) {
has_all_prerequisites = false;
break;
}
}
if (!has_all_prerequisites) {
continue;
}
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;
e.name.encode(reward.name, c->language());
e.description.encode(reward.description, c->language());
e.reward_id = reward.menu_item_id;
e.team_points = reward.team_points;
}
// 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;
S_TeamRewardList_BB_19EA_1AEA cmd;
cmd.num_entries = entries.size();
send_command_t_vt(c, 0x1AEA, 0x00000000, cmd, entries);
send_command_t_vt(c, show_purchased ? 0x19EA : 0x1AEA, 0x00000000, cmd, entries);
}
+3 -2
View File
@@ -395,5 +395,6 @@ void send_update_team_metadata_for_client(std::shared_ptr<Client> c); // 15EA (t
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
void send_intra_team_ranking(std::shared_ptr<Client> c); // 18EA
void send_team_reward_list(std::shared_ptr<Client> c, bool show_purchased); // 19EA, 1AEA
void send_cross_team_ranking(std::shared_ptr<Client> c); // 1CEA
+1
View File
@@ -314,6 +314,7 @@ Proxy session commands:\n\
auto config_json = this->state->load_config();
this->state->parse_config(config_json, true);
this->state->resolve_ep3_card_names();
this->state->load_teams();
} else {
throw invalid_argument("incorrect data type");
}
+7 -1
View File
@@ -898,6 +898,11 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
this->welcome_message = json.get_string("WelcomeMessage", "");
this->pc_patch_server_message = json.get_string("PCPatchServerMessage", "");
this->bb_patch_server_message = json.get_string("BBPatchServerMessage", "");
try {
this->team_reward_defs_json = std::move(json.at("TeamRewards"));
} catch (const out_of_range&) {
}
}
void ServerState::load_bb_private_keys() {
@@ -919,7 +924,8 @@ void ServerState::load_licenses() {
void ServerState::load_teams() {
config_log.info("Indexing teams");
this->team_index.reset(new TeamIndex("system/teams"));
this->team_index.reset(new TeamIndex("system/teams", this->team_reward_defs_json));
this->team_reward_defs_json = nullptr;
}
void ServerState::load_patch_indexes() {
+1
View File
@@ -143,6 +143,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::shared_ptr<LicenseIndex> license_index;
std::shared_ptr<TeamIndex> team_index;
JSON team_reward_defs_json;
std::shared_ptr<const Menu> information_menu_v2;
std::shared_ptr<const Menu> information_menu_v3;
+134 -7
View File
@@ -52,11 +52,20 @@ string TeamIndex::Team::flag_filename() const {
void TeamIndex::Team::load_config() {
auto json = JSON::parse(load_file(this->json_filename()));
this->name = json.get_string("Name");
this->spent_points = json.get_int("SpentPoints");
this->points = 0;
for (const auto& member_it : json.get_list("Members")) {
Member m(*member_it);
this->points += m.points;
uint32_t serial_number = m.serial_number;
this->members.emplace(serial_number, std::move(m));
}
try {
for (const auto& it : json.get_list("RewardKeys")) {
this->reward_keys.emplace(it->as_string());
}
} catch (const out_of_range&) {
}
this->reward_flags = json.get_int("RewardFlags");
}
@@ -65,9 +74,15 @@ void TeamIndex::Team::save_config() const {
for (const auto& it : this->members) {
members_json.emplace_back(it.second.json());
}
JSON reward_keys_json = JSON::list();
for (const auto& it : this->reward_keys) {
reward_keys_json.emplace_back(it);
}
JSON root = JSON::dict({
{"Name", this->name},
{"SpentPoints", this->spent_points},
{"Members", std::move(members_json)},
{"RewardKeys", std::move(reward_keys_json)},
{"RewardFlags", this->reward_flags},
});
save_file(this->json_filename(), root.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS));
@@ -128,6 +143,10 @@ PSOBBTeamMembership TeamIndex::Team::membership_for_member(uint32_t serial_numbe
return ret;
}
bool TeamIndex::Team::has_reward(const string& key) const {
return this->reward_keys.count(key);
}
size_t TeamIndex::Team::num_members() const {
return this->members.size();
}
@@ -178,9 +197,37 @@ bool TeamIndex::Team::can_promote_leader() const {
return this->num_leaders() < this->max_leaders();
}
TeamIndex::TeamIndex(const string& directory)
TeamIndex::Reward::Reward(uint32_t menu_item_id, const JSON& def_json)
: menu_item_id(menu_item_id),
key(def_json.get_string("Key")),
name(def_json.get_string("Name")),
description(def_json.get_string("Description")),
is_unique(def_json.get_bool("IsUnique", true)),
team_points(def_json.get_int("Points")) {
try {
for (const auto& it : def_json.get_list("PrerequisiteKeys")) {
this->prerequisite_keys.emplace(it->as_string());
}
} catch (const out_of_range&) {
}
try {
this->reward_flag = static_cast<Team::RewardFlag>(def_json.get_int("RewardFlag"));
} catch (const out_of_range&) {
}
try {
this->reward_item = ItemData::from_data(def_json.get_string("RewardItem"));
} catch (const out_of_range&) {
}
}
TeamIndex::TeamIndex(const string& directory, const JSON& reward_defs_json)
: directory(directory),
next_team_id(1) {
uint32_t reward_menu_item_id = 0;
for (const auto& it : reward_defs_json.as_list()) {
this->reward_defs.emplace_back(reward_menu_item_id++, *it);
}
if (!isdir(this->directory)) {
mkdir(this->directory.c_str(), 0755);
return;
@@ -214,7 +261,7 @@ size_t TeamIndex::count() const {
return this->id_to_team.size();
}
shared_ptr<TeamIndex::Team> TeamIndex::get_by_id(uint32_t team_id) {
shared_ptr<const TeamIndex::Team> TeamIndex::get_by_id(uint32_t team_id) const {
try {
return this->id_to_team.at(team_id);
} catch (const out_of_range&) {
@@ -222,7 +269,7 @@ shared_ptr<TeamIndex::Team> TeamIndex::get_by_id(uint32_t team_id) {
}
}
shared_ptr<TeamIndex::Team> TeamIndex::get_by_name(const string& name) {
shared_ptr<const TeamIndex::Team> TeamIndex::get_by_name(const string& name) const {
try {
return this->name_to_team.at(name);
} catch (const out_of_range&) {
@@ -230,7 +277,7 @@ shared_ptr<TeamIndex::Team> TeamIndex::get_by_name(const string& name) {
}
}
shared_ptr<TeamIndex::Team> TeamIndex::get_by_serial_number(uint32_t serial_number) {
shared_ptr<const TeamIndex::Team> TeamIndex::get_by_serial_number(uint32_t serial_number) const {
try {
return this->serial_number_to_team.at(serial_number);
} catch (const out_of_range&) {
@@ -238,15 +285,15 @@ shared_ptr<TeamIndex::Team> TeamIndex::get_by_serial_number(uint32_t serial_numb
}
}
vector<shared_ptr<TeamIndex::Team>> TeamIndex::all() {
vector<shared_ptr<Team>> ret;
vector<shared_ptr<const TeamIndex::Team>> TeamIndex::all() const {
vector<shared_ptr<const 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<const 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());
@@ -301,6 +348,86 @@ void TeamIndex::remove_member(uint32_t serial_number) {
}
}
void TeamIndex::update_member_name(uint32_t serial_number, const std::string& name) {
auto team = this->serial_number_to_team.at(serial_number);
auto& m = team->members.at(serial_number);
m.name = name;
team->save_config();
}
void TeamIndex::add_member_points(uint32_t serial_number, uint32_t points) {
auto team = this->serial_number_to_team.at(serial_number);
team->members.at(serial_number).points += points;
team->points += points;
team->save_config();
}
void TeamIndex::set_flag_data(uint32_t team_id, const parray<le_uint16_t, 0x20 * 0x20>& flag_data) {
auto team = this->id_to_team.at(team_id);
team->flag_data.reset(new parray<le_uint16_t, 0x20 * 0x20>(flag_data));
team->save_flag();
}
bool TeamIndex::promote_leader(uint32_t master_serial_number, uint32_t leader_serial_number) {
auto team = this->serial_number_to_team.at(master_serial_number);
auto& master_m = team->members.at(master_serial_number);
if (!master_m.check_flag(TeamIndex::Team::Member::Flag::IS_MASTER)) {
throw runtime_error("incorrect master serial number");
}
auto& other_m = team->members.at(leader_serial_number);
if (other_m.check_flag(TeamIndex::Team::Member::Flag::IS_LEADER) || !team->can_promote_leader()) {
return false;
}
other_m.set_flag(TeamIndex::Team::Member::Flag::IS_LEADER);
team->save_config();
return true;
}
bool TeamIndex::demote_leader(uint32_t master_serial_number, uint32_t leader_serial_number) {
auto team = this->serial_number_to_team.at(master_serial_number);
auto& master_m = team->members.at(master_serial_number);
if (!master_m.check_flag(TeamIndex::Team::Member::Flag::IS_MASTER)) {
throw runtime_error("incorrect master serial number");
}
auto& other_m = team->members.at(leader_serial_number);
if (!other_m.check_flag(TeamIndex::Team::Member::Flag::IS_LEADER)) {
return false;
}
other_m.clear_flag(TeamIndex::Team::Member::Flag::IS_LEADER);
team->save_config();
return true;
}
void TeamIndex::change_master(uint32_t master_serial_number, uint32_t new_master_serial_number) {
auto team = this->serial_number_to_team.at(master_serial_number);
auto& master_m = team->members.at(master_serial_number);
if (!master_m.check_flag(TeamIndex::Team::Member::Flag::IS_MASTER)) {
throw runtime_error("incorrect master serial number");
}
auto& new_master_m = team->members.at(new_master_serial_number);
master_m.clear_flag(TeamIndex::Team::Member::Flag::IS_MASTER);
master_m.set_flag(TeamIndex::Team::Member::Flag::IS_LEADER);
new_master_m.clear_flag(TeamIndex::Team::Member::Flag::IS_LEADER);
new_master_m.set_flag(TeamIndex::Team::Member::Flag::IS_MASTER);
team->save_config();
}
void TeamIndex::buy_reward(uint32_t team_id, const string& key, uint32_t points, Team::RewardFlag reward_flag) {
auto team = this->id_to_team.at(team_id);
if (team->spent_points + points > team->points) {
throw runtime_error("not enough points available");
}
team->reward_keys.emplace(key);
team->spent_points += points;
if (reward_flag != Team::RewardFlag::NONE) {
team->set_reward_flag(reward_flag);
}
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");
+39 -7
View File
@@ -47,6 +47,7 @@ public:
enum class RewardFlag {
// Only 0x00000001 and 0x00000002 are used by the client; the rest are
// free to be used however the server chooses.
NONE = 0x00000000,
TEAM_FLAG = 0x00000001,
DRESSING_ROOM = 0x00000002,
MEMBERS_20_LEADERS_3 = 0x00000004,
@@ -56,9 +57,12 @@ public:
};
uint32_t team_id = 0;
uint32_t points = 0;
uint32_t spent_points = 0;
std::string name;
std::unordered_map<uint32_t, Member> members;
uint32_t reward_flags = 0;
std::unordered_set<std::string> reward_keys;
std::shared_ptr<parray<le_uint16_t, 0x20 * 0x20>> flag_data;
Team() = default;
@@ -86,6 +90,8 @@ public:
this->reward_flags &= (~static_cast<uint8_t>(flag));
}
[[nodiscard]] bool has_reward(const std::string& key) const;
size_t num_members() const;
size_t num_leaders() const;
size_t max_members() const;
@@ -94,20 +100,45 @@ public:
bool can_promote_leader() const;
};
explicit TeamIndex(const std::string& directory);
struct Reward {
uint32_t menu_item_id = 0;
std::string key;
std::string name;
std::string description;
std::unordered_set<std::string> prerequisite_keys;
bool is_unique = true;
uint32_t team_points = 0;
Team::RewardFlag reward_flag = Team::RewardFlag::NONE;
ItemData reward_item;
Reward(uint32_t menu_item_id, const JSON& def_json);
};
TeamIndex(const std::string& directory, const JSON& reward_defs_json);
~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();
inline const std::vector<Reward>& reward_definitions() const {
return this->reward_defs;
}
std::shared_ptr<Team> create(std::string& name, uint32_t master_serial_number, const std::string& master_name);
size_t count() const;
std::shared_ptr<const Team> get_by_id(uint32_t team_id) const;
std::shared_ptr<const Team> get_by_name(const std::string& name) const;
std::shared_ptr<const Team> get_by_serial_number(uint32_t serial_number) const;
std::vector<std::shared_ptr<const Team>> all() const;
std::shared_ptr<const 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);
void update_member_name(uint32_t serial_number, const std::string& name);
void add_member_points(uint32_t serial_number, uint32_t points);
void set_flag_data(uint32_t team_id, const parray<le_uint16_t, 0x20 * 0x20>& flag_data);
bool promote_leader(uint32_t master_serial_number, uint32_t leader_serial_number);
bool demote_leader(uint32_t master_serial_number, uint32_t leader_serial_number);
void change_master(uint32_t master_serial_number, uint32_t new_master_serial_number);
void buy_reward(uint32_t team_id, const std::string& key, uint32_t points, Team::RewardFlag reward_flag);
protected:
std::string directory;
@@ -115,6 +146,7 @@ protected:
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;
std::vector<Reward> reward_defs;
void add_to_indexes(std::shared_ptr<Team> team);
void remove_from_indexes(std::shared_ptr<Team> team);
+97
View File
@@ -559,6 +559,103 @@
// limitation, and must be at least 1.
"BBGlobalEXPMultiplier": 1,
// BB team reward definitions.
"TeamRewards": [
{
"Key": "TeamFlag",
"Name": "Team flag",
"Description": "Show a custom banner\nabove your team's\nplayers in the lobby",
"Points": 2500,
"RewardFlag": 0x00000001,
}, {
"Key": "DressingRoom",
"Name": "Dressing room",
"Description": "Unlock the ability to\nchange your character's\nappearance",
"Points": 3000,
"RewardFlag": 0x00000002,
}, {
"Key": "Members20Leaders3",
"Name": "20 team members",
"Description": "Increase your team's\nsize limit to 30 members\nand 3 leaders",
"Points": 1500,
"RewardFlag": 0x00000004,
}, {
"Key": "Members40Leaders5",
"Name": "40 team members",
"Description": "Increase your team's\nsize limit to 40 members\nand 5 leaders",
"Points": 4000,
"PrerequisiteKeys": ["Members20Leaders3"],
"RewardFlag": 0x00000008,
}, {
"Key": "Members70Leaders8",
"Name": "70 team members",
"Description": "Increase your team's\nsize limit to 70 members\nand 8 leaders",
"Points": 9000,
"PrerequisiteKeys": ["Members40Leaders5"],
"RewardFlag": 0x00000010,
}, {
"Key": "Members100Leaders10",
"Name": "100 team members",
"Description": "Increase your team's\nsize limit to 100 members\nand 10 leaders",
"Points": 18000,
"PrerequisiteKeys": ["Members70Leaders8"],
"RewardFlag": 0x00000020,
}, {
"Key": "PointOfDisaster",
"Name": "Quest: Point of Disaster",
"Description": "Unlock the quest\nPoint of Disaster\nfor your team",
"Points": 1000,
}, {
"Key": "ToysTwilight",
"Name": "Quest: Toys Twilight",
"Description": "Unlock the quest\nToys Twilight\nfor your team",
"Points": 1000,
}, {
"Key": "CommanderBlade",
"Name": "Commander Blade",
"Description": "Create a Commander\nBlade weapon",
"IsUnique": false,
"Points": 8000,
"RewardItem": "00B200",
}, {
"Key": "UnionGuard",
"Name": "Union Guard",
"Description": "Create a Union Guard\nshield",
"IsUnique": false,
"Points": 100,
// TODO: There are 4 of these in names-v4.json; which should we use?
"RewardItem": "010295",
}, {
"Key": "Ticket500",
"Name": "Team Points Ticket 500",
"Description": "Create a 500-point ticket",
"IsUnique": false,
"Points": 500,
"RewardItem": "031900",
}, {
"Key": "Ticket1000",
"Name": "Team Points Ticket 1000",
"Description": "Create a 1000-point ticket",
"IsUnique": false,
"Points": 1000,
"RewardItem": "031901",
}, {
"Key": "Ticket5000",
"Name": "Team Points Ticket 5000",
"Description": "Create a 5000-point ticket",
"IsUnique": false,
"Points": 5000,
"RewardItem": "031902",
}, {
"Key": "Ticket10000",
"Name": "Team Points Ticket 10000",
"Description": "Create a 10000-point ticket",
"IsUnique": false,
"Points": 10000,
"RewardItem": "031903",
},
],
// Cheat mode behavior. There are three values:
// "Off": Cheat mode is disabled on the entire server. Cheat mode cannot be
// enabled in games, and the $cheat command does nothing. This also
+96
View File
@@ -184,4 +184,100 @@
"031000",
],
"BBGlobalEXPMultiplier": 1,
"TeamRewards": [
{
"Key": "TeamFlag",
"Name": "Team flag",
"Description": "Show a custom banner\nabove your team's\nplayers in the lobby",
"Points": 2500,
"RewardFlag": 0x00000001,
}, {
"Key": "DressingRoom",
"Name": "Dressing room",
"Description": "Unlock the ability to\nchange your character's\nappearance",
"Points": 3000,
"RewardFlag": 0x00000002,
}, {
"Key": "Members20Leaders3",
"Name": "20 team members",
"Description": "Increase your team's\nsize limit to 30 members\nand 3 leaders",
"Points": 1500,
"RewardFlag": 0x00000004,
}, {
"Key": "Members40Leaders5",
"Name": "40 team members",
"Description": "Increase your team's\nsize limit to 40 members\nand 5 leaders",
"Points": 4000,
"PrerequisiteKeys": ["Members20Leaders3"],
"RewardFlag": 0x00000008,
}, {
"Key": "Members70Leaders8",
"Name": "70 team members",
"Description": "Increase your team's\nsize limit to 70 members\nand 8 leaders",
"Points": 9000,
"PrerequisiteKeys": ["Members40Leaders5"],
"RewardFlag": 0x00000010,
}, {
"Key": "Members100Leaders10",
"Name": "100 team members",
"Description": "Increase your team's\nsize limit to 100 members\nand 10 leaders",
"Points": 18000,
"PrerequisiteKeys": ["Members70Leaders8"],
"RewardFlag": 0x00000020,
}, {
"Key": "PointOfDisaster",
"Name": "Quest: Point of Disaster",
"Description": "Unlock the quest\nPoint of Disaster\nfor your team",
"Points": 1000,
}, {
"Key": "ToysTwilight",
"Name": "Quest: Toys Twilight",
"Description": "Unlock the quest\nToys Twilight\nfor your team",
"Points": 1000,
}, {
"Key": "CommanderBlade",
"Name": "Commander Blade",
"Description": "Create a Commander\nBlade weapon",
"IsUnique": false,
"Points": 8000,
"RewardItem": "00B200",
}, {
"Key": "UnionGuard",
"Name": "Union Guard",
"Description": "Create a Union Guard\nshield",
"IsUnique": false,
"Points": 100,
// TODO: There are 4 of these in names-v4.json; which should we use?
"RewardItem": "010295",
}, {
"Key": "Ticket500",
"Name": "Team Points Ticket 500",
"Description": "Create a 500-point ticket",
"IsUnique": false,
"Points": 500,
"RewardItem": "031900",
}, {
"Key": "Ticket1000",
"Name": "Team Points Ticket 1000",
"Description": "Create a 1000-point ticket",
"IsUnique": false,
"Points": 1000,
"RewardItem": "031901",
}, {
"Key": "Ticket5000",
"Name": "Team Points Ticket 5000",
"Description": "Create a 5000-point ticket",
"IsUnique": false,
"Points": 5000,
"RewardItem": "031902",
}, {
"Key": "Ticket10000",
"Name": "Team Points Ticket 10000",
"Description": "Create a 10000-point ticket",
"IsUnique": false,
"Points": 10000,
"RewardItem": "031903",
},
],
}