initial spectator + recording implementation
This commit is contained in:
@@ -49,6 +49,7 @@ add_executable(newserv
|
||||
src/Compression.cc
|
||||
src/DNSServer.cc
|
||||
src/Episode3/AssistServer.cc
|
||||
src/Episode3/BattleRecord.cc
|
||||
src/Episode3/Card.cc
|
||||
src/Episode3/CardSpecial.cc
|
||||
src/Episode3/DataIndex.cc
|
||||
|
||||
@@ -82,6 +82,10 @@ struct Channel {
|
||||
// Sends a message with an automatically-constructed header.
|
||||
void send(uint16_t cmd, uint32_t flag = 0, const void* data = nullptr, size_t size = 0, bool print_contents = true);
|
||||
void send(uint16_t cmd, uint32_t flag, const std::string& data, bool print_contents = true);
|
||||
template <typename CmdT>
|
||||
void send(uint16_t cmd, uint32_t flag, const CmdT& data) {
|
||||
this->send(cmd, flag, &data, sizeof(data));
|
||||
}
|
||||
|
||||
// Sends a message with a pre-existing header (as the first few bytes in the
|
||||
// data)
|
||||
|
||||
@@ -392,6 +392,50 @@ static void server_command_lobby_type(shared_ptr<ServerState>, shared_ptr<Lobby>
|
||||
}
|
||||
}
|
||||
|
||||
static void server_command_saverec(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
if (args.find(u'/') != string::npos) {
|
||||
send_text_message(c, u"$C4Recording names\ncannot include\nthe / character");
|
||||
return;
|
||||
}
|
||||
if (!l->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();
|
||||
save_file(filename, data);
|
||||
send_text_message(l, u"$C7Recording saved");
|
||||
l->prev_battle_record.reset();
|
||||
}
|
||||
|
||||
static void server_command_playrec(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
if (!(c->flags & Client::Flag::IS_EPISODE_3)) {
|
||||
send_text_message(c, u"$C4This command can\nonly be used on\nEpisode 3");
|
||||
return;
|
||||
}
|
||||
if (args.find(u'/') != string::npos) {
|
||||
send_text_message(c, u"$C4Recording names\ncannot include\nthe / character");
|
||||
return;
|
||||
}
|
||||
|
||||
if (l->is_game() && (l->flags & Lobby::Flag::EPISODE_3_ONLY) && (l->flags & Lobby::Flag::IS_SPECTATOR_TEAM) && l->battle_player) {
|
||||
l->flags |= Lobby::Flag::BATTLE_IN_PROGRESS;
|
||||
l->battle_player->start();
|
||||
|
||||
} else if (args.empty()) {
|
||||
c->next_game_battle_record.reset();
|
||||
send_text_message(c, u"$C6Replay state\ncleared");
|
||||
|
||||
} else {
|
||||
string filename = "system/ep3/battle-records/" + encode_sjis(args) + ".mzrd";
|
||||
string data = load_file(filename);
|
||||
c->next_game_battle_record.reset(new Episode3::BattleRecord(data));
|
||||
send_text_message(c, u"$C6Replay state set");
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Game commands
|
||||
|
||||
@@ -1013,7 +1057,9 @@ static const unordered_map<u16string, ChatCommandDefinition> chat_commands({
|
||||
{u"$password", {server_command_password, nullptr, u"Usage:\nlock [password]\nomit password to\nunlock game"}},
|
||||
{u"$patch", {server_command_patch, proxy_command_patch, u"Usage:\npatch <name>"}},
|
||||
{u"$persist", {server_command_persist, nullptr, u"Usage:\npersist"}},
|
||||
{u"$playrec", {server_command_playrec, nullptr, u"Usage:\nplayrec <filename>"}},
|
||||
{u"$rand", {server_command_rand, proxy_command_rand, u"Usage:\nrand [hex seed]\nomit seed to revert\nto default"}},
|
||||
{u"$saverec", {server_command_saverec, nullptr, u"Usage:\nsaverec <filename>"}},
|
||||
{u"$secid", {server_command_secid, proxy_command_secid, u"Usage:\nsecid [section ID]\nomit section ID to\nrevert to normal"}},
|
||||
{u"$silence", {server_command_silence, nullptr, u"Usage:\nsilence <name-or-number>"}},
|
||||
// TODO: implement this on proxy server
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "PSOEncryption.hh"
|
||||
#include "PSOProtocol.hh"
|
||||
#include "Text.hh"
|
||||
#include "Episode3/BattleRecord.hh"
|
||||
|
||||
|
||||
|
||||
@@ -124,6 +125,7 @@ struct Client {
|
||||
bool can_chat;
|
||||
std::string pending_bb_save_username;
|
||||
uint8_t pending_bb_save_player_index;
|
||||
std::shared_ptr<const Episode3::BattleRecord> next_game_battle_record;
|
||||
|
||||
bool proxy_block_events;
|
||||
bool proxy_block_function_calls;
|
||||
|
||||
+18
-7
@@ -2513,15 +2513,26 @@ struct S_JoinSpectatorTeam_GC_Ep3_E8 {
|
||||
PlayerDispDataDCPCV3 disp; // 0xD0 bytes
|
||||
} __packed__; // 0x43C bytes
|
||||
parray<PlayerEntry, 4> players; // 84-1174
|
||||
parray<uint8_t, 8> unknown_a2; // 1174-117C
|
||||
le_uint32_t unknown_a3 = 0; // 117C-1180
|
||||
parray<uint8_t, 4> unknown_a4; // 1180-1184
|
||||
uint8_t client_id = 0;
|
||||
uint8_t leader_id = 0;
|
||||
uint8_t disable_udp = 1;
|
||||
uint8_t difficulty = 0;
|
||||
uint8_t battle_mode = 0;
|
||||
uint8_t event = 0;
|
||||
uint8_t section_id = 0;
|
||||
uint8_t challenge_mode = 0;
|
||||
le_uint32_t rare_seed = 0;
|
||||
uint8_t episode = 0;
|
||||
uint8_t unused2 = 1;
|
||||
uint8_t solo_mode = 0;
|
||||
uint8_t unused3 = 0;
|
||||
struct SpectatorEntry {
|
||||
le_uint32_t player_tag = 0x00010000;
|
||||
le_uint32_t player_tag = 0;
|
||||
le_uint32_t guild_card_number = 0;
|
||||
ptext<char, 0x20> name;
|
||||
parray<uint8_t, 2> unknown_a3;
|
||||
le_uint16_t unknown_a4 = 0;
|
||||
uint8_t present = 0;
|
||||
uint8_t unknown_a3 = 0;
|
||||
le_uint16_t level = 0;
|
||||
parray<le_uint32_t, 2> unknown_a5;
|
||||
parray<le_uint16_t, 2> unknown_a6;
|
||||
} __packed__; // 0x38 bytes
|
||||
@@ -2529,7 +2540,7 @@ struct S_JoinSpectatorTeam_GC_Ep3_E8 {
|
||||
// battle - they appear in the first positions. Presumably the first 4 are
|
||||
// always for battlers, and the last 8 are always for spectators.
|
||||
parray<SpectatorEntry, 12> entries; // 1184-1424
|
||||
ptext<uint8_t, 0x20> spectator_team_name;
|
||||
ptext<char, 0x20> spectator_team_name;
|
||||
// This field doesn't appear to be actually used by the game, but some servers
|
||||
// send it anyway (and the game presumably ignores it)
|
||||
parray<PlayerEntry, 8> spectator_players;
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
#include "BattleRecord.hh"
|
||||
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "../CommandFormats.hh"
|
||||
#include "../SendCommands.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
BattleRecord::Event::Event(StringReader& r) {
|
||||
this->type = r.get<Event::Type>();
|
||||
this->timestamp = r.get_u64l();
|
||||
switch (this->type) {
|
||||
case Event::Type::PLAYER_JOIN:
|
||||
this->players.emplace_back(r.get<PlayerEntry>());
|
||||
break;
|
||||
case Event::Type::PLAYER_LEAVE:
|
||||
this->leaving_client_id = r.get_u8();
|
||||
break;
|
||||
case Event::Type::SET_INITIAL_PLAYERS: {
|
||||
uint8_t count = r.get_u8();
|
||||
while (this->players.size() < count) {
|
||||
this->players.emplace_back(r.get<PlayerEntry>());
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Event::Type::CHAT_MESSAGE:
|
||||
this->guild_card_number = r.get_u32l();
|
||||
[[fallthrough]];
|
||||
case Event::Type::GAME_COMMAND:
|
||||
case Event::Type::BATTLE_COMMAND:
|
||||
case Event::Type::EP3_GAME_COMMAND:
|
||||
this->data = r.read(r.get_u16l());
|
||||
break;
|
||||
default:
|
||||
throw logic_error("unknown event type");
|
||||
}
|
||||
}
|
||||
|
||||
void BattleRecord::Event::serialize(StringWriter& w) const {
|
||||
w.put(this->type);
|
||||
w.put_u64l(this->timestamp);
|
||||
switch (this->type) {
|
||||
case Event::Type::PLAYER_JOIN:
|
||||
if (this->players.size() != 1) {
|
||||
throw logic_error("player join event does not contain 1 player entry");
|
||||
}
|
||||
w.put(this->players[0]);
|
||||
break;
|
||||
case Event::Type::PLAYER_LEAVE:
|
||||
w.put_u8(this->leaving_client_id);
|
||||
break;
|
||||
case Event::Type::SET_INITIAL_PLAYERS:
|
||||
w.put_u8(this->players.size());
|
||||
for (const auto& player : this->players) {
|
||||
w.put(player);
|
||||
}
|
||||
break;
|
||||
case Event::Type::CHAT_MESSAGE:
|
||||
w.put_u32l(this->guild_card_number);
|
||||
[[fallthrough]];
|
||||
case Event::Type::GAME_COMMAND:
|
||||
case Event::Type::BATTLE_COMMAND:
|
||||
case Event::Type::EP3_GAME_COMMAND:
|
||||
w.put_u16l(this->data.size());
|
||||
w.write(this->data);
|
||||
break;
|
||||
default:
|
||||
throw logic_error("unknown event type");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
BattleRecord::BattleRecord(uint32_t behavior_flags)
|
||||
: is_writable(true),
|
||||
behavior_flags(behavior_flags),
|
||||
battle_start_timestamp(0),
|
||||
battle_end_timestamp(0) { }
|
||||
|
||||
BattleRecord::BattleRecord(const string& data)
|
||||
: is_writable(false),
|
||||
behavior_flags(0),
|
||||
battle_start_timestamp(0),
|
||||
battle_end_timestamp(0) {
|
||||
StringReader r(data);
|
||||
uint64_t signature = r.get_u64l();
|
||||
if (signature != this->SIGNATURE) {
|
||||
throw runtime_error("incorrect battle record signature");
|
||||
}
|
||||
|
||||
this->battle_start_timestamp = r.get_u64l();
|
||||
this->battle_end_timestamp = r.get_u64l();
|
||||
this->behavior_flags = r.get_u32l();
|
||||
while (!r.eof()) {
|
||||
this->events.emplace_back(r);
|
||||
}
|
||||
}
|
||||
|
||||
string BattleRecord::serialize() const {
|
||||
StringWriter w;
|
||||
w.put_u64l(this->SIGNATURE);
|
||||
w.put_u64l(this->battle_start_timestamp);
|
||||
w.put_u64l(this->battle_end_timestamp);
|
||||
w.put_u32l(this->behavior_flags);
|
||||
for (const auto& ev : this->events) {
|
||||
ev.serialize(w);
|
||||
}
|
||||
return move(w.str());
|
||||
}
|
||||
|
||||
bool BattleRecord::writable() const {
|
||||
return this->is_writable;
|
||||
}
|
||||
|
||||
bool BattleRecord::battle_in_progress() const {
|
||||
return (this->battle_start_timestamp != 0);
|
||||
}
|
||||
|
||||
const BattleRecord::Event* BattleRecord::get_first_event() const {
|
||||
if (this->events.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &this->events.front();
|
||||
}
|
||||
|
||||
|
||||
void BattleRecord::add_player(
|
||||
const PlayerLobbyDataDCGC& lobby_data,
|
||||
const PlayerInventory& inventory,
|
||||
const PlayerDispDataDCPCV3& disp) {
|
||||
if (!this->is_writable) {
|
||||
throw logic_error("cannot write to battle record");
|
||||
}
|
||||
if (this->battle_start_timestamp != 0) {
|
||||
throw runtime_error("cannot add player during battle");
|
||||
}
|
||||
Event& ev = this->events.emplace_back();
|
||||
ev.type = Event::Type::PLAYER_JOIN;
|
||||
ev.timestamp = now();
|
||||
auto& player = ev.players.emplace_back();
|
||||
player.lobby_data = lobby_data;
|
||||
player.inventory = inventory;
|
||||
player.disp = disp;
|
||||
}
|
||||
|
||||
void BattleRecord::delete_player(uint8_t client_id) {
|
||||
if (!this->is_writable) {
|
||||
throw logic_error("cannot write to battle record");
|
||||
}
|
||||
Event& ev = this->events.emplace_back();
|
||||
ev.type = Event::Type::PLAYER_LEAVE;
|
||||
ev.timestamp = now();
|
||||
ev.leaving_client_id = client_id;
|
||||
}
|
||||
|
||||
void BattleRecord::add_command(Event::Type type, const void* data, size_t size) {
|
||||
if (!this->is_writable) {
|
||||
throw logic_error("cannot write to battle record");
|
||||
}
|
||||
Event& ev = this->events.emplace_back();
|
||||
ev.type = type;
|
||||
ev.timestamp = now();
|
||||
ev.data.assign(reinterpret_cast<const char*>(data), size);
|
||||
}
|
||||
|
||||
void BattleRecord::add_command(Event::Type type, string&& data) {
|
||||
if (!this->is_writable) {
|
||||
throw logic_error("cannot write to battle record");
|
||||
}
|
||||
Event& ev = this->events.emplace_back();
|
||||
ev.type = type;
|
||||
ev.timestamp = now();
|
||||
ev.data = move(data);
|
||||
}
|
||||
|
||||
void BattleRecord::add_chat_message(
|
||||
uint32_t guild_card_number, string&& data) {
|
||||
if (!this->is_writable) {
|
||||
throw logic_error("cannot write to battle record");
|
||||
}
|
||||
Event& ev = this->events.emplace_back();
|
||||
ev.type = Event::Type::CHAT_MESSAGE;
|
||||
ev.timestamp = now();
|
||||
ev.guild_card_number = guild_card_number;
|
||||
ev.data = move(data);
|
||||
}
|
||||
|
||||
bool BattleRecord::is_map_definition_event(const Event& ev) {
|
||||
if (ev.type == Event::Type::BATTLE_COMMAND) {
|
||||
auto& header = check_size_t<G_CardBattleCommandHeader>(
|
||||
ev.data, sizeof(G_CardBattleCommandHeader), 0xFFFF);
|
||||
if (header.subcommand == 0xB6) {
|
||||
auto& header = check_size_t<G_MapSubsubcommand_GC_Ep3_6xB6>(
|
||||
ev.data, sizeof(G_MapSubsubcommand_GC_Ep3_6xB6), 0xFFFF);
|
||||
if (header.subsubcommand == 0x41) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BattleRecord::set_battle_start_timestamp() {
|
||||
if (this->battle_start_timestamp != 0) {
|
||||
throw logic_error("battle start timestamp is already set");
|
||||
}
|
||||
this->battle_start_timestamp = now();
|
||||
|
||||
// First, find the correct map definition subcommand to keep, and execute
|
||||
// player join/leave events to get the present players
|
||||
size_t num_map_events = 0;
|
||||
PlayerEntry players[4];
|
||||
bool players_present[4];
|
||||
for (auto& ev : this->events) {
|
||||
if (ev.type == Event::Type::PLAYER_JOIN) {
|
||||
if (ev.players.size() != 1) {
|
||||
throw logic_error("player join event does not contain 1 player entry");
|
||||
}
|
||||
auto& player = ev.players[0];
|
||||
if (player.lobby_data.client_id >= 4) {
|
||||
throw runtime_error("invalid client ID");
|
||||
}
|
||||
players[player.lobby_data.client_id] = player;
|
||||
players_present[player.lobby_data.client_id] = true;
|
||||
|
||||
} else if (ev.type == Event::Type::PLAYER_LEAVE) {
|
||||
if (ev.leaving_client_id >= 4) {
|
||||
throw logic_error("invalid client ID");
|
||||
}
|
||||
players_present[ev.leaving_client_id] = false;
|
||||
|
||||
} else if (ev.type == Event::Type::SET_INITIAL_PLAYERS) {
|
||||
throw logic_error("BattleRecord::set_battle_start_timestamp called twice");
|
||||
|
||||
} else if (this->is_map_definition_event(ev)) {
|
||||
num_map_events++;
|
||||
}
|
||||
}
|
||||
|
||||
deque<Event> new_events;
|
||||
|
||||
// Generate the initial players event
|
||||
Event initial_ev;
|
||||
initial_ev.type = Event::Type::SET_INITIAL_PLAYERS;
|
||||
initial_ev.timestamp = this->battle_start_timestamp;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
if (players_present[z]) {
|
||||
initial_ev.players.emplace_back(players[z]);
|
||||
}
|
||||
}
|
||||
new_events.emplace_back(move(initial_ev));
|
||||
|
||||
// Skip all events before the last map definition event, and only retain
|
||||
// battle commands between then and now (since these battle commands will all
|
||||
// be replayed at once)
|
||||
auto it = this->events.begin();
|
||||
for (; it != this->events.end(); it++) {
|
||||
if (this->is_map_definition_event(*it)) {
|
||||
num_map_events--;
|
||||
if (num_map_events == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (; it != this->events.end(); it++) {
|
||||
if (it->type == Event::Type::BATTLE_COMMAND) {
|
||||
new_events.emplace_back(move(*it));
|
||||
}
|
||||
}
|
||||
this->events = move(new_events);
|
||||
}
|
||||
|
||||
void BattleRecord::set_battle_end_timestamp() {
|
||||
this->battle_end_timestamp = now();
|
||||
}
|
||||
|
||||
|
||||
|
||||
BattleRecordPlayer::BattleRecordPlayer(
|
||||
shared_ptr<const BattleRecord> rec,
|
||||
shared_ptr<struct event_base> base,
|
||||
shared_ptr<Lobby> l)
|
||||
: record(rec),
|
||||
event_it(this->record->events.begin()),
|
||||
play_start_timestamp(0),
|
||||
base(base),
|
||||
lobby(l),
|
||||
next_command_ev(event_new(this->base.get(), -1, EV_TIMEOUT, &BattleRecordPlayer::dispatch_schedule_events, this), event_free) { }
|
||||
|
||||
shared_ptr<const BattleRecord> BattleRecordPlayer::get_record() const {
|
||||
return this->record;
|
||||
}
|
||||
|
||||
void BattleRecordPlayer::start() {
|
||||
this->play_start_timestamp = now();
|
||||
this->schedule_events();
|
||||
}
|
||||
|
||||
void BattleRecordPlayer::dispatch_schedule_events(
|
||||
evutil_socket_t, short, void* ctx) {
|
||||
reinterpret_cast<BattleRecordPlayer*>(ctx)->schedule_events();
|
||||
}
|
||||
|
||||
void BattleRecordPlayer::schedule_events() {
|
||||
// If the lobby is destroyed, we can't replay anything - just return without
|
||||
// rescheduling
|
||||
auto l = this->lobby.lock();
|
||||
if (!l) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
uint64_t relative_ts = now() - this->play_start_timestamp + this->record->battle_start_timestamp;
|
||||
|
||||
if (this->event_it == this->record->events.end()) {
|
||||
if (relative_ts >= this->record->battle_end_timestamp) {
|
||||
// If the record is complete and the end timestamp has been reached, so
|
||||
// send exit commands to all players in the lobby, and don't reschedule
|
||||
// the event (it will be deleted along with the Player when the lobby is
|
||||
// destroyed, when the last client leaves)
|
||||
send_command(l, 0xED, 0x00);
|
||||
|
||||
} else {
|
||||
// There are no more events to play, but the battle has not officially
|
||||
// ended yet - reschedule the event for the end time
|
||||
auto tv = usecs_to_timeval(this->record->battle_end_timestamp - relative_ts);
|
||||
event_add(this->next_command_ev.get(), &tv);
|
||||
}
|
||||
break;
|
||||
|
||||
} else {
|
||||
if (this->event_it->timestamp <= relative_ts) {
|
||||
// Play the next event
|
||||
auto& ev = *this->event_it;
|
||||
switch (ev.type) {
|
||||
case BattleRecord::Event::Type::PLAYER_JOIN:
|
||||
// Technically we can support this, but it should never happen
|
||||
throw runtime_error("player join event during battle replay");
|
||||
case BattleRecord::Event::Type::PLAYER_LEAVE:
|
||||
send_player_leave_notification(l, ev.leaving_client_id);
|
||||
break;
|
||||
case BattleRecord::Event::Type::SET_INITIAL_PLAYERS:
|
||||
// This should have been handled before the lobby was even created
|
||||
break;
|
||||
case BattleRecord::Event::Type::BATTLE_COMMAND:
|
||||
send_command(l, 0xC9, 0x00, ev.data);
|
||||
break;
|
||||
case BattleRecord::Event::Type::GAME_COMMAND:
|
||||
send_command(l, 0x60, 0x00, ev.data);
|
||||
break;
|
||||
case BattleRecord::Event::Type::EP3_GAME_COMMAND:
|
||||
send_command(l, 0xCB, 0x00, ev.data);
|
||||
break;
|
||||
case BattleRecord::Event::Type::CHAT_MESSAGE:
|
||||
send_chat_message(l, ev.guild_card_number, decode_sjis(ev.data));
|
||||
break;
|
||||
}
|
||||
this->event_it++;
|
||||
|
||||
} else {
|
||||
// The next event should not occur yet, so reschedule for the time when
|
||||
// it should occur
|
||||
auto tv = usecs_to_timeval(this->event_it->timestamp - relative_ts);
|
||||
event_add(this->next_command_ev.get(), &tv);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
@@ -0,0 +1,123 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <event2/event.h>
|
||||
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <phosg/Strings.hh>
|
||||
|
||||
#include "../Player.hh"
|
||||
|
||||
struct Lobby;
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
// The comment in Server.hh does not apply to this file (and BattleRecord.cc).
|
||||
|
||||
|
||||
|
||||
class BattleRecord {
|
||||
public:
|
||||
struct PlayerEntry {
|
||||
PlayerLobbyDataDCGC lobby_data;
|
||||
PlayerInventory inventory;
|
||||
PlayerDispDataDCPCV3 disp;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct Event {
|
||||
enum class Type : uint8_t {
|
||||
PLAYER_JOIN = 0,
|
||||
PLAYER_LEAVE = 1,
|
||||
SET_INITIAL_PLAYERS = 2,
|
||||
BATTLE_COMMAND = 3,
|
||||
GAME_COMMAND = 4,
|
||||
EP3_GAME_COMMAND = 5,
|
||||
CHAT_MESSAGE = 6,
|
||||
};
|
||||
|
||||
// Fields used for all events
|
||||
Type type;
|
||||
uint64_t timestamp;
|
||||
// Fields used for PLAYER_JOIN and SET_INITIAL_PLAYERS only
|
||||
std::vector<PlayerEntry> players;
|
||||
// Fields used for PLAYER_LEAVE only
|
||||
uint8_t leaving_client_id;
|
||||
// Fields used for CHAT_MESSAGE only
|
||||
uint32_t guild_card_number;
|
||||
// Fields used for the COMMAND types and CHAT_MESSAGE
|
||||
std::string data;
|
||||
|
||||
Event() = default;
|
||||
explicit Event(StringReader& r);
|
||||
void serialize(StringWriter& w) const;
|
||||
};
|
||||
|
||||
explicit BattleRecord(uint32_t behavior_flags);
|
||||
explicit BattleRecord(const std::string& data);
|
||||
std::string serialize() const;
|
||||
|
||||
bool writable() const;
|
||||
bool battle_in_progress() const;
|
||||
|
||||
const Event* get_first_event() const;
|
||||
|
||||
void add_player(
|
||||
const PlayerLobbyDataDCGC& lobby_data,
|
||||
const PlayerInventory& inventory,
|
||||
const PlayerDispDataDCPCV3& disp);
|
||||
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);
|
||||
void add_chat_message(uint32_t guild_card_number, std::string&& data);
|
||||
// This function collapses all the existing player join/leave events into a
|
||||
// single SET_INITIAL_PLAYERS event, and deletes all events before the latest
|
||||
// BATTLE_COMMAND command that specifies the battle map. This should provide a
|
||||
// minimal set of commands to set up and start the battle during a replay.
|
||||
void set_battle_start_timestamp();
|
||||
void set_battle_end_timestamp();
|
||||
|
||||
private:
|
||||
static constexpr uint64_t SIGNATURE = 0x14C946D56D1DAC5A;
|
||||
|
||||
static bool is_map_definition_event(const Event& ev);
|
||||
|
||||
bool is_writable;
|
||||
|
||||
uint32_t behavior_flags;
|
||||
uint64_t battle_start_timestamp;
|
||||
uint64_t battle_end_timestamp;
|
||||
std::deque<Event> events;
|
||||
|
||||
friend class BattleRecordPlayer;
|
||||
};
|
||||
|
||||
class BattleRecordPlayer {
|
||||
public:
|
||||
BattleRecordPlayer(
|
||||
std::shared_ptr<const BattleRecord> rec,
|
||||
std::shared_ptr<struct event_base> base,
|
||||
std::shared_ptr<Lobby> l);
|
||||
~BattleRecordPlayer() = default;
|
||||
|
||||
std::shared_ptr<const BattleRecord> get_record() const;
|
||||
void start();
|
||||
|
||||
private:
|
||||
static void dispatch_schedule_events(evutil_socket_t, short, void* ctx);
|
||||
void schedule_events();
|
||||
|
||||
std::shared_ptr<const BattleRecord> record;
|
||||
std::deque<BattleRecord::Event>::const_iterator event_it;
|
||||
uint64_t play_start_timestamp;
|
||||
std::shared_ptr<struct event_base> base;
|
||||
std::weak_ptr<Lobby> lobby;
|
||||
std::shared_ptr<struct event> next_command_ev;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
@@ -30,6 +30,8 @@ enum BehaviorFlag {
|
||||
DISABLE_TIME_LIMITS = 0x00000008,
|
||||
ENABLE_STATUS_MESSAGES = 0x00000010,
|
||||
LOAD_CARD_TEXT = 0x00000020,
|
||||
ENABLE_RECORDING = 0x00000040,
|
||||
ENABLE_MASKING = 0x00000080,
|
||||
};
|
||||
|
||||
|
||||
|
||||
+99
-17
@@ -1,6 +1,7 @@
|
||||
#include "Server.hh"
|
||||
|
||||
#include <phosg/Time.hh>
|
||||
#include <phosg/Random.hh>
|
||||
|
||||
#include "../SendCommands.hh"
|
||||
|
||||
@@ -146,7 +147,64 @@ void Server::send(const void* data, size_t size) const {
|
||||
if (!l) {
|
||||
throw runtime_error("lobby is deleted");
|
||||
}
|
||||
|
||||
string masked_data;
|
||||
if (this->base()->data_index->behavior_flags & BehaviorFlag::ENABLE_MASKING) {
|
||||
if (size >= 8) {
|
||||
masked_data.assign(reinterpret_cast<const char*>(data), size);
|
||||
uint8_t mask_key = (random_object<uint32_t>() % 0xFF) + 1;
|
||||
set_mask_for_ep3_game_command(masked_data.data(), masked_data.size(), mask_key);
|
||||
data = masked_data.data();
|
||||
size = masked_data.size();
|
||||
}
|
||||
}
|
||||
|
||||
send_command(l, 0xC9, 0x00, data, size);
|
||||
for (auto watcher_l : l->watcher_lobbies) {
|
||||
send_command(watcher_l, 0xC9, 0x00, data, size);
|
||||
}
|
||||
if (l->battle_record && l->battle_record->writable()) {
|
||||
l->battle_record->add_command(
|
||||
BattleRecord::Event::Type::BATTLE_COMMAND, data, size);
|
||||
}
|
||||
}
|
||||
|
||||
string Server::prepare_6xB6x41_map_definition(
|
||||
shared_ptr<const DataIndex::MapEntry> map) {
|
||||
const auto& compressed = map->compressed();
|
||||
|
||||
StringWriter w;
|
||||
uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_GC_Ep3_6xB6x41) + 3) & (~3);
|
||||
w.put<G_MapData_GC_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, map->map.map_number.load(), compressed.size(), 0});
|
||||
w.write(compressed);
|
||||
return move(w.str());
|
||||
}
|
||||
|
||||
void Server::send_commands_for_joining_spectator(Channel& c) const {
|
||||
bool should_send_state = true;
|
||||
if (this->setup_phase == SetupPhase::REGISTRATION) {
|
||||
// If registration is still in progress, we only need to send the map data
|
||||
// (if a map is even chosen yet)
|
||||
if ((this->registration_phase != RegistrationPhase::REGISTERED) &&
|
||||
(this->registration_phase != RegistrationPhase::BATTLE_STARTED)) {
|
||||
should_send_state = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->last_chosen_map) {
|
||||
string data = this->prepare_6xB6x41_map_definition(this->last_chosen_map);
|
||||
c.send(0x6C, 0x00, data);
|
||||
}
|
||||
|
||||
if (should_send_state) {
|
||||
// Note: Some servers send the commented-out commands here. Is there a
|
||||
// situation where we should send them too?
|
||||
c.send(0xC9, 0x00, this->prepare_6xB4x07_decks_update());
|
||||
c.send(0xC9, 0x00, this->prepare_6xB4x1C_names_update());
|
||||
// 6xB4x3B - unknown
|
||||
c.send(0xC9, 0x00, this->prepare_6xB4x50_trap_tile_locations());
|
||||
// 6xB4x52 - unknown
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((format(printf, 2, 3)))
|
||||
@@ -411,7 +469,7 @@ void Server::check_for_destroyed_cards_and_send_6xB4x05_6xB4x02() {
|
||||
this->send_6xB4x02_for_all_players_if_needed();
|
||||
}
|
||||
|
||||
bool Server::check_presence_entry(uint8_t client_id) {
|
||||
bool Server::check_presence_entry(uint8_t client_id) const {
|
||||
return (client_id < 4)
|
||||
? this->base()->presence_entries[client_id].player_present : false;
|
||||
}
|
||||
@@ -870,7 +928,7 @@ void Server::move_phase_after() {
|
||||
// randomness per pass.
|
||||
if (this->num_trap_tiles_of_type[trap_type] == 2) {
|
||||
this->chosen_trap_tile_index_of_type[trap_type] ^= 1;
|
||||
this->send_6xB4x50();
|
||||
this->send_6xB4x50_trap_tile_locations();
|
||||
} else if (this->num_trap_tiles_of_type[trap_type] > 2) {
|
||||
// Generate a new random index, but forbid it from matching the existing
|
||||
// index
|
||||
@@ -879,7 +937,7 @@ void Server::move_phase_after() {
|
||||
new_index++;
|
||||
}
|
||||
this->chosen_trap_tile_index_of_type[trap_type] = new_index;
|
||||
this->send_6xB4x50();
|
||||
this->send_6xB4x50_trap_tile_locations();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -898,12 +956,16 @@ void Server::action_phase_before() {
|
||||
}
|
||||
}
|
||||
|
||||
void Server::send_6xB4x1C_names_update() {
|
||||
G_SetPlayerNames_GC_Ep3_6xB4x1C Server::prepare_6xB4x1C_names_update() const {
|
||||
G_SetPlayerNames_GC_Ep3_6xB4x1C cmd;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
cmd.entries[z] = this->base()->name_entries[z];
|
||||
}
|
||||
this->send(cmd);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
void Server::send_6xB4x1C_names_update() {
|
||||
this->send(this->prepare_6xB4x1C_names_update());
|
||||
}
|
||||
|
||||
int8_t Server::send_6xB4x33_remove_ally_atk_if_needed(const ActionState& pa) {
|
||||
@@ -963,7 +1025,7 @@ int8_t Server::send_6xB4x33_remove_ally_atk_if_needed(const ActionState& pa) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Server::send_all_state_updates() {
|
||||
G_UpdateDecks_GC_Ep3_6xB4x07 Server::prepare_6xB4x07_decks_update() const {
|
||||
G_UpdateDecks_GC_Ep3_6xB4x07 cmd07;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
if (!this->check_presence_entry(z)) {
|
||||
@@ -975,7 +1037,11 @@ void Server::send_all_state_updates() {
|
||||
cmd07.entries[z] = *this->base()->deck_entries[z];
|
||||
}
|
||||
}
|
||||
this->send(cmd07);
|
||||
return cmd07;
|
||||
}
|
||||
|
||||
void Server::send_all_state_updates() {
|
||||
this->send(this->prepare_6xB4x07_decks_update());
|
||||
|
||||
G_UpdateMap_GC_Ep3_6xB4x05 cmd05;
|
||||
cmd05.state = *this->base()->map_and_rules1;
|
||||
@@ -1299,7 +1365,7 @@ void Server::setup_and_start_battle() {
|
||||
this->send_6xB4x1C_names_update();
|
||||
this->registration_phase = RegistrationPhase::BATTLE_STARTED;
|
||||
this->update_battle_state_flags_and_send_6xB4x03_if_needed(true);
|
||||
this->send_6xB4x50();
|
||||
this->send_6xB4x50_trap_tile_locations();
|
||||
|
||||
G_UpdateMap_GC_Ep3_6xB4x05 cmd05;
|
||||
cmd05.state = *this->base()->map_and_rules1;
|
||||
@@ -1818,6 +1884,13 @@ void Server::handle_6xB3x1D_start_battle(const string& data) {
|
||||
}
|
||||
this->battle_in_progress = false;
|
||||
} else {
|
||||
auto l = this->base()->lobby.lock();
|
||||
if (!l) {
|
||||
throw runtime_error("lobby is deleted");
|
||||
}
|
||||
if (l->battle_record) {
|
||||
l->battle_record->set_battle_start_timestamp();
|
||||
}
|
||||
this->setup_and_start_battle();
|
||||
this->battle_in_progress = true;
|
||||
}
|
||||
@@ -2009,6 +2082,11 @@ void Server::handle_6xB3x40_map_list_request(const string& data) {
|
||||
G_MapList_GC_Ep3_6xB6x40{{{{0xB6, 0, 0}, subcommand_size}, 0x40, {}}, list_data.size(), 0});
|
||||
w.write(list_data);
|
||||
send_command(l, 0x6C, 0x00, w.str());
|
||||
|
||||
if (l->battle_record && l->battle_record->writable()) {
|
||||
l->battle_record->add_command(
|
||||
BattleRecord::Event::Type::BATTLE_COMMAND, move(w.str()));
|
||||
}
|
||||
}
|
||||
|
||||
void Server::handle_6xB3x41_map_request(const string& data) {
|
||||
@@ -2021,14 +2099,14 @@ void Server::handle_6xB3x41_map_request(const string& data) {
|
||||
throw runtime_error("lobby is deleted");
|
||||
}
|
||||
|
||||
auto entry = this->base()->data_index->definition_for_map_number(cmd.map_number);
|
||||
const auto& compressed = entry->compressed();
|
||||
this->last_chosen_map = this->base()->data_index->definition_for_map_number(cmd.map_number);
|
||||
auto out_cmd = this->prepare_6xB6x41_map_definition(this->last_chosen_map);
|
||||
send_command(l, 0x6C, 0x00, out_cmd);
|
||||
|
||||
StringWriter w;
|
||||
uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_GC_Ep3_6xB6x41) + 3) & (~3);
|
||||
w.put<G_MapData_GC_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, entry->map.map_number.load(), compressed.size(), 0});
|
||||
w.write(compressed);
|
||||
send_command(l, 0x6C, 0x00, w.str());
|
||||
if (l->battle_record && l->battle_record->writable()) {
|
||||
l->battle_record->add_command(
|
||||
BattleRecord::Event::Type::BATTLE_COMMAND, move(out_cmd));
|
||||
}
|
||||
}
|
||||
|
||||
void Server::handle_6xB3x48_end_turn(const string& data) {
|
||||
@@ -2509,7 +2587,7 @@ void Server::send_6xB4x02_for_all_players_if_needed(bool always_send) {
|
||||
}
|
||||
}
|
||||
|
||||
void Server::send_6xB4x50() const {
|
||||
G_SetTrapTileLocations_GC_Ep3_6xB4x50 Server::prepare_6xB4x50_trap_tile_locations() const {
|
||||
G_SetTrapTileLocations_GC_Ep3_6xB4x50 cmd;
|
||||
for (size_t trap_type = 0; trap_type < 5; trap_type++) {
|
||||
uint8_t trap_index = this->chosen_trap_tile_index_of_type[trap_type];
|
||||
@@ -2519,7 +2597,11 @@ void Server::send_6xB4x50() const {
|
||||
cmd.locations[trap_type].clear(0xFF);
|
||||
}
|
||||
}
|
||||
this->send(cmd);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
void Server::send_6xB4x50_trap_tile_locations() const {
|
||||
this->send(this->prepare_6xB4x50_trap_tile_locations());
|
||||
}
|
||||
|
||||
|
||||
|
||||
+12
-2
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "../Text.hh"
|
||||
#include "../CommandFormats.hh"
|
||||
#include "../Channel.hh"
|
||||
#include "AssistServer.hh"
|
||||
#include "CardSpecial.hh"
|
||||
#include "MapState.hh"
|
||||
@@ -109,6 +110,8 @@ public:
|
||||
}
|
||||
void send(const void* data, size_t size) const;
|
||||
|
||||
void send_commands_for_joining_spectator(Channel& ch) const;
|
||||
|
||||
__attribute__((format(printf, 2, 3)))
|
||||
void send_debug_message_printf(const char* fmt, ...) const;
|
||||
__attribute__((format(printf, 2, 3)))
|
||||
@@ -131,7 +134,7 @@ public:
|
||||
bool card_ref_is_empty_or_has_valid_card_id(uint16_t card_ref) const;
|
||||
bool check_for_battle_end();
|
||||
void check_for_destroyed_cards_and_send_6xB4x05_6xB4x02();
|
||||
bool check_presence_entry(uint8_t client_id);
|
||||
bool check_presence_entry(uint8_t client_id) const;
|
||||
void clear_player_flags_after_dice_phase();
|
||||
void compute_all_map_occupied_bits();
|
||||
void compute_team_dice_boost(uint8_t team_id);
|
||||
@@ -213,7 +216,13 @@ public:
|
||||
void send_6xB4x39() const;
|
||||
void send_6xB4x05(); // Recomputes the map occupied bits, so can't be const
|
||||
void send_6xB4x02_for_all_players_if_needed(bool always_send = false);
|
||||
void send_6xB4x50() const;
|
||||
void send_6xB4x50_trap_tile_locations() const;
|
||||
|
||||
G_UpdateDecks_GC_Ep3_6xB4x07 prepare_6xB4x07_decks_update() const;
|
||||
G_SetPlayerNames_GC_Ep3_6xB4x1C prepare_6xB4x1C_names_update() const;
|
||||
static std::string prepare_6xB6x41_map_definition(
|
||||
std::shared_ptr<const DataIndex::MapEntry> map);
|
||||
G_SetTrapTileLocations_GC_Ep3_6xB4x50 prepare_6xB4x50_trap_tile_locations() const;
|
||||
|
||||
std::vector<std::shared_ptr<Card>> const_cast_set_cards_v(
|
||||
const std::vector<std::shared_ptr<const Card>>& cards);
|
||||
@@ -222,6 +231,7 @@ private:
|
||||
static const std::unordered_map<uint8_t, handler_t> subcommand_handlers;
|
||||
|
||||
std::weak_ptr<ServerBase> w_base;
|
||||
std::shared_ptr<const DataIndex::MapEntry> last_chosen_map;
|
||||
|
||||
public:
|
||||
uint32_t battle_finished;
|
||||
|
||||
+20
-3
@@ -73,18 +73,19 @@ size_t Lobby::count_clients() const {
|
||||
|
||||
void Lobby::add_client(shared_ptr<Client> c) {
|
||||
ssize_t index;
|
||||
ssize_t min_client_id = (this->flags & Lobby::Flag::IS_SPECTATOR_TEAM) ? 4 : 0;
|
||||
if (c->prefer_high_lobby_client_id) {
|
||||
for (index = max_clients - 1; index >= 0; index--) {
|
||||
for (index = max_clients - 1; index >= min_client_id; index--) {
|
||||
if (!this->clients[index].get()) {
|
||||
this->clients[index] = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index < 0) {
|
||||
if (index < min_client_id) {
|
||||
throw out_of_range("no space left in lobby");
|
||||
}
|
||||
} else {
|
||||
for (index = 0; index < max_clients; index++) {
|
||||
for (index = min_client_id; index < max_clients; index++) {
|
||||
if (!this->clients[index].get()) {
|
||||
this->clients[index] = c;
|
||||
break;
|
||||
@@ -120,6 +121,17 @@ void Lobby::add_client(shared_ptr<Client> c) {
|
||||
}
|
||||
c->game_data.player()->print_inventory(stderr);
|
||||
}
|
||||
|
||||
// If the lobby is recording a battle record, add the player join event
|
||||
if (this->battle_record) {
|
||||
PlayerLobbyDataDCGC lobby_data;
|
||||
lobby_data.player_tag = 0x00010000;
|
||||
lobby_data.guild_card = c->license->serial_number;
|
||||
lobby_data.name = encode_sjis(c->game_data.player()->disp.name);
|
||||
this->battle_record->add_player(lobby_data,
|
||||
c->game_data.player()->inventory,
|
||||
c->game_data.player()->disp.to_dcpcv3());
|
||||
}
|
||||
}
|
||||
|
||||
void Lobby::remove_client(shared_ptr<Client> c) {
|
||||
@@ -141,6 +153,11 @@ void Lobby::remove_client(shared_ptr<Client> c) {
|
||||
}
|
||||
|
||||
this->reassign_leader_on_client_departure(c->lobby_client_id);
|
||||
|
||||
// If the lobby ios recording a battle record, add the player leave event
|
||||
if (this->battle_record) {
|
||||
this->battle_record->delete_player(c->lobby_client_id);
|
||||
}
|
||||
}
|
||||
|
||||
void Lobby::move_client_to_lobby(shared_ptr<Lobby> dest_lobby,
|
||||
|
||||
+33
-12
@@ -17,6 +17,7 @@
|
||||
#include "Text.hh"
|
||||
#include "Quest.hh"
|
||||
#include "Items.hh"
|
||||
#include "Episode3/BattleRecord.hh"
|
||||
#include "Episode3/Server.hh"
|
||||
|
||||
struct Lobby {
|
||||
@@ -24,20 +25,23 @@ struct Lobby {
|
||||
GAME = 0x00000001,
|
||||
EPISODE_3_ONLY = 0x00000002,
|
||||
NON_V1_ONLY = 0x00000004, // DC NTE and DCv1 not allowed
|
||||
PERSISTENT = 0x00000008,
|
||||
|
||||
// Flags used only for games
|
||||
CHEATS_ENABLED = 0x00000100,
|
||||
QUEST_IN_PROGRESS = 0x00000200,
|
||||
JOINABLE_QUEST_IN_PROGRESS = 0x00000400,
|
||||
ITEM_TRACKING_ENABLED = 0x00000800,
|
||||
BATTLE_MODE = 0x00002000,
|
||||
CHALLENGE_MODE = 0x00004000,
|
||||
SOLO_MODE = 0x00008000,
|
||||
BATTLE_IN_PROGRESS = 0x00000400,
|
||||
JOINABLE_QUEST_IN_PROGRESS = 0x00000800,
|
||||
ITEM_TRACKING_ENABLED = 0x00001000,
|
||||
IS_SPECTATOR_TEAM = 0x00002000, // EPISODE_3_ONLY must also be set
|
||||
SPECTATORS_FORBIDDEN = 0x00004000,
|
||||
BATTLE_MODE = 0x00008000,
|
||||
CHALLENGE_MODE = 0x00010000,
|
||||
SOLO_MODE = 0x00020000,
|
||||
|
||||
// Flags used only for lobbies
|
||||
PUBLIC = 0x00010000,
|
||||
DEFAULT = 0x00020000,
|
||||
PERSISTENT = 0x00040000,
|
||||
PUBLIC = 0x01000000,
|
||||
DEFAULT = 0x02000000,
|
||||
};
|
||||
|
||||
PrefixedLogger log;
|
||||
@@ -47,7 +51,7 @@ struct Lobby {
|
||||
uint32_t min_level;
|
||||
uint32_t max_level;
|
||||
|
||||
// item info
|
||||
// Item info
|
||||
struct FloorItem {
|
||||
PlayerInventoryItem inv_item;
|
||||
float x;
|
||||
@@ -61,7 +65,7 @@ struct Lobby {
|
||||
std::unordered_map<uint32_t, FloorItem> item_id_to_floor_item;
|
||||
parray<le_uint32_t, 0x20> variations;
|
||||
|
||||
// game config
|
||||
// Game config
|
||||
GameVersion version;
|
||||
uint8_t section_id;
|
||||
uint8_t episode; // 1 = Ep1, 2 = Ep2, 3 = Ep4, 0xFF = Ep3
|
||||
@@ -72,9 +76,26 @@ struct Lobby {
|
||||
uint32_t random_seed;
|
||||
std::shared_ptr<std::mt19937> random;
|
||||
std::shared_ptr<const CommonItemCreator> common_item_creator;
|
||||
std::shared_ptr<Episode3::ServerBase> ep3_server_base;
|
||||
|
||||
// lobby stuff
|
||||
// Ep3 stuff
|
||||
// There are three kinds of Episode 3 games. All of these types have the flag
|
||||
// EPISODE_3_ONLY; types 2 and 3 additionally have the IS_SPECTATOR_TEAM flag.
|
||||
// 1. Primary games. These are the lobbies where battles may take place.
|
||||
// 2. Watcher games. These lobbies receive all the battle and chat commands
|
||||
// from a primary game. (This the implementation of spectator teams.)
|
||||
// 3. Replay games. These lobbies replay a sequence of battle commands and
|
||||
// chat commands from a previous primary game.
|
||||
// Types 2 and 3 may be distinguished by the presence of the battle_record
|
||||
// field - in replay games, it will be present; in watcher games it will be
|
||||
// absent.
|
||||
std::shared_ptr<Episode3::ServerBase> ep3_server_base; // Only used in primary 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::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
|
||||
|
||||
// Lobby stuff
|
||||
uint8_t event;
|
||||
uint8_t block;
|
||||
uint8_t type; // number to give to PSO for the lobby number
|
||||
|
||||
@@ -593,34 +593,6 @@ PlayerBB ClientGameData::export_player_bb() {
|
||||
|
||||
|
||||
|
||||
XBNetworkLocation::XBNetworkLocation() noexcept
|
||||
: internal_ipv4_address(0x0A0A0A0A),
|
||||
external_ipv4_address(0x23232323),
|
||||
port(9100),
|
||||
account_id(0xFFFFFFFFFFFFFFFF) {
|
||||
this->unknown_a1[0] = 0xCCCCCCCC;
|
||||
this->unknown_a1[1] = 0xDDDDDDDD;
|
||||
this->mac_address.clear(0x77);
|
||||
}
|
||||
|
||||
// There's a strange behavior (bug? "feature"?) in Episode 3 where the start
|
||||
// button does nothing in the lobby (hence you can't "quit game") if the
|
||||
// client's IP address is zero. So, we fill it in with a fake nonzero value to
|
||||
// avoid this behavior, and to be consistent, we make IP addresses fake and
|
||||
// nonzero on all other versions too.
|
||||
|
||||
PlayerLobbyDataPC::PlayerLobbyDataPC() noexcept
|
||||
: player_tag(0), guild_card(0), ip_address(0x7F000001), client_id(0) { }
|
||||
|
||||
PlayerLobbyDataDCGC::PlayerLobbyDataDCGC() noexcept
|
||||
: player_tag(0), guild_card(0), ip_address(0x7F000001), client_id(0) { }
|
||||
|
||||
PlayerLobbyDataXB::PlayerLobbyDataXB() noexcept
|
||||
: player_tag(0), guild_card(0), client_id(0) { }
|
||||
|
||||
PlayerLobbyDataBB::PlayerLobbyDataBB() noexcept
|
||||
: player_tag(0), guild_card(0), ip_address(0x7F000001), client_id(0), unknown_a2(0) { }
|
||||
|
||||
void PlayerLobbyDataPC::clear() {
|
||||
this->player_tag = 0;
|
||||
this->guild_card = 0;
|
||||
|
||||
+28
-26
@@ -284,61 +284,63 @@ struct KeyAndTeamConfigBB {
|
||||
|
||||
|
||||
struct PlayerLobbyDataPC {
|
||||
le_uint32_t player_tag;
|
||||
le_uint32_t guild_card;
|
||||
be_uint32_t ip_address;
|
||||
le_uint32_t client_id;
|
||||
le_uint32_t player_tag = 0;
|
||||
le_uint32_t guild_card = 0;
|
||||
// There's a strange behavior (bug? "feature"?) in Episode 3 where the start
|
||||
// button does nothing in the lobby (hence you can't "quit game") if the
|
||||
// client's IP address is zero. So, we fill it in with a fake nonzero value to
|
||||
// avoid this behavior, and to be consistent, we make IP addresses fake and
|
||||
// nonzero on all other versions too.
|
||||
be_uint32_t ip_address = 0x7F000001;
|
||||
le_uint32_t client_id = 0;
|
||||
ptext<char16_t, 0x10> name;
|
||||
|
||||
PlayerLobbyDataPC() noexcept;
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerLobbyDataDCGC {
|
||||
le_uint32_t player_tag;
|
||||
le_uint32_t guild_card;
|
||||
be_uint32_t ip_address;
|
||||
le_uint32_t client_id;
|
||||
le_uint32_t player_tag = 0;
|
||||
le_uint32_t guild_card = 0;
|
||||
be_uint32_t ip_address = 0x7F000001;
|
||||
le_uint32_t client_id = 0;
|
||||
ptext<char, 0x10> name;
|
||||
|
||||
PlayerLobbyDataDCGC() noexcept;
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct XBNetworkLocation {
|
||||
le_uint32_t internal_ipv4_address;
|
||||
le_uint32_t external_ipv4_address;
|
||||
le_uint16_t port;
|
||||
parray<uint8_t, 6> mac_address;
|
||||
le_uint32_t internal_ipv4_address = 0x0A0A0A0A;
|
||||
le_uint32_t external_ipv4_address = 0x23232323;
|
||||
le_uint16_t port = 9100;
|
||||
parray<uint8_t, 6> mac_address = 0x77;
|
||||
parray<le_uint32_t, 2> unknown_a1;
|
||||
le_uint64_t account_id;
|
||||
le_uint64_t account_id = 0xFFFFFFFFFFFFFFFF;
|
||||
parray<le_uint32_t, 4> unknown_a2;
|
||||
|
||||
XBNetworkLocation() noexcept;
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerLobbyDataXB {
|
||||
le_uint32_t player_tag;
|
||||
le_uint32_t guild_card;
|
||||
le_uint32_t player_tag = 0;
|
||||
le_uint32_t guild_card = 0;
|
||||
XBNetworkLocation netloc;
|
||||
le_uint32_t client_id;
|
||||
le_uint32_t client_id = 0;
|
||||
ptext<char, 0x10> name;
|
||||
|
||||
PlayerLobbyDataXB() noexcept;
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerLobbyDataBB {
|
||||
le_uint32_t player_tag;
|
||||
le_uint32_t guild_card;
|
||||
be_uint32_t ip_address; // Guess - the official builds didn't use this, but all other versions have it
|
||||
le_uint32_t player_tag = 0;
|
||||
le_uint32_t guild_card = 0;
|
||||
// This field is a guess; the official builds didn't use this, but all other
|
||||
// versions have it
|
||||
be_uint32_t ip_address = 0x7F000001;
|
||||
parray<uint8_t, 0x10> unknown_a1;
|
||||
le_uint32_t client_id;
|
||||
le_uint32_t client_id = 0;
|
||||
ptext<char16_t, 0x10> name;
|
||||
le_uint32_t unknown_a2;
|
||||
le_uint32_t unknown_a2 = 0;
|
||||
|
||||
PlayerLobbyDataBB() noexcept;
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
|
||||
+64
-10
@@ -892,11 +892,15 @@ static void on_ep3_battle_table_confirm(shared_ptr<ServerState> s,
|
||||
}
|
||||
}
|
||||
|
||||
static void on_ep3_counter_state(shared_ptr<ServerState>, shared_ptr<Client> c,
|
||||
static void on_ep3_counter_state(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
uint16_t, uint32_t flag, const string& data) { // DC
|
||||
check_size_v(data.size(), 0);
|
||||
auto l = s->find_lobby(c->lobby_id);
|
||||
if (flag != 0) {
|
||||
send_command(c, 0xDC, 0x00);
|
||||
l->flags |= Lobby::Flag::BATTLE_IN_PROGRESS;
|
||||
} else {
|
||||
l->flags &= ~Lobby::Flag::BATTLE_IN_PROGRESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -932,6 +936,29 @@ static void on_ep3_server_data_request(shared_ptr<ServerState> s, shared_ptr<Cli
|
||||
}
|
||||
send_text_message_printf(l, "State seed: $C6%08" PRIX32, l->random_seed);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
l->battle_record.reset(new Episode3::BattleRecord(s->ep3_behavior_flags));
|
||||
for (auto existing_c : l->clients) {
|
||||
if (existing_c) {
|
||||
PlayerLobbyDataDCGC lobby_data;
|
||||
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,
|
||||
existing_c->game_data.player()->inventory,
|
||||
existing_c->game_data.player()->disp.to_dcpcv3());
|
||||
}
|
||||
}
|
||||
if (l->prev_battle_record) {
|
||||
send_text_message(l, u"$C6Recording complete");
|
||||
}
|
||||
send_text_message(l, u"$C6Recording enabled");
|
||||
}
|
||||
}
|
||||
l->ep3_server_base->server->on_server_data_input(data);
|
||||
}
|
||||
@@ -1104,6 +1131,12 @@ static void on_menu_item_info_request(shared_ptr<ServerState> s, shared_ptr<Clie
|
||||
info += "$C6Quest in progress";
|
||||
} else if (game->flags & Lobby::Flag::QUEST_IN_PROGRESS) {
|
||||
info += "$C4Quest in progress";
|
||||
} else if (game->flags & Lobby::Flag::BATTLE_IN_PROGRESS) {
|
||||
info += "$C4Battle in progress";
|
||||
}
|
||||
|
||||
if (game->flags & Lobby::Flag::SPECTATORS_FORBIDDEN) {
|
||||
info += "$C4View Battle forbidden";
|
||||
}
|
||||
|
||||
send_ship_info(c, decode_sjis(info));
|
||||
@@ -1350,6 +1383,10 @@ static void on_menu_selection(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
send_lobby_message_box(c, u"$C6You cannot join this\ngame because a\nquest is already\nin progress.");
|
||||
break;
|
||||
}
|
||||
if (game->flags & Lobby::Flag::BATTLE_IN_PROGRESS) {
|
||||
send_lobby_message_box(c, u"$C6You cannot join this\ngame because a\nbattle is already\nin progress.");
|
||||
break;
|
||||
}
|
||||
if (game->any_client_loading()) {
|
||||
send_lobby_message_box(c, u"$C6You cannot join this\ngame because\nanother player is\ncurrently loading.\nTry again soon.");
|
||||
break;
|
||||
@@ -1927,6 +1964,14 @@ static void on_chat_generic(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
c->game_data.player()->disp.name.data(), processed_text.c_str(),
|
||||
private_flags);
|
||||
}
|
||||
|
||||
if (l->battle_record && l->battle_record->battle_in_progress()) {
|
||||
auto prepared_message = prepare_chat_message(
|
||||
c->version(), c->game_data.player()->disp.name.data(),
|
||||
processed_text.c_str(), private_flags);
|
||||
string prepared_message_sjis = encode_sjis(prepared_message);
|
||||
l->battle_record->add_chat_message(c->license->serial_number, move(prepared_message_sjis));
|
||||
}
|
||||
}
|
||||
|
||||
static void on_chat_pc_bb(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
@@ -2445,6 +2490,10 @@ static shared_ptr<Lobby> create_game_generic(shared_ptr<ServerState> s,
|
||||
|
||||
shared_ptr<Lobby> game = s->create_lobby();
|
||||
game->name = name;
|
||||
game->flags = flags |
|
||||
Lobby::Flag::GAME |
|
||||
(is_ep3 ? Lobby::Flag::EPISODE_3_ONLY : 0) |
|
||||
(item_tracking_enabled ? Lobby::Flag::ITEM_TRACKING_ENABLED : 0);
|
||||
game->password = password;
|
||||
game->version = c->version();
|
||||
game->section_id = c->override_section_id >= 0
|
||||
@@ -2455,16 +2504,17 @@ static shared_ptr<Lobby> create_game_generic(shared_ptr<ServerState> s,
|
||||
game->random_seed = c->override_random_seed;
|
||||
game->random->seed(game->random_seed);
|
||||
}
|
||||
if (c->next_game_battle_record) {
|
||||
game->battle_player.reset(new Episode3::BattleRecordPlayer(
|
||||
c->next_game_battle_record, s->game_server->get_base(), game));
|
||||
c->next_game_battle_record.reset();
|
||||
game->flags |= Lobby::Flag::IS_SPECTATOR_TEAM;
|
||||
}
|
||||
game->common_item_creator.reset(new CommonItemCreator(
|
||||
s->common_item_data, game->random));
|
||||
game->event = Lobby::game_event_for_lobby_event(current_lobby->event);
|
||||
game->block = 0xFF;
|
||||
game->max_clients = 4;
|
||||
game->flags =
|
||||
(is_ep3 ? Lobby::Flag::EPISODE_3_ONLY : 0) |
|
||||
(item_tracking_enabled ? Lobby::Flag::ITEM_TRACKING_ENABLED : 0) |
|
||||
Lobby::Flag::GAME |
|
||||
flags;
|
||||
game->max_clients = (game->flags & Lobby::Flag::IS_SPECTATOR_TEAM) ? 12 : 4;
|
||||
game->min_level = min_level;
|
||||
game->max_level = 0xFFFFFFFF;
|
||||
|
||||
@@ -2555,8 +2605,8 @@ static void on_create_game_dc_v3(shared_ptr<ServerState> s, shared_ptr<Client> c
|
||||
const auto& cmd = check_size_t<C_CreateGame_DC_V3_0C_C1_Ep3_EC>(data);
|
||||
|
||||
// Only allow EC from Ep3 clients
|
||||
bool client_is_ep3 = c->flags & Client::Flag::IS_EPISODE_3;
|
||||
if ((command == 0xEC) && !client_is_ep3) {
|
||||
bool client_is_ep3 = !!(c->flags & Client::Flag::IS_EPISODE_3);
|
||||
if ((command == 0xEC) != client_is_ep3) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2581,7 +2631,11 @@ static void on_create_game_dc_v3(shared_ptr<ServerState> s, shared_ptr<Client> c
|
||||
flags |= Lobby::Flag::BATTLE_MODE;
|
||||
}
|
||||
if (cmd.challenge_mode) {
|
||||
flags |= Lobby::Flag::CHALLENGE_MODE;
|
||||
if (client_is_ep3) {
|
||||
flags |= Lobby::Flag::SPECTATORS_FORBIDDEN;
|
||||
} else {
|
||||
flags |= Lobby::Flag::CHALLENGE_MODE;
|
||||
}
|
||||
}
|
||||
create_game_generic(
|
||||
s, c, name.c_str(), password.c_str(), episode, cmd.difficulty, flags);
|
||||
|
||||
@@ -97,6 +97,17 @@ static void forward_subcommand(shared_ptr<Lobby> l, shared_ptr<Client> c,
|
||||
} else {
|
||||
send_command_excluding_client(l, c, command, flag, data, size);
|
||||
}
|
||||
|
||||
for (const auto& watcher_lobby : l->watcher_lobbies) {
|
||||
forward_subcommand(watcher_lobby, c, command, flag, data, size);
|
||||
}
|
||||
|
||||
if (l->battle_record && l->battle_record->battle_in_progress()) {
|
||||
auto type = ((command & 0xF0) == 0xC0)
|
||||
? Episode3::BattleRecord::Event::Type::EP3_GAME_COMMAND
|
||||
: Episode3::BattleRecord::Event::Type::GAME_COMMAND;
|
||||
l->battle_record->add_command(type, data, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+150
-12
@@ -694,14 +694,13 @@ void send_text_message(shared_ptr<ServerState> s, const u16string& text) {
|
||||
}
|
||||
}
|
||||
|
||||
void send_chat_message(Channel& ch, const u16string& text) {
|
||||
send_header_text(ch, 0x06, 0, text, false);
|
||||
}
|
||||
|
||||
void send_chat_message(shared_ptr<Client> c, uint32_t from_guild_card_number,
|
||||
const u16string& from_name, const u16string& text, char private_flags) {
|
||||
u16string prepare_chat_message(
|
||||
GameVersion version,
|
||||
const u16string& from_name,
|
||||
const u16string& text,
|
||||
char private_flags) {
|
||||
u16string data;
|
||||
if (c->version() == GameVersion::BB) {
|
||||
if (version == GameVersion::BB) {
|
||||
data.append(u"\tJ");
|
||||
}
|
||||
data.append(remove_language_marker(from_name));
|
||||
@@ -711,7 +710,31 @@ void send_chat_message(shared_ptr<Client> c, uint32_t from_guild_card_number,
|
||||
}
|
||||
data.append(u"\tJ");
|
||||
data.append(text);
|
||||
send_header_text(c->channel, 0x06, from_guild_card_number, data, false);
|
||||
return data;
|
||||
}
|
||||
|
||||
void send_chat_message(Channel& ch, const u16string& text) {
|
||||
send_header_text(ch, 0x06, 0, text, false);
|
||||
}
|
||||
|
||||
void send_chat_message(shared_ptr<Client> c, uint32_t from_guild_card_number,
|
||||
const u16string& prepared_data) {
|
||||
send_header_text(c->channel, 0x06, from_guild_card_number, prepared_data, false);
|
||||
}
|
||||
|
||||
void send_chat_message(shared_ptr<Lobby> l, uint32_t from_guild_card_number,
|
||||
const u16string& prepared_data) {
|
||||
for (auto c : l->clients) {
|
||||
if (c) {
|
||||
send_header_text(c->channel, 0x06, from_guild_card_number, prepared_data, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void send_chat_message(shared_ptr<Client> c, uint32_t from_guild_card_number,
|
||||
const u16string& from_name, const u16string& text, char private_flags) {
|
||||
auto data = prepare_chat_message(c->version(), from_name, text, private_flags);
|
||||
send_chat_message(c, from_guild_card_number, data);
|
||||
}
|
||||
|
||||
template <typename CmdT>
|
||||
@@ -1071,7 +1094,7 @@ void send_game_menu_t(shared_ptr<Client> c, shared_ptr<ServerState> s) {
|
||||
e.episode = ((c->version() == GameVersion::BB) ? (l->max_clients << 4) : 0) | l->episode;
|
||||
}
|
||||
if (l->flags & Lobby::Flag::EPISODE_3_ONLY) {
|
||||
e.flags = (l->password.empty() ? 0 : 2);
|
||||
e.flags = (l->password.empty() ? 0 : 2) | ((l->flags & Lobby::Flag::BATTLE_IN_PROGRESS) ? 4 : 0);
|
||||
} else {
|
||||
e.flags = ((l->episode << 6) | (l->password.empty() ? 0 : 2));
|
||||
if (l->flags & Lobby::Flag::BATTLE_MODE) {
|
||||
@@ -1210,8 +1233,114 @@ void send_lobby_list(shared_ptr<Client> c, shared_ptr<ServerState> s) {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// lobby joining
|
||||
|
||||
static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
if (!(c->flags & Client::Flag::IS_EPISODE_3)) {
|
||||
throw runtime_error("lobby is not Episode 3");
|
||||
}
|
||||
if (!(l->flags & Lobby::Flag::EPISODE_3_ONLY)) {
|
||||
throw runtime_error("lobby is not Episode 3");
|
||||
}
|
||||
if (!(l->flags & Lobby::Flag::IS_SPECTATOR_TEAM)) {
|
||||
throw runtime_error("lobby is not a spectator team");
|
||||
}
|
||||
|
||||
S_JoinSpectatorTeam_GC_Ep3_E8 cmd;
|
||||
|
||||
cmd.variations.clear(0);
|
||||
cmd.client_id = c->lobby_client_id;
|
||||
cmd.leader_id = l->leader_id;
|
||||
cmd.event = l->event;
|
||||
cmd.section_id = l->section_id;
|
||||
cmd.rare_seed = l->random_seed;
|
||||
cmd.episode = 3;
|
||||
|
||||
uint8_t player_count = 0;
|
||||
auto watched_lobby = l->watched_lobby.lock();
|
||||
if (watched_lobby) {
|
||||
// Live spectating
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
if (watched_lobby->clients[z]) {
|
||||
cmd.players[z].lobby_data.player_tag = 0x00010000;
|
||||
cmd.players[z].lobby_data.guild_card = watched_lobby->clients[z]->license->serial_number;
|
||||
cmd.players[z].lobby_data.client_id = watched_lobby->clients[z]->lobby_client_id;
|
||||
cmd.players[z].lobby_data.name = watched_lobby->clients[z]->game_data.player()->disp.name;
|
||||
remove_language_marker_inplace(cmd.players[z].lobby_data.name);
|
||||
cmd.players[z].inventory = watched_lobby->clients[z]->game_data.player()->inventory;
|
||||
cmd.players[z].disp = watched_lobby->clients[z]->game_data.player()->disp.to_dcpcv3();
|
||||
remove_language_marker_inplace(cmd.players[z].disp.name);
|
||||
cmd.entries[z].player_tag = 0x00010000;
|
||||
cmd.entries[z].guild_card_number = watched_lobby->clients[z]->license->serial_number;
|
||||
cmd.entries[z].name = watched_lobby->clients[z]->game_data.player()->disp.name;
|
||||
remove_language_marker_inplace(cmd.entries[z].name);
|
||||
cmd.entries[z].present = 1;
|
||||
cmd.entries[z].level = watched_lobby->clients[z]->game_data.player()->disp.level.load();
|
||||
player_count++;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (l->battle_player) {
|
||||
// Battle record replay
|
||||
const auto* ev = l->battle_player->get_record()->get_first_event();
|
||||
if (!ev) {
|
||||
throw runtime_error("battle record contains no events");
|
||||
}
|
||||
if (ev->type != Episode3::BattleRecord::Event::Type::SET_INITIAL_PLAYERS) {
|
||||
throw runtime_error("battle record does not begin with set players event");
|
||||
}
|
||||
for (const auto& entry : ev->players) {
|
||||
uint8_t client_id = entry.lobby_data.client_id;
|
||||
if (client_id >= 4) {
|
||||
throw runtime_error("invalid client id in battle record");
|
||||
}
|
||||
cmd.players[client_id].lobby_data = entry.lobby_data;
|
||||
remove_language_marker_inplace(cmd.players[client_id].lobby_data.name);
|
||||
cmd.players[client_id].inventory = entry.inventory;
|
||||
cmd.players[client_id].disp = entry.disp;
|
||||
remove_language_marker_inplace(cmd.players[client_id].disp.name);
|
||||
cmd.entries[client_id].player_tag = 0x00010000;
|
||||
cmd.entries[client_id].guild_card_number = entry.lobby_data.guild_card;
|
||||
cmd.entries[client_id].name = entry.disp.name;
|
||||
remove_language_marker_inplace(cmd.entries[client_id].name);
|
||||
cmd.entries[client_id].present = 1;
|
||||
cmd.entries[client_id].level = entry.disp.level.load();
|
||||
player_count++;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw runtime_error("neither a watched lobby nor a battle player are present");
|
||||
}
|
||||
|
||||
for (size_t z = 4; z < 12; z++) {
|
||||
if (l->clients[z]) {
|
||||
cmd.spectator_players[z - 4].lobby_data.player_tag = 0x00010000;
|
||||
cmd.spectator_players[z - 4].lobby_data.guild_card = l->clients[z]->license->serial_number;
|
||||
cmd.spectator_players[z - 4].lobby_data.client_id = l->clients[z]->lobby_client_id;
|
||||
cmd.spectator_players[z - 4].lobby_data.name = l->clients[z]->game_data.player()->disp.name;
|
||||
remove_language_marker_inplace(cmd.spectator_players[z - 4].lobby_data.name);
|
||||
cmd.spectator_players[z - 4].inventory = l->clients[z]->game_data.player()->inventory;
|
||||
cmd.spectator_players[z - 4].disp = l->clients[z]->game_data.player()->disp.to_dcpcv3();
|
||||
remove_language_marker_inplace(cmd.spectator_players[z - 4].disp.name);
|
||||
cmd.entries[z].player_tag = 0x00010000;
|
||||
cmd.entries[z].guild_card_number = l->clients[z]->license->serial_number;
|
||||
cmd.entries[z].name = l->clients[z]->game_data.player()->disp.name;
|
||||
remove_language_marker_inplace(cmd.entries[z].name);
|
||||
cmd.entries[z].present = 1;
|
||||
cmd.entries[z].level = l->clients[z]->game_data.player()->disp.level.load();
|
||||
player_count++;
|
||||
}
|
||||
}
|
||||
cmd.spectator_team_name = encode_sjis(l->name);
|
||||
|
||||
send_command_t(c, 0xE8, player_count, cmd);
|
||||
}
|
||||
|
||||
template <typename LobbyDataT, typename DispDataT>
|
||||
void send_join_game_t(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
if (l->flags & Lobby::Flag::IS_SPECTATOR_TEAM) {
|
||||
send_join_spectator_team(c, l);
|
||||
return;
|
||||
}
|
||||
|
||||
bool is_ep3 = (l->flags & Lobby::Flag::EPISODE_3_ONLY);
|
||||
string data(is_ep3 ? sizeof(S_JoinGame_GC_Ep3_64) : sizeof(S_JoinGame<LobbyDataT, DispDataT>), '\0');
|
||||
|
||||
@@ -1233,7 +1362,7 @@ void send_join_game_t(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
if (l->clients[x]) {
|
||||
cmd->lobby_data[x].player_tag = 0x00010000;
|
||||
cmd->lobby_data[x].guild_card = l->clients[x]->license->serial_number;
|
||||
cmd->lobby_data[x].client_id = c->lobby_client_id;
|
||||
cmd->lobby_data[x].client_id = l->clients[x]->lobby_client_id;
|
||||
cmd->lobby_data[x].name = l->clients[x]->game_data.player()->disp.name;
|
||||
if (cmd_ep3) {
|
||||
cmd_ep3->players_ep3[x].inventory = l->clients[x]->game_data.player()->inventory;
|
||||
@@ -1269,7 +1398,7 @@ void send_join_lobby_t(shared_ptr<Client> c, shared_ptr<Lobby> l,
|
||||
uint8_t command;
|
||||
if (l->is_game()) {
|
||||
if (joining_client) {
|
||||
command = 0x65;
|
||||
command = (l->flags & Lobby::Flag::IS_SPECTATOR_TEAM) ? 0xEB : 0x65;
|
||||
} else {
|
||||
throw logic_error("send_join_lobby_t should not be used for primary game join command");
|
||||
}
|
||||
@@ -1405,7 +1534,16 @@ void send_player_join_notification(shared_ptr<Client> c,
|
||||
|
||||
void send_player_leave_notification(shared_ptr<Lobby> l, uint8_t leaving_client_id) {
|
||||
S_LeaveLobby_66_69_Ep3_E9 cmd = {leaving_client_id, l->leader_id, 1, 0};
|
||||
send_command_t(l, l->is_game() ? 0x66 : 0x69, leaving_client_id, cmd);
|
||||
uint8_t cmd_num;
|
||||
if (l->is_game()) {
|
||||
cmd_num = (l->flags & Lobby::Flag::IS_SPECTATOR_TEAM) ? 0xE9 : 0x66;
|
||||
} else {
|
||||
cmd_num = 0x69;
|
||||
}
|
||||
send_command_t(l, cmd_num, leaving_client_id, cmd);
|
||||
for (const auto& watcher_l : l->watcher_lobbies) {
|
||||
send_command_t(watcher_l, cmd_num, leaving_client_id, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
void send_self_leave_notification(shared_ptr<Client> c) {
|
||||
|
||||
+25
-4
@@ -173,11 +173,32 @@ void send_text_message(Channel& ch, const std::u16string& text);
|
||||
void send_text_message(std::shared_ptr<Client> c, const std::u16string& text);
|
||||
void send_text_message(std::shared_ptr<Lobby> l, const std::u16string& text);
|
||||
void send_text_message(std::shared_ptr<ServerState> l, const std::u16string& text);
|
||||
|
||||
std::u16string prepare_chat_message(
|
||||
GameVersion version,
|
||||
const std::u16string& from_name,
|
||||
const std::u16string& text,
|
||||
char private_flags);
|
||||
void send_chat_message(Channel& ch, const std::u16string& text);
|
||||
void send_chat_message(std::shared_ptr<Client> c, uint32_t from_serial_number,
|
||||
const std::u16string& from_name, const std::u16string& text, char private_flags);
|
||||
void send_simple_mail(std::shared_ptr<Client> c, uint32_t from_serial_number,
|
||||
const std::u16string& from_name, const std::u16string& text);
|
||||
void send_chat_message(
|
||||
std::shared_ptr<Client> c,
|
||||
uint32_t from_guild_card_number,
|
||||
const std::u16string& prepared_data);
|
||||
void send_chat_message(
|
||||
std::shared_ptr<Lobby> l,
|
||||
uint32_t from_guild_card_number,
|
||||
const std::u16string& prepared_data);
|
||||
void send_chat_message(
|
||||
std::shared_ptr<Client> c,
|
||||
uint32_t from_guild_card_number,
|
||||
const std::u16string& from_name,
|
||||
const u16string& text,
|
||||
char private_flags);
|
||||
void send_simple_mail(
|
||||
std::shared_ptr<Client> c,
|
||||
uint32_t from_serial_number,
|
||||
const std::u16string& from_name,
|
||||
const std::u16string& text);
|
||||
|
||||
template <typename TargetT>
|
||||
__attribute__((format(printf, 2, 3))) void send_text_message_printf(
|
||||
|
||||
@@ -234,3 +234,7 @@ shared_ptr<Client> Server::get_client() const {
|
||||
}
|
||||
return this->channel_to_client.begin()->second;
|
||||
}
|
||||
|
||||
shared_ptr<struct event_base> Server::get_base() const {
|
||||
return this->base;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ public:
|
||||
GameVersion version, ServerBehavior initial_state);
|
||||
|
||||
std::shared_ptr<Client> get_client() const;
|
||||
std::shared_ptr<struct event_base> get_base() const;
|
||||
|
||||
private:
|
||||
std::shared_ptr<struct event_base> base;
|
||||
|
||||
+35
-1
@@ -156,6 +156,14 @@ void ServerState::send_lobby_join_notifications(shared_ptr<Lobby> l,
|
||||
send_player_join_notification(other_client, l, joining_client);
|
||||
}
|
||||
}
|
||||
for (auto& watcher_l : l->watcher_lobbies) {
|
||||
for (auto& watcher_c : watcher_l->clients) {
|
||||
if (!watcher_c) {
|
||||
continue;
|
||||
}
|
||||
send_player_join_notification(watcher_c, watcher_l, joining_client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<Lobby> ServerState::find_lobby(uint32_t lobby_id) {
|
||||
@@ -184,7 +192,33 @@ void ServerState::remove_lobby(uint32_t lobby_id) {
|
||||
if (lobby_it == this->id_to_lobby.end()) {
|
||||
throw logic_error("attempted to remove nonexistent lobby");
|
||||
}
|
||||
lobby_it->second->log.info("Deleted lobby");
|
||||
|
||||
auto l = lobby_it->second;
|
||||
if (l->count_clients() != 0) {
|
||||
throw logic_error("attempted to delete lobby with clients in it");
|
||||
}
|
||||
|
||||
if (l->flags & Lobby::Flag::IS_SPECTATOR_TEAM) {
|
||||
auto primary_l = l->watched_lobby.lock();
|
||||
if (primary_l) {
|
||||
primary_l->log.info("Unlinking watcher lobby %" PRIX32, l->lobby_id);
|
||||
primary_l->watcher_lobbies.erase(l);
|
||||
} else {
|
||||
l->log.info("Watched lobby is missing");
|
||||
}
|
||||
l->watched_lobby.reset();
|
||||
} else {
|
||||
// Tell all players in all spectator teams to go back to the lobby
|
||||
for (auto watcher_l : l->watcher_lobbies) {
|
||||
if (!(watcher_l->flags & Lobby::Flag::EPISODE_3_ONLY)) {
|
||||
throw logic_error("spectator team is not an Episode 3 lobby");
|
||||
}
|
||||
l->log.info("Disbanding watcher lobby %" PRIX32, watcher_l->lobby_id);
|
||||
send_command(watcher_l, 0xED, 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
l->log.info("Deleted lobby");
|
||||
this->id_to_lobby.erase(lobby_it);
|
||||
}
|
||||
|
||||
|
||||
@@ -262,6 +262,9 @@
|
||||
// 0x00000020 => Load card text as well as card definitions (has no behavioral
|
||||
// effects; this flag exists to be used internally when the
|
||||
// --show-ep3-data option is given)
|
||||
// 0x00000040 => Enable battle recording (after a battle, players can save the
|
||||
// recording with the $saverec <filename> command)
|
||||
// 0x00000080 => Enable command masking during battles
|
||||
"Episode3BehaviorFlags": 0x00000002,
|
||||
|
||||
// Episode 3 card auction configuration. CardAuctionPoints specifies how many
|
||||
|
||||
Reference in New Issue
Block a user