fix minor isses in battle replays
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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++;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user