make BB games deterministic for replays
This commit is contained in:
@@ -1,19 +1,16 @@
|
|||||||
## General
|
## General
|
||||||
|
|
||||||
- Encapsulate BB server-side random state and make replays deterministic
|
|
||||||
- Write a simple status API
|
- Write a simple status API
|
||||||
- Implement per-game logging
|
- Implement per-game logging
|
||||||
- Make reloading happen on separate threads so compression doesn't block active clients
|
- Make reloading happen on separate threads so compression doesn't block active clients
|
||||||
- Implement decrypt/encrypt actions for VMS files
|
- Implement decrypt/encrypt actions for VMS files
|
||||||
- Make UI strings localizable (e.g. entries in menus, welcome message, etc.)
|
- Make UI strings localizable (e.g. entries in menus, welcome message, etc.)
|
||||||
- Figure out what causes the corruption message on PC proxy sessions and fix it
|
|
||||||
- Add an idle connection timeout for proxy sessions
|
- Add an idle connection timeout for proxy sessions
|
||||||
- Look into JP heart symbol bug on Linux
|
- Look into JP heart symbol bug on Linux
|
||||||
|
|
||||||
## Episode 3
|
## Episode 3
|
||||||
|
|
||||||
- Enforce tournament deck restrictions (e.g. rank checks, No Assist option) when populating COMs at tournament start time
|
- Enforce tournament deck restrictions (e.g. rank checks, No Assist option) when populating COMs at tournament start time
|
||||||
- Add support for recording battles on the proxy server (both in primary and spectator teams)
|
|
||||||
- Make `reload licenses` not vulnerable to online players' licenses overwriting licenses on disk somehow
|
- Make `reload licenses` not vulnerable to online players' licenses overwriting licenses on disk somehow
|
||||||
- Implement ranks (based on total Meseta earned)
|
- Implement ranks (based on total Meseta earned)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1617,7 +1617,7 @@ static void proxy_command_item(shared_ptr<ProxyServer::LinkedSession> ses, const
|
|||||||
bool set_drop = (!args.empty() && (args[0] == '!'));
|
bool set_drop = (!args.empty() && (args[0] == '!'));
|
||||||
|
|
||||||
ItemData item = s->item_name_index->parse_item_description(ses->version(), (set_drop ? args.substr(1) : args));
|
ItemData item = s->item_name_index->parse_item_description(ses->version(), (set_drop ? args.substr(1) : args));
|
||||||
item.id = random_object<uint32_t>();
|
item.id = random_object<uint32_t>() | 0x80000000;
|
||||||
|
|
||||||
if (set_drop) {
|
if (set_drop) {
|
||||||
ses->next_drop_item = item;
|
ses->next_drop_item = item;
|
||||||
|
|||||||
+2
-4
@@ -2,13 +2,11 @@
|
|||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include <phosg/Random.hh>
|
|
||||||
|
|
||||||
#include "SendCommands.hh"
|
#include "SendCommands.hh"
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<PSOLFGEncryption> random_crypt) {
|
||||||
auto s = c->require_server_state();
|
auto s = c->require_server_state();
|
||||||
|
|
||||||
// On PC (and presumably DC), the client sends a 6x29 after this to delete the
|
// On PC (and presumably DC), the client sends a 6x29 after this to delete the
|
||||||
@@ -179,7 +177,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
|||||||
if (sum == 0) {
|
if (sum == 0) {
|
||||||
throw runtime_error("no unwrap results available for event");
|
throw runtime_error("no unwrap results available for event");
|
||||||
}
|
}
|
||||||
size_t det = random_object<size_t>() % sum;
|
size_t det = random_crypt->next() % sum;
|
||||||
for (size_t z = 0; z < table.second; z++) {
|
for (size_t z = 0; z < table.second; z++) {
|
||||||
const auto& entry = table.first[z];
|
const auto& entry = table.first[z];
|
||||||
if (det > entry.probability) {
|
if (det > entry.probability) {
|
||||||
|
|||||||
+2
-1
@@ -6,8 +6,9 @@
|
|||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
#include "Client.hh"
|
#include "Client.hh"
|
||||||
|
#include "PSOEncryption.hh"
|
||||||
#include "ServerState.hh"
|
#include "ServerState.hh"
|
||||||
#include "StaticGameData.hh"
|
#include "StaticGameData.hh"
|
||||||
|
|
||||||
void player_use_item(std::shared_ptr<Client> c, size_t item_index);
|
void player_use_item(std::shared_ptr<Client> c, size_t item_index, std::shared_ptr<PSOLFGEncryption> random_crypt);
|
||||||
void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fed_item_index);
|
void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fed_item_index);
|
||||||
|
|||||||
+1
-1
@@ -254,7 +254,7 @@ void Lobby::create_item_creator() {
|
|||||||
|
|
||||||
void Lobby::load_maps() {
|
void Lobby::load_maps() {
|
||||||
auto s = this->require_server_state();
|
auto s = this->require_server_state();
|
||||||
this->map = make_shared<Map>(this->lobby_id);
|
this->map = make_shared<Map>(this->lobby_id, this->random_crypt);
|
||||||
|
|
||||||
if (this->quest) {
|
if (this->quest) {
|
||||||
auto leader_c = this->clients.at(this->leader_id);
|
auto leader_c = this->clients.at(this->leader_id);
|
||||||
|
|||||||
+4
-3
@@ -131,8 +131,9 @@ string Map::Object::str(shared_ptr<const ItemNameIndex> name_index) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Map::Map(uint32_t lobby_id)
|
Map::Map(uint32_t lobby_id, std::shared_ptr<PSOLFGEncryption> random_crypt)
|
||||||
: log(string_printf("[Lobby:%08" PRIX32 ":map] ", lobby_id), lobby_log.min_level) {}
|
: log(string_printf("[Lobby:%08" PRIX32 ":map] ", lobby_id), lobby_log.min_level),
|
||||||
|
random_crypt(random_crypt) {}
|
||||||
|
|
||||||
void Map::clear() {
|
void Map::clear() {
|
||||||
this->objects.clear();
|
this->objects.clear();
|
||||||
@@ -167,7 +168,7 @@ bool Map::check_and_log_rare_enemy(bool default_is_rare, uint32_t rare_rate) {
|
|||||||
if (default_is_rare) {
|
if (default_is_rare) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if ((this->rare_enemy_indexes.size() < 0x10) && (random_object<uint32_t>() < rare_rate)) {
|
if ((this->rare_enemy_indexes.size() < 0x10) && (this->random_crypt->next() < rare_rate)) {
|
||||||
this->rare_enemy_indexes.emplace_back(this->enemies.size());
|
this->rare_enemy_indexes.emplace_back(this->enemies.size());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -253,7 +253,7 @@ struct Map {
|
|||||||
void generate_shuffled_location_table(const Map::RandomEnemyLocationsHeader& header, StringReader r, uint16_t section);
|
void generate_shuffled_location_table(const Map::RandomEnemyLocationsHeader& header, StringReader r, uint16_t section);
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit Map(uint32_t lobby_id);
|
Map(uint32_t lobby_id, std::shared_ptr<PSOLFGEncryption> random_crypt);
|
||||||
~Map() = default;
|
~Map() = default;
|
||||||
|
|
||||||
void clear();
|
void clear();
|
||||||
@@ -309,6 +309,7 @@ struct Map {
|
|||||||
static std::string disassemble_quest_data(const void* data, size_t size);
|
static std::string disassemble_quest_data(const void* data, size_t size);
|
||||||
|
|
||||||
PrefixedLogger log;
|
PrefixedLogger log;
|
||||||
|
std::shared_ptr<PSOLFGEncryption> random_crypt;
|
||||||
std::vector<Object> objects;
|
std::vector<Object> objects;
|
||||||
std::vector<Enemy> enemies;
|
std::vector<Enemy> enemies;
|
||||||
std::vector<size_t> rare_enemy_indexes;
|
std::vector<size_t> rare_enemy_indexes;
|
||||||
|
|||||||
@@ -1453,7 +1453,7 @@ static void on_use_item(
|
|||||||
const auto& item = p->inventory.items[index].data;
|
const auto& item = p->inventory.items[index].data;
|
||||||
name = s->describe_item(c->version(), item, false);
|
name = s->describe_item(c->version(), item, false);
|
||||||
}
|
}
|
||||||
player_use_item(c, index);
|
player_use_item(c, index, l->random_crypt);
|
||||||
|
|
||||||
if (l->log.should_log(LogLevel::INFO)) {
|
if (l->log.should_log(LogLevel::INFO)) {
|
||||||
l->log.info("Player %hhu used item %hu:%08" PRIX32 " (%s)",
|
l->log.info("Player %hhu used item %hu:%08" PRIX32 " (%s)",
|
||||||
@@ -2803,7 +2803,7 @@ static void on_secret_lottery_ticket_exchange_bb(shared_ptr<Client> c, uint8_t,
|
|||||||
|
|
||||||
ItemData item = (s->secret_lottery_results.size() == 1)
|
ItemData item = (s->secret_lottery_results.size() == 1)
|
||||||
? s->secret_lottery_results[0]
|
? s->secret_lottery_results[0]
|
||||||
: s->secret_lottery_results[random_object<uint32_t>() % s->secret_lottery_results.size()];
|
: s->secret_lottery_results[l->random_crypt->next() % s->secret_lottery_results.size()];
|
||||||
item.enforce_min_stack_size();
|
item.enforce_min_stack_size();
|
||||||
item.id = l->generate_item_id(c->lobby_client_id);
|
item.id = l->generate_item_id(c->lobby_client_id);
|
||||||
p->add_item(item);
|
p->add_item(item);
|
||||||
@@ -2819,7 +2819,7 @@ static void on_secret_lottery_ticket_exchange_bb(shared_ptr<Client> c, uint8_t,
|
|||||||
out_cmd.unknown_a3.clear(1);
|
out_cmd.unknown_a3.clear(1);
|
||||||
} else {
|
} else {
|
||||||
for (size_t z = 0; z < out_cmd.unknown_a3.size(); z++) {
|
for (size_t z = 0; z < out_cmd.unknown_a3.size(); z++) {
|
||||||
out_cmd.unknown_a3[z] = random_object<uint32_t>() % s->secret_lottery_results.size();
|
out_cmd.unknown_a3[z] = l->random_crypt->next() % s->secret_lottery_results.size();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
send_command_t(c, 0x24, (slt_index >= 0) ? 0 : 1, out_cmd);
|
send_command_t(c, 0x24, (slt_index >= 0) ? 0 : 1, out_cmd);
|
||||||
@@ -2849,7 +2849,7 @@ static void on_quest_F95E_result_bb(shared_ptr<Client> c, uint8_t, uint8_t, void
|
|||||||
if (results.empty()) {
|
if (results.empty()) {
|
||||||
throw runtime_error("invalid result type");
|
throw runtime_error("invalid result type");
|
||||||
}
|
}
|
||||||
ItemData item = (results.size() == 1) ? results[0] : results[random_object<uint32_t>() % results.size()];
|
ItemData item = (results.size() == 1) ? results[0] : results[l->random_crypt->next() % results.size()];
|
||||||
if (item.data1[0] == 0x04) { // Meseta
|
if (item.data1[0] == 0x04) { // Meseta
|
||||||
// TODO: What is the right amount of Meseta to use here? Presumably it
|
// TODO: What is the right amount of Meseta to use here? Presumably it
|
||||||
// should be random within a certain range, but it's not obvious what
|
// should be random within a certain range, but it's not obvious what
|
||||||
@@ -2926,10 +2926,10 @@ static void on_quest_F960_result_bb(shared_ptr<Client> c, uint8_t, uint8_t, void
|
|||||||
size_t tier = cmd.result_tier - num_failures;
|
size_t tier = cmd.result_tier - num_failures;
|
||||||
const auto& results = s->quest_F960_success_results.at(tier);
|
const auto& results = s->quest_F960_success_results.at(tier);
|
||||||
uint64_t probability = results.base_probability + num_failures * results.probability_upgrade;
|
uint64_t probability = results.base_probability + num_failures * results.probability_upgrade;
|
||||||
if (random_object<uint32_t>() <= probability) {
|
if (l->random_crypt->next() <= probability) {
|
||||||
c->log.info("Tier %zu yielded a prize", tier);
|
c->log.info("Tier %zu yielded a prize", tier);
|
||||||
const auto& result_items = results.results.at(weekday);
|
const auto& result_items = results.results.at(weekday);
|
||||||
item = result_items[random_object<uint32_t>() % result_items.size()];
|
item = result_items[l->random_crypt->next() % result_items.size()];
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
c->log.info("Tier %zu did not yield a prize", tier);
|
c->log.info("Tier %zu did not yield a prize", tier);
|
||||||
@@ -2938,7 +2938,7 @@ static void on_quest_F960_result_bb(shared_ptr<Client> c, uint8_t, uint8_t, void
|
|||||||
if (item.empty()) {
|
if (item.empty()) {
|
||||||
c->log.info("Choosing result from failure tier");
|
c->log.info("Choosing result from failure tier");
|
||||||
const auto& result_items = s->quest_F960_failure_results.results.at(weekday);
|
const auto& result_items = s->quest_F960_failure_results.results.at(weekday);
|
||||||
item = result_items[random_object<uint32_t>() % result_items.size()];
|
item = result_items[l->random_crypt->next() % result_items.size()];
|
||||||
}
|
}
|
||||||
if (item.empty()) {
|
if (item.empty()) {
|
||||||
throw runtime_error("no item produced, even from failure tier");
|
throw runtime_error("no item produced, even from failure tier");
|
||||||
|
|||||||
+2
-1
@@ -501,11 +501,12 @@ void send_pc_console_split_reconnect(shared_ptr<Client> c, uint32_t address,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void send_client_init_bb(shared_ptr<Client> c, uint32_t error_code) {
|
void send_client_init_bb(shared_ptr<Client> c, uint32_t error_code) {
|
||||||
|
auto team = c->team();
|
||||||
S_ClientInit_BB_00E6 cmd;
|
S_ClientInit_BB_00E6 cmd;
|
||||||
cmd.error_code = error_code;
|
cmd.error_code = error_code;
|
||||||
cmd.player_tag = 0x00010000;
|
cmd.player_tag = 0x00010000;
|
||||||
cmd.guild_card_number = c->license->serial_number;
|
cmd.guild_card_number = c->license->serial_number;
|
||||||
cmd.team_id = static_cast<uint32_t>(random_object<uint32_t>());
|
cmd.team_id = team ? team->team_id : 0;
|
||||||
c->config.serialize_into(cmd.client_config);
|
c->config.serialize_into(cmd.client_config);
|
||||||
cmd.can_create_team = 1;
|
cmd.can_create_team = 1;
|
||||||
cmd.episode_4_unlocked = 1;
|
cmd.episode_4_unlocked = 1;
|
||||||
|
|||||||
+1
-1
@@ -811,7 +811,7 @@ Proxy session commands:\n\
|
|||||||
|
|
||||||
auto s = ses->require_server_state();
|
auto s = ses->require_server_state();
|
||||||
ItemData item = s->item_name_index->parse_item_description(ses->version(), command_args);
|
ItemData item = s->item_name_index->parse_item_description(ses->version(), command_args);
|
||||||
item.id = random_object<uint32_t>();
|
item.id = random_object<uint32_t>() | 0x80000000;
|
||||||
|
|
||||||
if (command_name == "set-next-item") {
|
if (command_name == "set-next-item") {
|
||||||
ses->next_drop_item = item;
|
ses->next_drop_item = item;
|
||||||
|
|||||||
Reference in New Issue
Block a user