add Ep3 trial map format
This commit is contained in:
@@ -68,7 +68,6 @@ Current known issues / missing features / things to do:
|
||||
- Implement the C5 (battle/challenge records) command.
|
||||
- Implement choice search.
|
||||
- Episode 3 bugs
|
||||
- Trial Edition can't select maps in battle setup. Fix this.
|
||||
- Fix behavior when joining a spectator team after the beginning of a battle.
|
||||
- Disconnecting during a match turns you into a COM if there are other humans in the match, even if the match is part of a tournament. This may be incorrect behavior for tournaments.
|
||||
- Disconnecting during a tournament when there are no other humans in the match simply cancels the match (so it can be replayed) instead of forfeiting, which is almost certainly incorrect behavior. (Then again, no one likes losing tournaments to COMs...)
|
||||
@@ -101,7 +100,7 @@ newserv supports several versions of PSO. Specifically:
|
||||
2. *newserv's implementations of these versions are based on disassembly of the client executables and have never been tested.*
|
||||
3. *BB games are mostly playable, but there are still some unimplemented features (for example, some quests that use rare commands may not work). Please submit a GitHub issue if you find something that doesn't work.*
|
||||
4. *Support for PSO Dreamcast Trial Edition and the December 2000 prototype is somewhat incomplete and probably never will be complete. These versions are rather unstable and seem to crash often, but it's not obvious whether it's because they're prototypes or because newserv sends data they can't handle.*
|
||||
5. *Creating a game works, but choosing a map during battle setup causes the Trial Edition client to crash. This is likely due to the trial version's map format being slightly different from the final version's map format.*
|
||||
5. *Creating a game works and battle setup behaves mostly normally, but starting a battle doesn't work.*
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -1630,6 +1630,50 @@ string MapDefinition::str(const CardIndex* card_index) const {
|
||||
return join(lines, "\n");
|
||||
}
|
||||
|
||||
MapDefinitionTrial::MapDefinitionTrial(const MapDefinition& map)
|
||||
: unknown_a1(map.unknown_a1),
|
||||
map_number(map.map_number),
|
||||
width(map.width),
|
||||
height(map.height),
|
||||
environment_number(map.environment_number),
|
||||
num_alt_maps(map.num_alt_maps),
|
||||
map_tiles(map.map_tiles),
|
||||
start_tile_definitions(map.start_tile_definitions),
|
||||
alt_maps1(map.alt_maps1),
|
||||
alt_maps_unknown_a3(map.alt_maps_unknown_a3),
|
||||
unknown_a4(map.unknown_a4),
|
||||
modification_tiles(map.modification_tiles),
|
||||
unknown_a5(map.unknown_a5),
|
||||
default_rules(map.default_rules),
|
||||
unknown_a6(map.unknown_a6),
|
||||
name(map.name),
|
||||
location_name(map.location_name),
|
||||
quest_name(map.quest_name),
|
||||
description(map.description),
|
||||
map_x(map.map_x),
|
||||
map_y(map.map_y),
|
||||
npc_decks(map.npc_decks),
|
||||
npc_chars(map.npc_chars),
|
||||
unknown_a7_a(map.unknown_a7_a),
|
||||
unknown_a7_b(map.unknown_a7_b),
|
||||
before_message(map.before_message),
|
||||
after_message(map.after_message),
|
||||
dispatch_message(map.dispatch_message),
|
||||
dialogue_sets(),
|
||||
reward_card_ids(map.reward_card_ids),
|
||||
unknown_a9_a(map.unknown_a9_a),
|
||||
unknown_a9_b(map.unknown_a9_b),
|
||||
unknown_a9_c(map.unknown_a9_c),
|
||||
unknown_a9_d(map.unknown_a9_d),
|
||||
unknown_a10(map.unknown_a10),
|
||||
cyber_block_type(map.cyber_block_type),
|
||||
unknown_a11(map.unknown_a11),
|
||||
unknown_t12(0xFF) {
|
||||
for (size_t z = 0; z < this->dialogue_sets.size(); z++) {
|
||||
this->dialogue_sets[z] = map.dialogue_sets[z].sub<8>(0);
|
||||
}
|
||||
}
|
||||
|
||||
bool Rules::check_invalid_fields() const {
|
||||
Rules t = *this;
|
||||
return t.check_and_reset_invalid_fields();
|
||||
@@ -1969,11 +2013,19 @@ MapIndex::MapEntry::MapEntry(const string& compressed, bool is_quest)
|
||||
this->map = *reinterpret_cast<const MapDefinition*>(decompressed.data());
|
||||
}
|
||||
|
||||
string MapIndex::MapEntry::compressed() const {
|
||||
if (this->compressed_data.empty()) {
|
||||
this->compressed_data = prs_compress(&this->map, sizeof(this->map));
|
||||
const string& MapIndex::MapEntry::compressed(bool is_trial) const {
|
||||
if (is_trial) {
|
||||
if (this->compressed_trial_data.empty()) {
|
||||
MapDefinitionTrial mdt(this->map);
|
||||
this->compressed_trial_data = prs_compress(&mdt, sizeof(mdt));
|
||||
}
|
||||
return this->compressed_trial_data;
|
||||
} else {
|
||||
if (this->compressed_data.empty()) {
|
||||
this->compressed_data = prs_compress(&this->map, sizeof(this->map));
|
||||
}
|
||||
return this->compressed_data;
|
||||
}
|
||||
return this->compressed_data;
|
||||
}
|
||||
|
||||
const string& MapIndex::get_compressed_list() const {
|
||||
|
||||
+68
-11
@@ -553,7 +553,7 @@ struct CardDefinition {
|
||||
/* 00A0 */ ptext<char, 0x14> en_name;
|
||||
/* 00B4 */ ptext<char, 0x0B> jp_short_name;
|
||||
/* 00BF */ ptext<char, 0x08> en_short_name;
|
||||
/* 00C7 */ Effect effects[3];
|
||||
/* 00C7 */ parray<Effect, 3> effects;
|
||||
/* 0127 */ uint8_t unused4;
|
||||
/* 0128 */
|
||||
|
||||
@@ -573,9 +573,9 @@ struct CardDefinitionsFooter {
|
||||
/* 08 */ be_uint32_t num_cards2;
|
||||
/* 0C */ parray<be_uint32_t, 11> unknown_a2;
|
||||
/* 38 */ be_uint32_t unknown_offset_a3;
|
||||
/* 3C */ be_uint32_t unknown_a4[3];
|
||||
/* 3C */ parray<be_uint32_t, 3> unknown_a4;
|
||||
/* 48 */ be_uint32_t footer_offset;
|
||||
/* 4C */ be_uint32_t unknown_a5[3];
|
||||
/* 4C */ parray<be_uint32_t, 3> unknown_a5;
|
||||
/* 58 */
|
||||
} __attribute__((packed));
|
||||
|
||||
@@ -701,6 +701,12 @@ struct Rules {
|
||||
/* 0D */ parray<uint8_t, 3> unused;
|
||||
/* 10 */
|
||||
|
||||
// Annoyingly, this structure is a different size in Episode 3 Trial Edition.
|
||||
// This means that many command formats, as well as the map format, are
|
||||
// different, and the existing Server implementation can't serve Trial Edition
|
||||
// clients. It'd be nice to support Trial Edition battles, but that would
|
||||
// likely be more work than it's worth.
|
||||
|
||||
Rules();
|
||||
explicit Rules(std::shared_ptr<const JSONObject> json);
|
||||
std::shared_ptr<JSONObject> json() const;
|
||||
@@ -838,9 +844,9 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
// - If the team has 3 players, bytes [3] through [5] are used.
|
||||
/* 010C */ parray<parray<uint8_t, 6>, 2> start_tile_definitions;
|
||||
|
||||
/* 0118 */ parray<parray<uint8_t, 0x10>, 0x10> alt_maps1[2][0x0A];
|
||||
/* 1518 */ parray<be_float, 0x12> alt_maps_unknown_a3[2][0x0A];
|
||||
/* 1AB8 */ parray<be_float, 0x24> unknown_a4[3];
|
||||
/* 0118 */ parray<parray<parray<parray<uint8_t, 0x10>, 0x10>, 0x0A>, 2> alt_maps1;
|
||||
/* 1518 */ parray<parray<parray<be_float, 0x12>, 0x0A>, 2> alt_maps_unknown_a3;
|
||||
/* 1AB8 */ parray<parray<be_float, 0x24>, 3> unknown_a4;
|
||||
|
||||
// In the modification_tiles array, the values are:
|
||||
// 10 = blocked by rock (as if the corresponding map_tiles value was 00)
|
||||
@@ -871,7 +877,8 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
/* 18 */ parray<be_uint16_t, 0x20> card_ids; // Last one appears to always be FFFF
|
||||
/* 58 */
|
||||
} __attribute__((packed));
|
||||
/* 1FE8 */ NPCDeck npc_decks[3]; // Unused if name[0] == 0
|
||||
/* 1FE8 */ parray<NPCDeck, 3> npc_decks; // Unused if name[0] == 0
|
||||
|
||||
struct NPCCharacter {
|
||||
/* 0000 */ parray<be_uint16_t, 2> unknown_a1;
|
||||
/* 0004 */ parray<uint8_t, 4> unknown_a2;
|
||||
@@ -879,7 +886,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
/* 0018 */ parray<be_uint16_t, 0x7E> unknown_a3;
|
||||
/* 0114 */
|
||||
} __attribute__((packed));
|
||||
/* 20F0 */ NPCCharacter npc_chars[3]; // Unused if name[0] == 0
|
||||
/* 20F0 */ parray<NPCCharacter, 3> npc_chars; // Unused if name[0] == 0
|
||||
|
||||
/* 242C */ parray<uint8_t, 8> unknown_a7_a; // Always FF?
|
||||
/* 2434 */ parray<be_uint32_t, 3> unknown_a7_b; // Always FF?
|
||||
@@ -896,10 +903,10 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
struct DialogueSet {
|
||||
/* 0000 */ be_uint16_t unknown_a1;
|
||||
/* 0002 */ be_uint16_t unknown_a2; // Always 0x0064 if valid, 0xFFFF if unused?
|
||||
/* 0004 */ ptext<char, 0x40> strings[4];
|
||||
/* 0004 */ parray<ptext<char, 0x40>, 4> strings;
|
||||
/* 0104 */
|
||||
} __attribute__((packed));
|
||||
/* 28F0 */ DialogueSet dialogue_sets[3][0x10]; // Up to 0x10 per valid NPC
|
||||
/* 28F0 */ parray<parray<DialogueSet, 0x10>, 3> dialogue_sets; // Up to 0x10 per valid NPC
|
||||
|
||||
/* 59B0 */ parray<be_uint16_t, 0x10> reward_card_ids;
|
||||
|
||||
@@ -958,6 +965,55 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
std::string str(const CardIndex* card_index = nullptr) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct MapDefinitionTrial {
|
||||
// This is the format of Episode 3 Trial Edition maps. See the comments in
|
||||
// MapDefinition for what each field means.
|
||||
|
||||
/* 0000 */ be_uint32_t unknown_a1;
|
||||
/* 0004 */ be_uint32_t map_number;
|
||||
/* 0008 */ uint8_t width;
|
||||
/* 0009 */ uint8_t height;
|
||||
/* 000A */ uint8_t environment_number;
|
||||
/* 000B */ uint8_t num_alt_maps;
|
||||
/* 000C */ parray<parray<uint8_t, 0x10>, 0x10> map_tiles;
|
||||
/* 010C */ parray<parray<uint8_t, 6>, 2> start_tile_definitions;
|
||||
/* 0118 */ parray<parray<parray<parray<uint8_t, 0x10>, 0x10>, 0x0A>, 2> alt_maps1;
|
||||
/* 1518 */ parray<parray<parray<be_float, 0x12>, 0x0A>, 2> alt_maps_unknown_a3;
|
||||
/* 1AB8 */ parray<parray<be_float, 0x24>, 3> unknown_a4;
|
||||
/* 1C68 */ parray<parray<uint8_t, 0x10>, 0x10> modification_tiles;
|
||||
/* 1D68 */ parray<uint8_t, 0x6C> unknown_a5;
|
||||
/* 1DD4 */ Rules default_rules;
|
||||
/* 1DE4 */ parray<uint8_t, 4> unknown_a6;
|
||||
/* 1DE8 */ ptext<char, 0x14> name;
|
||||
/* 1DFC */ ptext<char, 0x14> location_name;
|
||||
/* 1E10 */ ptext<char, 0x3C> quest_name;
|
||||
/* 1E4C */ ptext<char, 0x190> description;
|
||||
/* 1FDC */ be_uint16_t map_x;
|
||||
/* 1FDE */ be_uint16_t map_y;
|
||||
/* 1FE0 */ parray<MapDefinition::NPCDeck, 3> npc_decks;
|
||||
/* 20E8 */ parray<MapDefinition::NPCCharacter, 3> npc_chars;
|
||||
/* 2424 */ parray<uint8_t, 8> unknown_a7_a;
|
||||
/* 242C */ parray<be_uint32_t, 3> unknown_a7_b;
|
||||
/* 2438 */ ptext<char, 0x190> before_message;
|
||||
/* 25C8 */ ptext<char, 0x190> after_message;
|
||||
/* 2758 */ ptext<char, 0x190> dispatch_message;
|
||||
/* 28E8 */ parray<parray<MapDefinition::DialogueSet, 8>, 3> dialogue_sets;
|
||||
/* 4148 */ parray<be_uint16_t, 0x10> reward_card_ids;
|
||||
/* 4168 */ be_uint32_t unknown_a9_a;
|
||||
/* 416C */ be_uint32_t unknown_a9_b;
|
||||
/* 4170 */ be_uint16_t unknown_a9_c;
|
||||
/* 4172 */ be_uint16_t unknown_a9_d;
|
||||
/* 4174 */ uint8_t unknown_a10;
|
||||
/* 4175 */ uint8_t cyber_block_type;
|
||||
/* 4176 */ parray<uint8_t, 2> unknown_a11;
|
||||
// TODO: This field may contain some version of unavailable_sc_cards and/or
|
||||
// entry_states from MapDefinition, but the format isn't the same
|
||||
/* 4178 */ parray<uint8_t, 0x28> unknown_t12;
|
||||
/* 41A0 */
|
||||
|
||||
MapDefinitionTrial(const MapDefinition& map);
|
||||
} __attribute__((packed));
|
||||
|
||||
struct COMDeckDefinition {
|
||||
size_t index;
|
||||
std::string player_name;
|
||||
@@ -1000,10 +1056,11 @@ public:
|
||||
MapEntry(const MapDefinition& map, bool is_quest);
|
||||
MapEntry(const std::string& compressed_data, bool is_quest);
|
||||
|
||||
std::string compressed() const;
|
||||
const std::string& compressed(bool is_trial) const;
|
||||
|
||||
private:
|
||||
mutable std::string compressed_data;
|
||||
mutable std::string compressed_trial_data;
|
||||
};
|
||||
|
||||
const std::string& get_compressed_list() const;
|
||||
|
||||
@@ -225,8 +225,9 @@ void Server::send_6xB4x46() const {
|
||||
this->send(cmd46);
|
||||
}
|
||||
|
||||
string Server::prepare_6xB6x41_map_definition(shared_ptr<const MapIndex::MapEntry> map) {
|
||||
const auto& compressed = map->compressed();
|
||||
string Server::prepare_6xB6x41_map_definition(
|
||||
shared_ptr<const MapIndex::MapEntry> map, bool is_trial) {
|
||||
const auto& compressed = map->compressed(is_trial);
|
||||
|
||||
StringWriter w;
|
||||
uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_GC_Ep3_6xB6x41) + 3) & (~3);
|
||||
@@ -235,7 +236,7 @@ string Server::prepare_6xB6x41_map_definition(shared_ptr<const MapIndex::MapEntr
|
||||
return std::move(w.str());
|
||||
}
|
||||
|
||||
void Server::send_commands_for_joining_spectator(Channel& c) const {
|
||||
void Server::send_commands_for_joining_spectator(Channel& c, bool is_trial) 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
|
||||
@@ -248,7 +249,7 @@ void Server::send_commands_for_joining_spectator(Channel& c) const {
|
||||
|
||||
auto map = this->base()->last_chosen_map;
|
||||
if (map) {
|
||||
string data = this->prepare_6xB6x41_map_definition(map);
|
||||
string data = this->prepare_6xB6x41_map_definition(map, is_trial);
|
||||
c.send(0x6C, 0x00, data);
|
||||
}
|
||||
|
||||
@@ -2167,7 +2168,8 @@ void Server::handle_6xB3x41_map_request(const string& data) {
|
||||
}
|
||||
|
||||
base->last_chosen_map = base->map_index->definition_for_number(cmd.map_number);
|
||||
auto out_cmd = this->prepare_6xB6x41_map_definition(base->last_chosen_map);
|
||||
auto out_cmd = this->prepare_6xB6x41_map_definition(
|
||||
base->last_chosen_map, l->flags & Lobby::Flag::IS_EP3_TRIAL);
|
||||
send_command(l, 0x6C, 0x00, out_cmd);
|
||||
for (auto watcher_l : l->watcher_lobbies) {
|
||||
send_command_if_not_loading(watcher_l, 0x6C, 0x00, out_cmd);
|
||||
|
||||
@@ -120,7 +120,7 @@ public:
|
||||
}
|
||||
void send(const void* data, size_t size) const;
|
||||
|
||||
void send_commands_for_joining_spectator(Channel& ch) const;
|
||||
void send_commands_for_joining_spectator(Channel& ch, bool is_trial) const;
|
||||
|
||||
__attribute__((format(printf, 2, 3))) void log_debug(const char* fmt, ...) const;
|
||||
|
||||
@@ -233,7 +233,7 @@ public:
|
||||
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 MapIndex::MapEntry> map);
|
||||
std::shared_ptr<const MapIndex::MapEntry> map, bool is_trial);
|
||||
G_SetTrapTileLocations_GC_Ep3_6xB4x50 prepare_6xB4x50_trap_tile_locations() const;
|
||||
|
||||
std::vector<std::shared_ptr<Card>> const_cast_set_cards_v(
|
||||
|
||||
@@ -916,9 +916,9 @@ static HandlerResult S_6x(shared_ptr<ServerState>,
|
||||
string map_data = prs_decompress(
|
||||
data.data() + sizeof(cmd), data.size() - sizeof(cmd));
|
||||
save_file(filename, map_data);
|
||||
if (map_data.size() != sizeof(Episode3::MapDefinition)) {
|
||||
session.log.warning("Wrote %zu bytes to %s (expected %zu bytes; the file may be invalid)",
|
||||
map_data.size(), filename.c_str(), sizeof(Episode3::MapDefinition));
|
||||
if (map_data.size() != sizeof(Episode3::MapDefinition) && map_data.size() != sizeof(Episode3::MapDefinitionTrial)) {
|
||||
session.log.warning("Wrote %zu bytes to %s (expected %zu or %zu bytes; the file may be invalid)",
|
||||
map_data.size(), filename.c_str(), sizeof(Episode3::MapDefinitionTrial), sizeof(Episode3::MapDefinition));
|
||||
} else {
|
||||
session.log.info("Wrote %zu bytes to %s", map_data.size(), filename.c_str());
|
||||
}
|
||||
|
||||
@@ -1241,7 +1241,7 @@ static void on_DC_Ep3(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
if (tourn) {
|
||||
send_ep3_set_tournament_player_decks(s, l, c, l->tournament_match);
|
||||
string data = Episode3::Server::prepare_6xB6x41_map_definition(
|
||||
tourn->get_map());
|
||||
tourn->get_map(), l->flags & Lobby::Flag::IS_EP3_TRIAL);
|
||||
c->channel.send(0x6C, 0x00, data);
|
||||
}
|
||||
}
|
||||
@@ -3525,7 +3525,8 @@ static void on_6F(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
if (l->battle_player && (l->flags & Lobby::Flag::START_BATTLE_PLAYER_IMMEDIATELY)) {
|
||||
l->battle_player->start();
|
||||
} else if (watched_lobby && watched_lobby->ep3_server_base) {
|
||||
watched_lobby->ep3_server_base->server->send_commands_for_joining_spectator(c->channel);
|
||||
watched_lobby->ep3_server_base->server->send_commands_for_joining_spectator(
|
||||
c->channel, c->flags & Client::Flag::IS_EP3_TRIAL_EDITION);
|
||||
}
|
||||
|
||||
// If there are more players to bring in, try to do so
|
||||
|
||||
Reference in New Issue
Block a user