From 949ad0d2609cb8b8347e4b39cd0979ead67da422 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 24 Sep 2023 23:17:22 -0700 Subject: [PATCH] fix minor isses in battle replays --- README.md | 15 ++++++++++----- TODO.md | 2 -- src/ChatCommands.cc | 9 ++++----- src/Client.hh | 1 + src/Episode3/BattleRecord.cc | 6 ++++-- src/Episode3/BattleRecord.hh | 6 ++++-- src/Lobby.cc | 3 ++- src/Lobby.hh | 1 - src/ReceiveCommands.cc | 24 +++++++++++++++++++----- src/SendCommands.cc | 6 ++++-- system/config.example.json | 17 ++++++++--------- 11 files changed, 56 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 7d26d3ae..ced614b7 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/TODO.md b/TODO.md index 944f541c..b3d42a24 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 4ff1f35e..5cbfa186 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -542,20 +542,19 @@ static void proxy_command_lobby_type(shared_ptr ses, } static void server_command_saverec(shared_ptr 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 c, const std::u16string& args) { diff --git a/src/Client.hh b/src/Client.hh index 2547a39e..993cf8bd 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -162,6 +162,7 @@ struct Client : public std::enable_shared_from_this { uint16_t card_battle_table_seat_number; uint16_t card_battle_table_seat_state; std::weak_ptr ep3_tournament_team; + std::shared_ptr ep3_prev_battle_record; std::shared_ptr last_menu_sent; // Miscellaneous (used by chat commands) diff --git a/src/Episode3/BattleRecord.cc b/src/Episode3/BattleRecord.cc index 193bcb4b..5937868c 100644 --- a/src/Episode3/BattleRecord.cc +++ b/src/Episode3/BattleRecord.cc @@ -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)); diff --git a/src/Episode3/BattleRecord.hh b/src/Episode3/BattleRecord.hh index 9b5bec9f..8c6a2e18 100644 --- a/src/Episode3/BattleRecord.hh +++ b/src/Episode3/BattleRecord.hh @@ -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); diff --git a/src/Lobby.cc b/src/Lobby.cc index b7e7f570..779893ac 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -144,7 +144,8 @@ void Lobby::add_client(shared_ptr 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 diff --git a/src/Lobby.hh b/src/Lobby.hh index 7a50f80f..31792d87 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -97,7 +97,6 @@ struct Lobby : public std::enable_shared_from_this { std::weak_ptr watched_lobby; // Only used in watcher games std::unordered_set> watcher_lobbies; // Only used in primary games std::shared_ptr battle_record; // Not used in watcher games - std::shared_ptr prev_battle_record; // Only used in primary games std::shared_ptr battle_player; // Only used in replay games std::shared_ptr tournament_match; std::shared_ptr ep3_ex_result_values; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 26dc5935..25166be0 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -1278,6 +1278,11 @@ static void on_CA_Ep3(shared_ptr 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(data, 0xFFFF); @@ -1314,8 +1319,11 @@ static void on_CA_Ep3(shared_ptr 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 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) { diff --git a/src/SendCommands.cc b/src/SendCommands.cc index ce77389e..3748bbb6 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1428,8 +1428,8 @@ static void send_join_spectator_team(shared_ptr c, shared_ptr 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 c, shared_ptr 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 c, shared_ptr 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++; } diff --git a/system/config.example.json b/system/config.example.json index acde970f..180f7cb8 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -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