fix minor isses in battle replays

This commit is contained in:
Martin Michelsen
2023-09-24 23:17:22 -07:00
parent 9272feff8f
commit 949ad0d260
11 changed files with 56 additions and 34 deletions
+10 -5
View File
@@ -152,17 +152,20 @@ All quests, including those originally in GCI or DLQ format, are treated as onli
### Episode 3 features
The following Episode 3 features work well:
newserv supports many features unique to Episode 3:
* CARD battles. Not every combination of abilities has been tested yet, so if you find a feature or card ability that doesn't work like it's supposed to, please make a GitHub issue and describe the situation (the attacking card(s), defending card(s), and ability or condition that didn't work).
* Spectator teams.
* Tournaments. (But they don't work like Sega's tournaments did - see below)
* Tournaments. (But they work differently than Sega's tournaments did - see below)
* Downloading quests.
* Trading cards.
* Participating in card auctions. (The auction contents must be configured in config.json.)
The following Episode 3 features are implemented, but are only partially tested:
* Decorations in lobbies. Currently only images are supported; the game also supports loading custom 3D models in lobbies, but newserv does not implement this (yet).
* Battle replays also sometimes cause the client to crash during the replay. Using the $playrec command is therefore not recommended.
#### Battle records
After playing a battle, you can save the record of the battle with the $saverec command. You can then replay the battle later by using the $playrec command in a lobby - this will create a spectator team and play the recording of the battle as if it were happening in realtime. Note that there is a bug in older versions of Dolphin that seems to be frequently triggered when playing battle records, which causes the emulator to crash with the message `QObject::~QObject: Timers cannot be stopped from another thread`. To avoid this, use the latest version of Dolphin.
#### Tournaments
Tournaments work differently than they did on Sega's servers. Tournaments can be created with the `create-tournament` shell command, which enables players to register for them. (Use `help` to see all the arguments - there are many!) The `start-tournament` shell command starts the tournament (and prevents further registrations), but this doesn't schedule any matches. Instead, players who are ready to play their next match can all stand at the 4-player battle table near the lobby warp in the same CARD lobby, and the tournament match will start automatically.
@@ -170,6 +173,8 @@ These tournament semantics mean that there can be multiple matches in the same t
The Meseta rewards for winning tournament matches can be configured in config.json.
#### Episode 3 files
Episode 3 state and game data is stored in the system/ep3 directory. The files in there are:
* card-definitions.mnr: Compressed card definition list, sent to Episode 3 clients at connect time. Card stats and abilities can be changed by editing this file.
* card-definitions.mnrd: Decompressed version of the above. If present, newserv will use this instead of the compressed version, since this is easier to edit.
-2
View File
@@ -21,8 +21,6 @@
- Make disconnecting during a tournament match cause you to forfeit the match
- Enforce tournament deck restrictions (e.g. rarity checks, No Assist option) when populating COMs at tournament start time
- Spectator teams
- Send 6xB4x4C, etc. commands when joining a spectator team - maybe this makes it load faster?
- Figure out why spectators crash during replays sometimes
- Spectator teams sometimes stop receiving commands during live battles?
- It may be possible to send spectators back to the waiting room after a non-tournament battle by sending 6xB4x05 with environment 0x19, then 6xB4x3B again; try this
+4 -5
View File
@@ -542,20 +542,19 @@ static void proxy_command_lobby_type(shared_ptr<ProxyServer::LinkedSession> ses,
}
static void server_command_saverec(shared_ptr<Client> c, const std::u16string& args) {
auto l = c->require_lobby();
if (args.find(u'/') != string::npos) {
send_text_message(c, u"$C4Recording names\ncannot include\nthe / character");
return;
}
if (!l->prev_battle_record) {
if (!c->ep3_prev_battle_record) {
send_text_message(c, u"$C4No finished\nrecording is\npresent");
return;
}
string filename = "system/ep3/battle-records/" + encode_sjis(args) + ".mzrd";
string data = l->prev_battle_record->serialize();
string data = c->ep3_prev_battle_record->serialize();
save_file(filename, data);
send_text_message(l, u"$C7Recording saved");
l->prev_battle_record.reset();
send_text_message(c, u"$C7Recording saved");
c->ep3_prev_battle_record.reset();
}
static void server_command_playrec(shared_ptr<Client> c, const std::u16string& args) {
+1
View File
@@ -162,6 +162,7 @@ struct Client : public std::enable_shared_from_this<Client> {
uint16_t card_battle_table_seat_number;
uint16_t card_battle_table_seat_state;
std::weak_ptr<Episode3::Tournament::Team> ep3_tournament_team;
std::shared_ptr<Episode3::BattleRecord> ep3_prev_battle_record;
std::shared_ptr<const Menu> last_menu_sent;
// Miscellaneous (used by chat commands)
+4 -2
View File
@@ -127,7 +127,8 @@ const BattleRecord::Event* BattleRecord::get_first_event() const {
void BattleRecord::add_player(
const PlayerLobbyDataDCGC& lobby_data,
const PlayerInventory& inventory,
const PlayerDispDataDCPCV3& disp) {
const PlayerDispDataDCPCV3& disp,
uint32_t level) {
if (!this->is_writable) {
throw logic_error("cannot write to battle record");
}
@@ -141,6 +142,7 @@ void BattleRecord::add_player(
player.lobby_data = lobby_data;
player.inventory = inventory;
player.disp = disp;
player.level = level;
}
void BattleRecord::delete_player(uint8_t client_id) {
@@ -349,7 +351,7 @@ void BattleRecordPlayer::schedule_events() {
send_command(l, (ev.data.size() >= 0x400) ? 0x6C : 0x60, 0x00, ev.data);
break;
case BattleRecord::Event::Type::EP3_GAME_COMMAND:
send_command(l, 0xCB, 0x00, ev.data);
send_command(l, 0xC9, 0x00, ev.data);
break;
case BattleRecord::Event::Type::CHAT_MESSAGE:
send_chat_message(l, ev.guild_card_number, decode_sjis(ev.data));
+4 -2
View File
@@ -23,6 +23,7 @@ public:
PlayerLobbyDataDCGC lobby_data;
PlayerInventory inventory;
PlayerDispDataDCPCV3 disp;
uint32_t level;
} __attribute__((packed));
struct Event {
@@ -65,7 +66,8 @@ public:
void add_player(
const PlayerLobbyDataDCGC& lobby_data,
const PlayerInventory& inventory,
const PlayerDispDataDCPCV3& disp);
const PlayerDispDataDCPCV3& disp,
uint32_t level);
void delete_player(uint8_t client_id);
void add_command(Event::Type type, const void* data, size_t size);
void add_command(Event::Type type, std::string&& data);
@@ -78,7 +80,7 @@ public:
void set_battle_end_timestamp();
private:
static constexpr uint64_t SIGNATURE = 0x14C946D56D1DAC5A;
static constexpr uint64_t SIGNATURE = 0x14C946D56D1DAC50;
static bool is_map_definition_event(const Event& ev);
+2 -1
View File
@@ -144,7 +144,8 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
this->battle_record->add_player(
lobby_data,
c->game_data.player()->inventory,
c->game_data.player()->disp.to_dcpcv3());
c->game_data.player()->disp.to_dcpcv3(),
c->game_data.ep3_config ? (c->game_data.ep3_config->online_clv_exp / 100) : 0);
}
// Send spectator count notifications if needed
-1
View File
@@ -97,7 +97,6 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
std::weak_ptr<Lobby> watched_lobby; // Only used in watcher games
std::unordered_set<shared_ptr<Lobby>> watcher_lobbies; // Only used in primary games
std::shared_ptr<Episode3::BattleRecord> battle_record; // Not used in watcher games
std::shared_ptr<Episode3::BattleRecord> prev_battle_record; // Only used in primary games
std::shared_ptr<Episode3::BattleRecordPlayer> battle_player; // Only used in replay games
std::shared_ptr<Episode3::Tournament::Match> tournament_match;
std::shared_ptr<const G_SetEXResultValues_GC_Ep3_6xB4x4B> ep3_ex_result_values;
+19 -5
View File
@@ -1278,6 +1278,11 @@ static void on_CA_Ep3(shared_ptr<Client> c, uint16_t, uint32_t, const string& da
if (!l->is_game() || !l->is_ep3()) {
throw runtime_error("Episode 3 server data request sent outside of Episode 3 game");
}
if (l->battle_player) {
return;
}
auto s = l->require_server_state();
const auto& header = check_size_t<G_CardServerDataCommandHeader>(data, 0xFFFF);
@@ -1314,8 +1319,11 @@ static void on_CA_Ep3(shared_ptr<Client> c, uint16_t, uint32_t, const string& da
if (s->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_RECORDING) {
if (l->battle_record) {
l->prev_battle_record = l->battle_record;
l->prev_battle_record->set_battle_end_timestamp();
for (const auto& c : l->clients) {
if (c) {
c->ep3_prev_battle_record = l->battle_record;
}
}
}
l->battle_record.reset(new Episode3::BattleRecord(s->ep3_behavior_flags));
for (auto existing_c : l->clients) {
@@ -1324,18 +1332,24 @@ static void on_CA_Ep3(shared_ptr<Client> c, uint16_t, uint32_t, const string& da
lobby_data.name = encode_sjis(existing_c->game_data.player()->disp.name);
lobby_data.player_tag = 0x00010000;
lobby_data.guild_card = existing_c->license->serial_number;
l->battle_record->add_player(lobby_data,
l->battle_record->add_player(
lobby_data,
existing_c->game_data.player()->inventory,
existing_c->game_data.player()->disp.to_dcpcv3());
existing_c->game_data.player()->disp.to_dcpcv3(),
c->game_data.ep3_config ? (c->game_data.ep3_config->online_clv_exp / 100) : 0);
}
}
if (l->prev_battle_record) {
if (c->ep3_prev_battle_record) {
send_text_message(l, u"$C6Recording complete");
}
send_text_message(l, u"$C6Recording enabled");
}
}
bool battle_finished_before = l->ep3_server->battle_finished;
l->ep3_server->on_server_data_input(data);
if (!battle_finished_before && l->ep3_server->battle_finished && l->battle_record) {
l->battle_record->set_battle_end_timestamp();
}
if (l->tournament_match &&
l->ep3_server->setup_phase == Episode3::SetupPhase::BATTLE_ENDED &&
!l->ep3_server->tournament_match_result_sent) {
+4 -2
View File
@@ -1428,8 +1428,8 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
uint8_t player_count = 0;
auto watched_lobby = l->watched_lobby.lock();
if (watched_lobby) {
cmd.leader_id = watched_lobby->leader_id;
// Live spectating
cmd.leader_id = watched_lobby->leader_id;
for (size_t z = 0; z < 4; z++) {
auto& wc = watched_lobby->clients[z];
if (!wc) {
@@ -1480,7 +1480,6 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
throw runtime_error("invalid client id in battle record");
}
auto& p = cmd.players[client_id];
auto& e = cmd.entries[client_id];
p.lobby_data = entry.lobby_data;
remove_language_marker_inplace(p.lobby_data.name);
p.inventory = entry.inventory;
@@ -1489,12 +1488,15 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
}
p.disp = entry.disp;
remove_language_marker_inplace(p.disp.visual.name);
auto& e = cmd.entries[client_id];
e.player_tag = 0x00010000;
e.guild_card_number = entry.lobby_data.guild_card;
e.name = entry.disp.visual.name;
remove_language_marker_inplace(e.name);
e.present = 1;
e.level = entry.disp.stats.level.load();
player_count++;
}
+8 -9
View File
@@ -216,15 +216,14 @@
"StaticGameData": "INFO",
},
// If this option is disabled, the server only allows users who are registered
// in the license file to connect. If this is enabled, all users will be
// allowed to connect even if they're not registered in the license file. Each
// time a user connects with an unregistered license (serial number / access
// key combination, or username/password combination on BB), a temporary
// license is created for them, which lasts until the server is shut down.
// These temporary licenses are not saved to the license file. For BB, player
// and account data is still saved on the server, even for users with
// temporary licenses.
// If this option is disabled, the server only allows users who have licenses
// on the server to connect. If this is enabled, all users will be allowed to
// connect even if they don't have licenses. Each time a user connects with an
// unregistered license (serial number and access key combination, or username
// and password combination on BB), a temporary license is created for them,
// which lasts until the server is shut down. These temporary licenses are not
// saved in the licenses directory. For BB, player and account data is still
// saved on the server, even for users with temporary licenses.
"AllowUnregisteredUsers": true,
// User to run the server as. If present, newserv will attempt to switch to