implement card auctions
This commit is contained in:
@@ -205,6 +205,22 @@ static void server_command_dbgid(shared_ptr<ServerState>, shared_ptr<Lobby>,
|
||||
c->prefer_high_lobby_client_id ? "high" : "low");
|
||||
}
|
||||
|
||||
static void server_command_auction(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string&) {
|
||||
check_privileges(c, Privilege::DEBUG);
|
||||
if (l->is_game() && (l->flags & Lobby::Flag::EPISODE_3_ONLY)) {
|
||||
G_InitiateCardAuction_GC_Ep3_6xB5x42 cmd;
|
||||
send_command_t(l, 0xC9, 0x00, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
static void proxy_command_auction(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, const std::u16string&) {
|
||||
G_InitiateCardAuction_GC_Ep3_6xB5x42 cmd;
|
||||
session.client_channel.send(0xC9, 0x00, &cmd, sizeof(cmd));
|
||||
session.server_channel.send(0xC9, 0x00, &cmd, sizeof(cmd));
|
||||
}
|
||||
|
||||
static void server_command_patch(shared_ptr<ServerState> s, shared_ptr<Lobby>,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
std::shared_ptr<CompiledFunctionCode> fn;
|
||||
@@ -976,6 +992,7 @@ static const unordered_map<u16string, ChatCommandDefinition> chat_commands({
|
||||
{u"$allevent", {server_command_lobby_event_all, nullptr, u"Usage:\nallevent <name/ID>"}},
|
||||
{u"$ann", {server_command_announce, nullptr, u"Usage:\nann <message>"}},
|
||||
{u"$arrow", {server_command_arrow, proxy_command_arrow, u"Usage:\narrow <color>"}},
|
||||
{u"$auction", {server_command_auction, proxy_command_auction, u"Usage:\nauction"}},
|
||||
{u"$ax", {server_command_ax, nullptr, u"Usage:\nax <message>"}},
|
||||
{u"$ban", {server_command_ban, nullptr, u"Usage:\nban <name-or-number>"}},
|
||||
// TODO: implement this on proxy server
|
||||
|
||||
+19
-17
@@ -33,43 +33,45 @@ struct Client {
|
||||
// Note that this flag is NOT set for Episode 3 Trial Edition clients, since
|
||||
// that version is similar enough to the release version of Episode 3 that
|
||||
// newserv does not have to change its behavior at all.
|
||||
IS_TRIAL_EDITION = 0x2000,
|
||||
IS_TRIAL_EDITION = 0x00002000,
|
||||
// Client is DC v1
|
||||
IS_DC_V1 = 0x0010,
|
||||
IS_DC_V1 = 0x00000010,
|
||||
// For patch server clients, client is Blue Burst rather than PC
|
||||
IS_BB_PATCH = 0x0001,
|
||||
IS_BB_PATCH = 0x00000001,
|
||||
// After joining a lobby, client will no longer send D6 commands when they
|
||||
// close message boxes
|
||||
NO_D6_AFTER_LOBBY = 0x0002,
|
||||
NO_D6_AFTER_LOBBY = 0x00000002,
|
||||
// Client has the above flag and has already joined a lobby, or is not GC
|
||||
NO_D6 = 0x0004,
|
||||
NO_D6 = 0x00000004,
|
||||
// Client is Episode 3, should be able to see CARD lobbies, and should only
|
||||
// be able to see/join games with the EPISODE_3_ONLY flag
|
||||
IS_EPISODE_3 = 0x0008,
|
||||
IS_EPISODE_3 = 0x00000008,
|
||||
// Client disconnects if it receives B2 (send_function_call)
|
||||
NO_SEND_FUNCTION_CALL = 0x0200,
|
||||
NO_SEND_FUNCTION_CALL = 0x00000200,
|
||||
// Client requires doubly-encrypted code section in send_function_call
|
||||
ENCRYPTED_SEND_FUNCTION_CALL = 0x0800,
|
||||
ENCRYPTED_SEND_FUNCTION_CALL = 0x00000800,
|
||||
// Client supports send_function_call but does not actually run the code
|
||||
SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x1000,
|
||||
SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x00001000,
|
||||
// Client is vulnerable to a buffer overflow that we can use to enable
|
||||
// send_function_call
|
||||
USE_OVERFLOW_FOR_SEND_FUNCTION_CALL = 0x8000,
|
||||
USE_OVERFLOW_FOR_SEND_FUNCTION_CALL = 0x00008000,
|
||||
|
||||
// Client is loading into a game
|
||||
LOADING = 0x0020,
|
||||
LOADING = 0x00000020,
|
||||
// Client is loading a quest
|
||||
LOADING_QUEST = 0x0040,
|
||||
LOADING_QUEST = 0x00000040,
|
||||
// Client is waiting to join an Episode 3 card auction
|
||||
AWAITING_CARD_AUCTION = 0x00010000,
|
||||
// Client is in the information menu (login server only)
|
||||
IN_INFORMATION_MENU = 0x0080,
|
||||
IN_INFORMATION_MENU = 0x00000080,
|
||||
// Client is at the welcome message (login server only)
|
||||
AT_WELCOME_MESSAGE = 0x0100,
|
||||
AT_WELCOME_MESSAGE = 0x00000100,
|
||||
// Client has already received a 97 (enable saves) command, so don't show
|
||||
// the programs menu anymore
|
||||
SAVE_ENABLED = 0x0400,
|
||||
SAVE_ENABLED = 0x00000400,
|
||||
// Client has received newserv's Episode 3 card definitions, so don't send
|
||||
// them again
|
||||
HAS_EP3_CARD_DEFS = 0x4000,
|
||||
HAS_EP3_CARD_DEFS = 0x00004000,
|
||||
};
|
||||
|
||||
uint64_t id;
|
||||
@@ -82,7 +84,7 @@ struct Client {
|
||||
// config can be up to 0x20 bytes; on BB it can be 0x28 bytes. We don't use
|
||||
// all of that space.
|
||||
uint8_t bb_game_state;
|
||||
uint16_t flags;
|
||||
uint32_t flags;
|
||||
|
||||
// Network
|
||||
Channel channel;
|
||||
|
||||
@@ -71,10 +71,10 @@ enum ClientStateBB : uint8_t {
|
||||
|
||||
struct ClientConfig {
|
||||
uint64_t magic;
|
||||
uint16_t flags;
|
||||
uint32_t flags;
|
||||
uint32_t proxy_destination_address;
|
||||
uint16_t proxy_destination_port;
|
||||
parray<uint8_t, 0x10> unused;
|
||||
parray<uint8_t, 0x0E> unused;
|
||||
} __packed__;
|
||||
|
||||
struct ClientConfigBB {
|
||||
|
||||
@@ -1241,6 +1241,11 @@ DataIndex::DataIndex(const string& directory, bool debug)
|
||||
"duplicate card id: %08" PRIX32, entry->def.card_id.load()));
|
||||
}
|
||||
|
||||
// Some cards intentionally have the same name, so we just leave them
|
||||
// unindexed (they can still be looked up by ID, of course)
|
||||
string name = entry->def.en_name;
|
||||
this->card_definitions_by_name.emplace(name, entry);
|
||||
|
||||
entry->def.hp.decode_code();
|
||||
entry->def.ap.decode_code();
|
||||
entry->def.tp.decode_code();
|
||||
@@ -1320,6 +1325,11 @@ shared_ptr<const DataIndex::CardEntry> DataIndex::definition_for_card_id(
|
||||
return this->card_definitions.at(id);
|
||||
}
|
||||
|
||||
shared_ptr<const DataIndex::CardEntry> DataIndex::definition_for_card_name(
|
||||
const string& name) const {
|
||||
return this->card_definitions_by_name.at(name);
|
||||
}
|
||||
|
||||
set<uint32_t> DataIndex::all_card_ids() const {
|
||||
set<uint32_t> ret;
|
||||
for (const auto& it : this->card_definitions) {
|
||||
|
||||
@@ -790,6 +790,8 @@ public:
|
||||
|
||||
const std::string& get_compressed_card_definitions() const;
|
||||
std::shared_ptr<const CardEntry> definition_for_card_id(uint32_t id) const;
|
||||
std::shared_ptr<const CardEntry> definition_for_card_name(
|
||||
const std::string& name) const;
|
||||
std::set<uint32_t> all_card_ids() const;
|
||||
|
||||
const std::string& get_compressed_map_list() const;
|
||||
@@ -801,6 +803,7 @@ private:
|
||||
|
||||
std::string compressed_card_definitions;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<CardEntry>> card_definitions;
|
||||
std::unordered_map<std::string, std::shared_ptr<CardEntry>> card_definitions_by_name;
|
||||
|
||||
// The compressed map list is generated on demand from the maps map below.
|
||||
// It's marked mutable because the logical consistency of the DataIndex object
|
||||
|
||||
+28
@@ -160,6 +160,34 @@ void populate_state_from_config(shared_ptr<ServerState> s,
|
||||
s->ep3_behavior_flags = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
s->ep3_card_auction_points = d.at("CardAuctionPoints")->as_int();
|
||||
} catch (const out_of_range&) {
|
||||
s->ep3_card_auction_points = 0;
|
||||
}
|
||||
try {
|
||||
auto i = d.at("CardAuctionSize");
|
||||
if (i->is_int()) {
|
||||
s->ep3_card_auction_min_size = i->as_int();
|
||||
s->ep3_card_auction_max_size = s->ep3_card_auction_min_size;
|
||||
} else {
|
||||
s->ep3_card_auction_min_size = i->as_list().at(0)->as_int();
|
||||
s->ep3_card_auction_max_size = i->as_list().at(1)->as_int();
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
s->ep3_card_auction_min_size = 0;
|
||||
s->ep3_card_auction_max_size = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const auto& it : d.at("CardAuctionPool")->as_dict()) {
|
||||
const auto& card_name = it.first;
|
||||
const auto& card_cfg_json = it.second->as_list();
|
||||
s->ep3_card_auction_pool.emplace(card_name, make_pair(
|
||||
card_cfg_json.at(0)->as_int(), card_cfg_json.at(1)->as_int()));
|
||||
}
|
||||
} catch (const out_of_range&) { }
|
||||
|
||||
shared_ptr<JSONObject> log_levels_json;
|
||||
try {
|
||||
log_levels_json = d.at("LogLevels");
|
||||
|
||||
+89
-1
@@ -923,6 +923,15 @@ static void on_ep3_server_data_request(shared_ptr<ServerState> s, shared_ptr<Cli
|
||||
l->ep3_server_base = make_shared<Episode3::ServerBase>(
|
||||
l, s->ep3_data_index, s->ep3_behavior_flags, l->random_seed);
|
||||
l->ep3_server_base->init();
|
||||
|
||||
if (s->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES) {
|
||||
for (size_t z = 0; z < l->max_clients; z++) {
|
||||
if (l->clients[z]) {
|
||||
send_text_message_printf(l->clients[z], "Your client ID: $C6%zu", z);
|
||||
}
|
||||
}
|
||||
send_text_message_printf(l, "State seed: $C6%08" PRIX32, l->random_seed);
|
||||
}
|
||||
}
|
||||
l->ep3_server_base->server->on_server_data_input(data);
|
||||
}
|
||||
@@ -2831,6 +2840,85 @@ static void on_card_trade(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
}
|
||||
}
|
||||
|
||||
static void on_card_auction_join(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
uint16_t, uint32_t, const string& data) { // EF
|
||||
check_size_v(data.size(), 0);
|
||||
|
||||
if (!(c->flags & Client::Flag::IS_EPISODE_3)) {
|
||||
throw runtime_error("non-Ep3 client sent card auction join command");
|
||||
}
|
||||
auto l = s->find_lobby(c->lobby_id);
|
||||
if (!(l->flags & Lobby::Flag::EPISODE_3_ONLY)) {
|
||||
throw runtime_error("client sent card auction join command outside of Ep3 lobby");
|
||||
}
|
||||
if (!l->is_game()) {
|
||||
throw runtime_error("client sent card auction join command in non-game lobby");
|
||||
}
|
||||
|
||||
if (c->flags & Client::Flag::AWAITING_CARD_AUCTION) {
|
||||
return;
|
||||
}
|
||||
c->flags |= Client::Flag::AWAITING_CARD_AUCTION;
|
||||
|
||||
// Check if any client is still loading
|
||||
// TODO: We need to handle clients disconnecting during this procedure.
|
||||
// Probably on_client_disconnect needs to check for this case...
|
||||
size_t x;
|
||||
for (x = 0; x < l->max_clients; x++) {
|
||||
if (!l->clients[x]) {
|
||||
continue;
|
||||
}
|
||||
if (!(l->clients[x]->flags & Client::Flag::AWAITING_CARD_AUCTION)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (x != l->max_clients) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (x = 0; x < l->max_clients; x++) {
|
||||
if (l->clients[x]) {
|
||||
l->clients[x]->flags &= ~Client::Flag::AWAITING_CARD_AUCTION;
|
||||
}
|
||||
}
|
||||
|
||||
if ((s->ep3_card_auction_points == 0) ||
|
||||
(s->ep3_card_auction_min_size == 0) ||
|
||||
(s->ep3_card_auction_max_size == 0)) {
|
||||
throw runtime_error("card auctions are not configured on this server");
|
||||
}
|
||||
|
||||
uint16_t num_cards;
|
||||
if (s->ep3_card_auction_min_size == s->ep3_card_auction_max_size) {
|
||||
num_cards = s->ep3_card_auction_min_size;
|
||||
} else {
|
||||
num_cards = s->ep3_card_auction_min_size +
|
||||
(random_object<uint16_t>() % (s->ep3_card_auction_max_size - s->ep3_card_auction_min_size + 1));
|
||||
}
|
||||
num_cards = min<uint16_t>(num_cards, 0x14);
|
||||
|
||||
uint64_t distribution_size = 0;
|
||||
for (const auto& it : s->ep3_card_auction_pool) {
|
||||
distribution_size += it.second.first;
|
||||
}
|
||||
|
||||
S_StartCardAuction_GC_Ep3_EF cmd;
|
||||
cmd.points_available = s->ep3_card_auction_points;
|
||||
for (size_t z = 0; z < num_cards; z++) {
|
||||
uint64_t v = random_object<uint64_t>() % distribution_size;
|
||||
for (const auto& it : s->ep3_card_auction_pool) {
|
||||
if (v >= it.second.first) {
|
||||
v -= it.second.first;
|
||||
} else {
|
||||
cmd.entries[z].card_id = s->ep3_data_index->definition_for_card_name(it.first)->def.card_id.load();
|
||||
cmd.entries[z].min_price = it.second.second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
send_command_t(l, 0xEF, num_cards, cmd);
|
||||
}
|
||||
|
||||
|
||||
|
||||
static void on_team_command_bb(shared_ptr<ServerState>, shared_ptr<Client> c,
|
||||
@@ -3263,7 +3351,7 @@ static on_command_t handlers[0x100][6] = {
|
||||
/* EC */ {nullptr, nullptr, nullptr, on_create_game_dc_v3, nullptr, on_leave_char_select_bb, }, /* EC */
|
||||
/* ED */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_change_account_data_bb, }, /* ED */
|
||||
/* EE */ {nullptr, nullptr, nullptr, on_card_trade, nullptr, nullptr, }, /* EE */
|
||||
/* EF */ {nullptr, nullptr, nullptr, on_ignored_command, nullptr, nullptr, }, /* EF */
|
||||
/* EF */ {nullptr, nullptr, nullptr, on_card_auction_join, nullptr, nullptr, }, /* EF */
|
||||
/* F0 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* F0 */
|
||||
/* F1 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* F1 */
|
||||
/* F2 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* F2 */
|
||||
|
||||
+5
-1
@@ -25,7 +25,11 @@ ServerState::ServerState()
|
||||
episode_3_send_function_call_enabled(false),
|
||||
catch_handler_exceptions(true),
|
||||
ep3_behavior_flags(0),
|
||||
run_shell_behavior(RunShellBehavior::DEFAULT), next_lobby_id(1),
|
||||
run_shell_behavior(RunShellBehavior::DEFAULT),
|
||||
ep3_card_auction_points(0),
|
||||
ep3_card_auction_min_size(0),
|
||||
ep3_card_auction_max_size(0),
|
||||
next_lobby_id(1),
|
||||
pre_lobby_event(0),
|
||||
ep3_menu_song(-1) {
|
||||
vector<shared_ptr<Lobby>> non_v1_only_lobbies;
|
||||
|
||||
@@ -66,6 +66,11 @@ struct ServerState {
|
||||
std::shared_ptr<const GSLArchive> bb_data_gsl;
|
||||
std::shared_ptr<const RareItemSet> rare_item_set;
|
||||
|
||||
uint16_t ep3_card_auction_points;
|
||||
uint16_t ep3_card_auction_min_size;
|
||||
uint16_t ep3_card_auction_max_size;
|
||||
std::unordered_map<std::string, std::pair<uint64_t, uint16_t>> ep3_card_auction_pool;
|
||||
|
||||
std::shared_ptr<LicenseManager> license_manager;
|
||||
|
||||
std::vector<MenuItem> main_menu;
|
||||
|
||||
@@ -249,9 +249,37 @@
|
||||
// "Episode3MenuSong": 0,
|
||||
|
||||
// Episode 3 battle behavior flags. When set to zero, battles behave as they
|
||||
// did on the original Sega servers.
|
||||
// TODO: Document what nonzero values do here.
|
||||
"Episode3BehaviorFlags": 0,
|
||||
// did on the original Sega servers. Combinations of behaviors can be enabled
|
||||
// by bitwise-OR'ing together the following values:
|
||||
// 0x00000001 => Disable deck verification entirely
|
||||
// 0x00000002 => Disable owned card count check during deck verification (this
|
||||
// enables the use of the non-saveable Have All Cards code, but
|
||||
// retains all the other validity checks)
|
||||
// 0x00000004 => Allow card with the D1 and D2 ranks to be used in battle
|
||||
// 0x00000008 => Disable overall and per-phase battle time limits, regardless
|
||||
// of the options chosen during battle setup
|
||||
// 0x00000010 => Enable debug messages in Episode 3 games and battles
|
||||
"Episode3BehaviorFlags": 0x00000002,
|
||||
|
||||
// Episode 3 card auction configuration. CardAuctionPoints specifies how many
|
||||
// points each player gets when they join an auction (may be anywhere from 0
|
||||
// to 65535, but a somewhat-low number is generally good). CardAuctionSize
|
||||
// specifies how many cards will be present in each auction; if this is a
|
||||
// list, then the number of cards is random in the specified range. Finally,
|
||||
// CardAuctionContents is a dictionary specifying the relative frequencies and
|
||||
// costs of each card in the auction pool. Relative frequencies are 64-bit
|
||||
// integers, but should generally be less than 0x0100000000000000 to avoid
|
||||
// excessive bias. There is no fixed summation bound for relative frequencies.
|
||||
// Cards are always drawn (with replacement) from the same distribution.
|
||||
"CardAuctionPoints": 30,
|
||||
"CardAuctionSize": [2, 4],
|
||||
"CardAuctionPool": {
|
||||
// "CardName": [RelativeFrequency, MinPrice]
|
||||
"Red Sword": [500, 8],
|
||||
"Hildeblue": [400, 10],
|
||||
"Grants": [300, 15],
|
||||
"Megid": [700, 6],
|
||||
},
|
||||
|
||||
// Whether to enable patches on Episode 3 USA. This functionality depends on
|
||||
// exploiting a bug in Episode 3, and while it seems to work reliably on
|
||||
|
||||
Reference in New Issue
Block a user