add random stream into Ep3 battle records

This commit is contained in:
Martin Michelsen
2024-04-21 01:13:34 -07:00
parent de42135532
commit 673c767a42
8 changed files with 94 additions and 54 deletions
+21 -3
View File
@@ -121,7 +121,7 @@ void BattleRecord::Event::print(FILE* stream) const {
print_data(stream, this->data);
break;
default:
throw runtime_error("unknown event type in batlte record");
throw runtime_error("unknown event type in battle record");
}
}
@@ -137,14 +137,23 @@ BattleRecord::BattleRecord(const string& data)
battle_start_timestamp(0),
battle_end_timestamp(0) {
StringReader r(data);
uint64_t signature = r.get_u64l();
if (signature != this->SIGNATURE) {
bool has_random_stream;
if (signature == this->SIGNATURE_V1) {
has_random_stream = false;
} else if (signature == this->SIGNATURE_V2) {
has_random_stream = true;
} else {
throw runtime_error("incorrect battle record signature");
}
this->battle_start_timestamp = r.get_u64l();
this->battle_end_timestamp = r.get_u64l();
this->behavior_flags = r.get_u32l();
if (has_random_stream) {
this->random_stream = r.read(r.get_u32l());
}
while (!r.eof()) {
this->events.emplace_back(r);
}
@@ -152,10 +161,12 @@ BattleRecord::BattleRecord(const string& data)
string BattleRecord::serialize() const {
StringWriter w;
w.put_u64l(this->SIGNATURE);
w.put_u64l(this->SIGNATURE_V2);
w.put_u64l(this->battle_start_timestamp);
w.put_u64l(this->battle_end_timestamp);
w.put_u32l(this->behavior_flags);
w.put_u32l(this->random_stream.size());
w.write(this->random_stream);
for (const auto& ev : this->events) {
ev.serialize(w);
}
@@ -240,6 +251,10 @@ void BattleRecord::add_chat_message(
ev.data = std::move(data);
}
void BattleRecord::add_random_data(const void* data, size_t size) {
this->random_stream.append(reinterpret_cast<const char*>(data), size);
}
bool BattleRecord::is_map_definition_event(const Event& ev) {
if (ev.type == Event::Type::BATTLE_COMMAND) {
auto& header = check_size_t<G_CardBattleCommandHeader>(ev.data, 0xFFFF);
@@ -321,6 +336,9 @@ void BattleRecord::set_battle_start_timestamp() {
}
}
this->events = std::move(new_events);
// Clear any existing random data (there shouldn't be any)
this->random_stream.clear();
}
void BattleRecord::set_battle_end_timestamp() {
+5 -1
View File
@@ -76,6 +76,7 @@ public:
void add_command(Event::Type type, const void* data, size_t size);
void add_command(Event::Type type, std::string&& data);
void add_chat_message(uint32_t guild_card_number, std::string&& data);
void add_random_data(const void* data, size_t size);
// This function collapses all the existing player join/leave events into a
// single SET_INITIAL_PLAYERS event, and deletes all events before the latest
// BATTLE_COMMAND command that specifies the battle map. This should provide a
@@ -86,7 +87,8 @@ public:
void print(FILE* stream) const;
private:
static constexpr uint64_t SIGNATURE = 0x14C946D56D1DAC50;
static constexpr uint64_t SIGNATURE_V1 = 0x14C946D56D1DAC50;
static constexpr uint64_t SIGNATURE_V2 = 0xD01E5EC12853C377;
static bool is_map_definition_event(const Event& ev);
@@ -96,6 +98,7 @@ private:
uint64_t battle_start_timestamp;
uint64_t battle_end_timestamp;
std::deque<Event> events;
std::string random_stream;
friend class BattleRecordPlayer;
};
@@ -120,6 +123,7 @@ private:
std::shared_ptr<struct event_base> base;
std::weak_ptr<Lobby> lobby;
std::shared_ptr<struct event> next_command_ev;
StringReader random_r;
};
} // namespace Episode3
+11 -9
View File
@@ -3859,8 +3859,10 @@ void CardSpecial::check_for_defense_interference(
shared_ptr<const Card> attacker_card,
shared_ptr<Card> target_card,
int16_t* inout_unknown_p4) {
auto s = this->server();
// Note: This check is not part of the original implementation.
if (this->server()->options.behavior_flags & BehaviorFlag::DISABLE_INTERFERENCE) {
if (s->options.behavior_flags & BehaviorFlag::DISABLE_INTERFERENCE) {
return;
}
@@ -3871,13 +3873,13 @@ void CardSpecial::check_for_defense_interference(
return;
}
uint16_t ally_sc_card_ref = this->server()->ruler_server->get_ally_sc_card_ref(
uint16_t ally_sc_card_ref = s->ruler_server->get_ally_sc_card_ref(
target_card->get_card_ref());
if (ally_sc_card_ref == 0xFFFF) {
return;
}
auto ally_sc = this->server()->card_for_set_card_ref(ally_sc_card_ref);
auto ally_sc = s->card_for_set_card_ref(ally_sc_card_ref);
if (!ally_sc || (ally_sc->card_flags & 2)) {
return;
}
@@ -3892,17 +3894,17 @@ void CardSpecial::check_for_defense_interference(
return;
}
auto ally_hes = this->server()->ruler_server->get_hand_and_equip_state_for_client_id(target_ally_client_id);
if (!ally_hes || (!(this->server()->options.behavior_flags & BehaviorFlag::ALLOW_NON_COM_INTERFERENCE) && !ally_hes->is_cpu_player)) {
auto ally_hes = s->ruler_server->get_hand_and_equip_state_for_client_id(target_ally_client_id);
if (!ally_hes || (!(s->options.behavior_flags & BehaviorFlag::ALLOW_NON_COM_INTERFERENCE) && !ally_hes->is_cpu_player)) {
return;
}
uint16_t target_card_id = this->server()->card_id_for_card_ref(target_card->get_card_ref());
uint16_t target_card_id = s->card_id_for_card_ref(target_card->get_card_ref());
if (target_card_id == 0xFFFF) {
return;
}
uint16_t ally_sc_card_id = this->server()->card_id_for_card_ref(ally_sc_card_ref);
uint16_t ally_sc_card_id = s->card_id_for_card_ref(ally_sc_card_ref);
if (ally_sc_card_id == 0xFFFF) {
return;
}
@@ -3916,7 +3918,7 @@ void CardSpecial::check_for_defense_interference(
}
auto entry = get_interference_probability_entry(
target_card_id, ally_sc_card_id, false);
if (!entry || (this->server()->get_random(99) >= entry->defense_probability)) {
if (!entry || (s->get_random(99) >= entry->defense_probability)) {
return;
}
@@ -3928,7 +3930,7 @@ void CardSpecial::check_for_defense_interference(
cmd.effect.target_card_ref = target_card->get_card_ref();
cmd.effect.value = 0;
cmd.effect.operation = 0x7D;
this->server()->send(cmd);
s->send(cmd);
if (inout_unknown_p4) {
*inout_unknown_p4 = 0;
target_card->action_metadata.set_flags(0x10);
+16 -4
View File
@@ -1,5 +1,7 @@
#include "DeckState.hh"
#include "Server.hh"
using namespace std;
namespace Episode3 {
@@ -203,12 +205,17 @@ void DeckState::do_mulligan(bool is_nte) {
this->card_refs[index + 5] = temp_ref;
}
auto s = this->server.lock();
if (!s) {
throw runtime_error("server is missing");
}
// Shuffle the deck, except the first 5 cards (which are about to be drawn).
size_t max = this->num_drawable_cards() - 5;
uint8_t base_index = this->draw_index + 5;
for (size_t z = 0; z < this->card_refs.size(); z++) {
uint8_t index1 = random_from_optional_crypt(this->opt_rand_crypt) % max;
uint8_t index2 = random_from_optional_crypt(this->opt_rand_crypt) % max;
uint8_t index1 = s->get_random(max);
uint8_t index2 = s->get_random(max);
uint16_t temp_ref = this->card_refs[base_index + index1];
this->card_refs[base_index + index1] = this->card_refs[base_index + index2];
this->card_refs[base_index + index2] = temp_ref;
@@ -260,6 +267,11 @@ void DeckState::set_card_discarded(uint16_t card_ref) {
void DeckState::shuffle() {
if (this->shuffle_enabled) {
auto s = this->server.lock();
if (!s) {
throw runtime_error("server is missing");
}
size_t max = this->num_drawable_cards();
for (size_t z = 0; z < this->card_refs.size(); z++) {
// Note: This is the way Sega originally implemented shuffling - they just
@@ -267,8 +279,8 @@ void DeckState::shuffle() {
// instead swap each item with another random item (possibly itself) that
// doesn't appear earlier than it in the array, but this is not what Sega
// did.
uint8_t index1 = this->draw_index + random_from_optional_crypt(this->opt_rand_crypt) % max;
uint8_t index2 = this->draw_index + random_from_optional_crypt(this->opt_rand_crypt) % max;
uint8_t index1 = this->draw_index + s->get_random(max);
uint8_t index2 = this->draw_index + s->get_random(max);
uint16_t temp_ref = this->card_refs[index1];
this->card_refs[index1] = this->card_refs[index2];
this->card_refs[index2] = temp_ref;
+8 -6
View File
@@ -10,6 +10,8 @@
namespace Episode3 {
class Server;
struct NameEntry {
/* 00 */ pstring<TextEncoding::MARKED, 0x10> name;
/* 10 */ uint8_t client_id;
@@ -57,13 +59,13 @@ public:
DeckState(
uint8_t client_id,
const parray<CardIDT, 0x1F>& card_ids,
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt)
: client_id(client_id),
std::shared_ptr<Server> server)
: server(server),
client_id(client_id),
draw_index(1),
card_ref_base(this->client_id << 8),
shuffle_enabled(true),
loop_enabled(true),
opt_rand_crypt(opt_rand_crypt) {
loop_enabled(true) {
for (size_t z = 0; z < card_ids.size(); z++) {
auto& e = this->entries[z];
e.card_id = card_ids[z];
@@ -99,6 +101,8 @@ public:
void print(FILE* stream, std::shared_ptr<const CardIndex> card_index = nullptr) const;
private:
std::weak_ptr<Server> server;
struct CardEntry {
uint16_t card_id;
uint8_t deck_index;
@@ -111,8 +115,6 @@ private:
bool loop_enabled;
parray<CardEntry, 31> entries;
parray<uint16_t, 31> card_refs;
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt;
};
} // namespace Episode3
+1 -1
View File
@@ -49,7 +49,7 @@ void PlayerState::init() {
throw logic_error("replacing a player state object is not permitted");
}
this->deck_state = make_shared<DeckState>(this->client_id, s->deck_entries[client_id]->card_ids, s->options.opt_rand_crypt);
this->deck_state = make_shared<DeckState>(this->client_id, s->deck_entries[client_id]->card_ids, s);
if (s->map_and_rules->rules.disable_deck_shuffle) {
this->deck_state->disable_shuffle();
}
+29 -30
View File
@@ -4,6 +4,7 @@
#include <phosg/Time.hh>
#include "../Loggers.hh"
#include "../Revision.hh"
#include "../SendCommands.hh"
using namespace std;
@@ -29,6 +30,7 @@ void Server::PresenceEntry::clear() {
Server::Server(shared_ptr<Lobby> lobby, Options&& options)
: lobby(lobby),
battle_record(lobby->battle_record),
has_lobby(lobby != nullptr),
options(std::move(options)),
last_chosen_map(this->options.tournament ? this->options.tournament->get_map() : nullptr),
@@ -99,10 +101,10 @@ void Server::init() {
this->card_special = make_shared<CardSpecial>(this->shared_from_this());
// Note: The original implementation calls the default PSOV2Encryption
// constructor for opt_rand_crypt, which just uses 0 as the seed. It then
// constructor for random_crypt, which just uses 0 as the seed. It then
// re-seeds the generator later. We instead expect the caller to provide a
// seeded generator, and we don't re-seed it at all.
// this->opt_rand_crypt = make_shared<PSOV2Encryption>(0);
// this->random_crypt = make_shared<PSOV2Encryption>(0);
this->state_flags = make_shared<StateFlags>();
@@ -252,8 +254,8 @@ void Server::send(const void* data, size_t size, uint8_t command, bool enable_ma
for (auto watcher_l : l->watcher_lobbies) {
send_command_if_not_loading(watcher_l, command, 0x00, data, size);
}
if (l->battle_record && l->battle_record->writable()) {
l->battle_record->add_command(BattleRecord::Event::Type::BATTLE_COMMAND, data, size);
if (this->battle_record && this->battle_record->writable()) {
this->battle_record->add_command(BattleRecord::Event::Type::BATTLE_COMMAND, data, size);
}
} else if ((this->options.behavior_flags & BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING) &&
@@ -271,19 +273,8 @@ void Server::send_6xB4x46() const {
G_ServerVersionStrings_Ep3_6xB4x46 cmd;
cmd.version_signature.encode(this->options.is_nte() ? VERSION_SIGNATURE_NTE : VERSION_SIGNATURE, 1);
cmd.date_str1.encode(format_time(this->options.card_index->definitions_mtime() * 1000000), 1);
string date_str2;
if (this->options.opt_rand_crypt) {
date_str2 = string_printf(
"Random:%08" PRIX32 "+%08" PRIX32,
this->options.opt_rand_crypt->seed(),
this->options.opt_rand_crypt->absolute_offset());
} else {
date_str2 = "Random:<SYS>";
}
if (this->last_chosen_map) {
date_str2 += string_printf(" Map:%08" PRIX32, this->last_chosen_map->map_number);
}
cmd.date_str2.encode(date_str2, 1);
string build_date = format_time(BUILD_TIMESTAMP);
cmd.date_str2.encode(string_printf("newserv %s compiled at %s", GIT_REVISION_HASH, build_date.c_str()), 1);
this->send(cmd);
}
@@ -1083,16 +1074,24 @@ shared_ptr<const PlayerState> Server::get_player_state(uint8_t client_id) const
return this->player_states[client_id];
}
uint32_t Server::get_random_raw() {
le_uint32_t ret = random_from_optional_crypt(this->options.opt_rand_crypt);
if (this->battle_record && this->battle_record->writable()) {
this->battle_record->add_random_data(&ret, sizeof(ret));
}
return ret;
}
uint32_t Server::get_random(uint32_t max) {
// The original implementation was essentially:
// return (static_cast<double>(this->opt_rand_crypt->next() >> 16) / 65536.0) * max
// return (static_cast<double>(this->random_source->next() >> 16) / 65536.0) * max
// This is unnecessarily complicated and imprecise, so we instead just do:
return random_from_optional_crypt(this->options.opt_rand_crypt) % max;
return this->get_random_raw() % max;
}
float Server::get_random_float_0_1() {
// This lacks some precision, but matches the original implementation.
return (static_cast<double>(random_from_optional_crypt(this->options.opt_rand_crypt) >> 16) / 65536.0);
return (static_cast<double>(this->get_random_raw() >> 16) / 65536.0);
}
uint32_t Server::get_round_num() const {
@@ -1549,8 +1548,8 @@ void Server::setup_and_start_battle() {
this->setup_phase = SetupPhase::STARTER_ROLLS;
// Note: This is where original implementation re-seeds opt_rand_crypt (it
// uses time() as the seed value).
// Note: This is where original implementation re-seeds its random generator
// (it uses time() as the seed value).
for (size_t z = 0; z < 4; z++) {
if (!this->check_presence_entry(z)) {
@@ -1840,9 +1839,8 @@ void Server::on_server_data_input(shared_ptr<Client> sender_c, const string& dat
throw runtime_error("unknown CAx subsubcommand");
}
auto l = this->lobby.lock();
if (l && l->battle_record && l->battle_record->writable()) {
l->battle_record->add_command(BattleRecord::Event::Type::SERVER_DATA_COMMAND, data.data(), data.size());
if (this->battle_record && this->battle_record->writable()) {
this->battle_record->add_command(BattleRecord::Event::Type::SERVER_DATA_COMMAND, data.data(), data.size());
}
if ((sender_c && (sender_c->version() == Version::GC_EP3_NTE)) || !header.mask_key) {
@@ -2356,11 +2354,12 @@ void Server::handle_CAx1D_start_battle(shared_ptr<Client>, const string& data) {
}
if (should_start) {
if (this->battle_record && this->battle_record->writable()) {
this->battle_record->set_battle_start_timestamp();
}
auto l = this->lobby.lock();
if (l) {
if (l->battle_record) {
l->battle_record->set_battle_start_timestamp();
}
// Note: Sega's implementation doesn't set EX results values here; they
// did it at game join time instead. We do it here for code simplicity.
if ((l->base_version != Version::GC_EP3_NTE) && l->ep3_ex_result_values) {
@@ -2617,13 +2616,13 @@ void Server::send_6xB6x41_to_all_clients() const {
}
}
if (l->battle_record && l->battle_record->writable()) {
if (this->battle_record && this->battle_record->writable()) {
// TODO: It's not great that we just pick the first one; ideally we'd put
// all of them in the recording and send the appropriate one to the client
// in the playback lobby
for (string& data : map_commands_by_language) {
if (!data.empty()) {
l->battle_record->add_command(
this->battle_record->add_command(
BattleRecord::Event::Type::BATTLE_COMMAND, std::move(data));
break;
}
+3
View File
@@ -9,6 +9,7 @@
#include "../CommandFormats.hh"
#include "../Text.hh"
#include "AssistServer.hh"
#include "BattleRecord.hh"
#include "CardSpecial.hh"
#include "MapState.hh"
#include "PlayerState.hh"
@@ -191,6 +192,7 @@ public:
uint8_t get_current_team_turn() const;
std::shared_ptr<PlayerState> get_player_state(uint8_t client_id);
std::shared_ptr<const PlayerState> get_player_state(uint8_t client_id) const;
uint32_t get_random_raw();
uint32_t get_random(uint32_t max);
float get_random_float_0_1();
uint32_t get_round_num() const;
@@ -273,6 +275,7 @@ private:
public:
// These fields are not part of the original implementation
std::weak_ptr<Lobby> lobby;
std::shared_ptr<BattleRecord> battle_record;
bool has_lobby;
Options options;
std::shared_ptr<const MapIndex::Map> last_chosen_map;