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 ### 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). * 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. * 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. * Downloading quests.
* Trading cards. * Trading cards.
* Participating in card auctions. (The auction contents must be configured in config.json.) * 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). * 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. 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. 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: 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.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. * 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 - 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 - Enforce tournament deck restrictions (e.g. rarity checks, No Assist option) when populating COMs at tournament start time
- Spectator teams - 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? - 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 - 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) { static void server_command_saverec(shared_ptr<Client> c, const std::u16string& args) {
auto l = c->require_lobby();
if (args.find(u'/') != string::npos) { if (args.find(u'/') != string::npos) {
send_text_message(c, u"$C4Recording names\ncannot include\nthe / character"); send_text_message(c, u"$C4Recording names\ncannot include\nthe / character");
return; return;
} }
if (!l->prev_battle_record) { if (!c->ep3_prev_battle_record) {
send_text_message(c, u"$C4No finished\nrecording is\npresent"); send_text_message(c, u"$C4No finished\nrecording is\npresent");
return; return;
} }
string filename = "system/ep3/battle-records/" + encode_sjis(args) + ".mzrd"; 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); save_file(filename, data);
send_text_message(l, u"$C7Recording saved"); send_text_message(c, u"$C7Recording saved");
l->prev_battle_record.reset(); c->ep3_prev_battle_record.reset();
} }
static void server_command_playrec(shared_ptr<Client> c, const std::u16string& args) { 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_number;
uint16_t card_battle_table_seat_state; uint16_t card_battle_table_seat_state;
std::weak_ptr<Episode3::Tournament::Team> ep3_tournament_team; 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; std::shared_ptr<const Menu> last_menu_sent;
// Miscellaneous (used by chat commands) // 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( void BattleRecord::add_player(
const PlayerLobbyDataDCGC& lobby_data, const PlayerLobbyDataDCGC& lobby_data,
const PlayerInventory& inventory, const PlayerInventory& inventory,
const PlayerDispDataDCPCV3& disp) { const PlayerDispDataDCPCV3& disp,
uint32_t level) {
if (!this->is_writable) { if (!this->is_writable) {
throw logic_error("cannot write to battle record"); throw logic_error("cannot write to battle record");
} }
@@ -141,6 +142,7 @@ void BattleRecord::add_player(
player.lobby_data = lobby_data; player.lobby_data = lobby_data;
player.inventory = inventory; player.inventory = inventory;
player.disp = disp; player.disp = disp;
player.level = level;
} }
void BattleRecord::delete_player(uint8_t client_id) { 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); send_command(l, (ev.data.size() >= 0x400) ? 0x6C : 0x60, 0x00, ev.data);
break; break;
case BattleRecord::Event::Type::EP3_GAME_COMMAND: case BattleRecord::Event::Type::EP3_GAME_COMMAND:
send_command(l, 0xCB, 0x00, ev.data); send_command(l, 0xC9, 0x00, ev.data);
break; break;
case BattleRecord::Event::Type::CHAT_MESSAGE: case BattleRecord::Event::Type::CHAT_MESSAGE:
send_chat_message(l, ev.guild_card_number, decode_sjis(ev.data)); send_chat_message(l, ev.guild_card_number, decode_sjis(ev.data));
+4 -2
View File
@@ -23,6 +23,7 @@ public:
PlayerLobbyDataDCGC lobby_data; PlayerLobbyDataDCGC lobby_data;
PlayerInventory inventory; PlayerInventory inventory;
PlayerDispDataDCPCV3 disp; PlayerDispDataDCPCV3 disp;
uint32_t level;
} __attribute__((packed)); } __attribute__((packed));
struct Event { struct Event {
@@ -65,7 +66,8 @@ public:
void add_player( void add_player(
const PlayerLobbyDataDCGC& lobby_data, const PlayerLobbyDataDCGC& lobby_data,
const PlayerInventory& inventory, const PlayerInventory& inventory,
const PlayerDispDataDCPCV3& disp); const PlayerDispDataDCPCV3& disp,
uint32_t level);
void delete_player(uint8_t client_id); 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, const void* data, size_t size);
void add_command(Event::Type type, std::string&& data); void add_command(Event::Type type, std::string&& data);
@@ -78,7 +80,7 @@ public:
void set_battle_end_timestamp(); void set_battle_end_timestamp();
private: private:
static constexpr uint64_t SIGNATURE = 0x14C946D56D1DAC5A; static constexpr uint64_t SIGNATURE = 0x14C946D56D1DAC50;
static bool is_map_definition_event(const Event& ev); 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( this->battle_record->add_player(
lobby_data, lobby_data,
c->game_data.player()->inventory, 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 // 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::weak_ptr<Lobby> watched_lobby; // Only used in watcher games
std::unordered_set<shared_ptr<Lobby>> watcher_lobbies; // Only used in primary 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> 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::BattleRecordPlayer> battle_player; // Only used in replay games
std::shared_ptr<Episode3::Tournament::Match> tournament_match; std::shared_ptr<Episode3::Tournament::Match> tournament_match;
std::shared_ptr<const G_SetEXResultValues_GC_Ep3_6xB4x4B> ep3_ex_result_values; 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()) { if (!l->is_game() || !l->is_ep3()) {
throw runtime_error("Episode 3 server data request sent outside of Episode 3 game"); 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(); auto s = l->require_server_state();
const auto& header = check_size_t<G_CardServerDataCommandHeader>(data, 0xFFFF); 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 (s->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_RECORDING) {
if (l->battle_record) { if (l->battle_record) {
l->prev_battle_record = l->battle_record; for (const auto& c : l->clients) {
l->prev_battle_record->set_battle_end_timestamp(); if (c) {
c->ep3_prev_battle_record = l->battle_record;
}
}
} }
l->battle_record.reset(new Episode3::BattleRecord(s->ep3_behavior_flags)); l->battle_record.reset(new Episode3::BattleRecord(s->ep3_behavior_flags));
for (auto existing_c : l->clients) { 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.name = encode_sjis(existing_c->game_data.player()->disp.name);
lobby_data.player_tag = 0x00010000; lobby_data.player_tag = 0x00010000;
lobby_data.guild_card = existing_c->license->serial_number; 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()->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 complete");
} }
send_text_message(l, u"$C6Recording enabled"); send_text_message(l, u"$C6Recording enabled");
} }
} }
bool battle_finished_before = l->ep3_server->battle_finished;
l->ep3_server->on_server_data_input(data); 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 && if (l->tournament_match &&
l->ep3_server->setup_phase == Episode3::SetupPhase::BATTLE_ENDED && l->ep3_server->setup_phase == Episode3::SetupPhase::BATTLE_ENDED &&
!l->ep3_server->tournament_match_result_sent) { !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; uint8_t player_count = 0;
auto watched_lobby = l->watched_lobby.lock(); auto watched_lobby = l->watched_lobby.lock();
if (watched_lobby) { if (watched_lobby) {
cmd.leader_id = watched_lobby->leader_id;
// Live spectating // Live spectating
cmd.leader_id = watched_lobby->leader_id;
for (size_t z = 0; z < 4; z++) { for (size_t z = 0; z < 4; z++) {
auto& wc = watched_lobby->clients[z]; auto& wc = watched_lobby->clients[z];
if (!wc) { 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"); throw runtime_error("invalid client id in battle record");
} }
auto& p = cmd.players[client_id]; auto& p = cmd.players[client_id];
auto& e = cmd.entries[client_id];
p.lobby_data = entry.lobby_data; p.lobby_data = entry.lobby_data;
remove_language_marker_inplace(p.lobby_data.name); remove_language_marker_inplace(p.lobby_data.name);
p.inventory = entry.inventory; 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; p.disp = entry.disp;
remove_language_marker_inplace(p.disp.visual.name); remove_language_marker_inplace(p.disp.visual.name);
auto& e = cmd.entries[client_id];
e.player_tag = 0x00010000; e.player_tag = 0x00010000;
e.guild_card_number = entry.lobby_data.guild_card; e.guild_card_number = entry.lobby_data.guild_card;
e.name = entry.disp.visual.name; e.name = entry.disp.visual.name;
remove_language_marker_inplace(e.name); remove_language_marker_inplace(e.name);
e.present = 1; e.present = 1;
e.level = entry.disp.stats.level.load(); e.level = entry.disp.stats.level.load();
player_count++; player_count++;
} }
+8 -9
View File
@@ -216,15 +216,14 @@
"StaticGameData": "INFO", "StaticGameData": "INFO",
}, },
// If this option is disabled, the server only allows users who are registered // If this option is disabled, the server only allows users who have licenses
// in the license file to connect. If this is enabled, all users will be // on the server to connect. If this is enabled, all users will be allowed to
// allowed to connect even if they're not registered in the license file. Each // connect even if they don't have licenses. Each time a user connects with an
// time a user connects with an unregistered license (serial number / access // unregistered license (serial number and access key combination, or username
// key combination, or username/password combination on BB), a temporary // and password combination on BB), a temporary license is created for them,
// license is created for them, which lasts until the server is shut down. // which lasts until the server is shut down. These temporary licenses are not
// These temporary licenses are not saved to the license file. For BB, player // saved in the licenses directory. For BB, player and account data is still
// and account data is still saved on the server, even for users with // saved on the server, even for users with temporary licenses.
// temporary licenses.
"AllowUnregisteredUsers": true, "AllowUnregisteredUsers": true,
// User to run the server as. If present, newserv will attempt to switch to // User to run the server as. If present, newserv will attempt to switch to