reorganize BB file loading abstractions

This commit is contained in:
Martin Michelsen
2022-09-03 01:13:11 -07:00
parent 89285fef98
commit 9a35f5ca63
41 changed files with 870 additions and 607 deletions
+3
View File
@@ -29,3 +29,6 @@ old-khyller
old-newserv
release
release.zip
system/patch-bb/data
system/patch-bb/psobb.pat
system/dol
+1
View File
@@ -54,6 +54,7 @@ add_executable(newserv
src/Episode3.cc
src/FileContentsCache.cc
src/FunctionCompiler.cc
src/GSLArchive.cc
src/IPFrameInfo.cc
src/IPStackSimulator.cc
src/Items.cc
+16 -5
View File
@@ -38,7 +38,7 @@ Current known issues / missing features:
- PSOBB is not well-tested and likely will disconnect or misbehave when clients try to use unimplemented features.
- Fix some edge cases on the BB proxy server (e.g. make sure Change Ship does the right thing, which is not the same as what it should do on V2/V3).
- PSOX is not tested at all.
- Patches currently are platform-specific but not version-specific. This makes them quite a bit harder to write and use properly.
- Memory patches currently are platform-specific but not version-specific. This makes them quite a bit harder to write and use properly.
- Find a way to silence audio in RunDOL.s. Some old DOLs don't reset audio systems at load time and it's annoying to hear the crash buzz when the GC hasn't actually crashed.
- Implement private and overflow lobbies.
- Enforce client-side size limits (e.g. for 60/62 commands) on the server side as well. (For 60/62 specifically, perhaps transform them to 6C/6D if needed.)
@@ -53,14 +53,15 @@ There is a probably-not-too-old macOS ARM64 release on the newserv GitHub reposi
If you're using an older AMD64 Mac, you're running Linux, or you just want to build newserv yourself, here's what you do:
1. Make sure you have CMake and libevent installed. (`brew install cmake libevent` on macOS, `sudo apt-get install cmake libevent-dev` on most Linuxes)
2. Build and install phosg (https://github.com/fuzziqersoftware/phosg).
3. Optionally, install resource_dasm (https://github.com/fuzziqersoftware/resource_dasm). This will enable newserv to run patches and load DOL files on PSO GC clients. PSO GC clients can play PSO normally on newserv without this.
3. Optionally, install resource_dasm (https://github.com/fuzziqersoftware/resource_dasm). This will enable newserv to send memory patches and load DOL files on PSO GC clients. PSO GC clients can play PSO normally on newserv without this.
4. Run `cmake . && make` on the newserv directory.
After building newserv or downloading a release, do this to set it up and use it:
1. In the system/ directory, make a copy of config.example.json named config.json, and edit it appropriately.
2. Run `./newserv` in the newserv directory. This will start the game server and run the interactive shell. You may need `sudo` if newserv's built-in DNS server is enabled.
3. Use the interactive shell to add a license. Run `help` in the shell to see how to do this.
4. Set your client's network settings appropriately and start an online game. See the "Connecting local clients" or "Connecting remote clients" section to see how to get your game client to connect.
4. If you plan to play PSO Blue Burst on newserv, set up the patch directory appropriately. See the "Client patch directories" section below.
5. Set your client's network settings appropriately and start an online game. See the "Connecting local clients" or "Connecting remote clients" section to see how to get your game client to connect.
### Installing quests
@@ -87,11 +88,21 @@ If you've changed the contents of the quests directory, you can re-index the que
All quests, including those originally in GCI or DLQ format, are treated as online quests unless their filenames specify the dl category. newserv allows players to download all quests, even those in non-download categories.
### Patches and DOL files
### Client patch directories
If you're not playing PSO Blue Burst on newserv, you can skip these steps.
newserv implements a patch server for PSO PC and PSO BB game data. Any file or directory you put in the system/patch-bb or system/patch-pc directories will be synced to clients when they connect to the patch server.
For BB clients, newserv reads some files out of the patch data to implement game logic, so it's important that certain game files are synchronized between the server and the client. newserv contains defaults for these files in the system/blueburst/map directory, but if these don't match the client's copies of the files, odd behavior will occur in games.
Specifically, the patch-bb directory should contain at least the data.gsl file and all map_*.dat files from the version of PSOBB that you want to play on newserv. You can copy these files out of the client's data directory from a clean installation, and put them in system/patch-bb/data.
### Memory patches and DOL files
Everything in this section requires resource_dasm to be installed, so newserv can use the PowerPC assembler and disassembler from its libresource_file library. If resource_dasm is not installed, newserv will still build and run, but these features will not be available.
You can put patches in the system/ppc directory with filenames like PatchName.patch.s and they will appear in the Patches menu for PSO GC clients that support patching. Patches are written in PowerPC assembly and are compiled when newserv is started. The PowerPC assembly system's features are documented in the comments in system/ppc/WriteMemory.s - this file is not a patch itself, but it describes how patches may be written and the restrictions that apply to them.
You can put memory patches in the system/ppc directory with filenames like PatchName.patch.s and they will appear in the Patches menu for PSO GC clients that support patching. Memory patches are written in PowerPC assembly and are compiled when newserv is started. The PowerPC assembly system's features are documented in the comments in system/ppc/WriteMemory.s - this file is not a memory patch itself, but it describes how memory patches may be written and the restrictions that apply to them.
You can also put DOL files in the system/dol directory, and they will appear in the Programs menu. Selecting a DOL file there will load the file into the GameCube's memory and run it, just like the old homebrew loaders (PSUL and PSOload) did. For this to work, ReadMemoryWord.s, WriteMemory.s, and RunDOL.s must be present in the system/ppc directory. This has been tested on Dolphin but not on a real GameCube, so results may vary.
+67
View File
@@ -0,0 +1,67 @@
#include "GSLArchive.hh"
#include <phosg/Encoding.hh>
#include <phosg/Filesystem.hh>
#include <stdexcept>
#include "Text.hh"
using namespace std;
// TODO: Support big-endian GSLs also (e.g. from PSO GC)
struct GSLHeaderEntry {
ptext<char, 0x20> filename;
le_uint32_t offset; // In pages, so actual offset is this * 0x800
le_uint32_t size;
uint64_t unused;
};
GSLArchive::GSLArchive(shared_ptr<const string> data) : data(data) {
StringReader r(*this->data);
uint64_t min_data_offset = 0xFFFFFFFFFFFFFFFF;
while (r.where() < min_data_offset) {
const auto& entry = r.get<GSLHeaderEntry>();
if (entry.filename.len() == 0) {
break;
}
uint64_t offset = static_cast<uint64_t>(entry.offset) * 0x800;
if (offset + entry.size > this->data->size()) {
throw runtime_error("GSL entry extends beyond end of data");
}
this->entries.emplace(entry.filename, Entry{offset, entry.size});
}
}
const unordered_map<string, GSLArchive::Entry> GSLArchive::all_entries() const {
return this->entries;
}
pair<const void*, size_t> GSLArchive::get(const std::string& name) const {
try {
const auto& entry = this->entries.at(name);
return make_pair(this->data->data() + entry.offset, entry.size);
} catch (const out_of_range&) {
throw out_of_range("GSL does not contain file: " + name);
}
}
string GSLArchive::get_copy(const string& name) const {
try {
const auto& entry = this->entries.at(name);
return this->data->substr(entry.offset, entry.size);
} catch (const out_of_range&) {
throw out_of_range("GSL does not contain file: " + name);
}
}
StringReader GSLArchive::get_reader(const string& name) const {
try {
const auto& entry = this->entries.at(name);
return StringReader(this->data->data() + entry.offset, entry.size);
} catch (const out_of_range&) {
throw out_of_range("GSL does not contain file: " + name);
}
}
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <stdint.h>
#include <phosg/Filesystem.hh>
#include <phosg/Strings.hh>
#include <string>
#include <unordered_map>
class GSLArchive {
public:
GSLArchive(std::shared_ptr<const std::string> data);
~GSLArchive() = default;
struct Entry {
uint64_t offset;
uint32_t size;
};
const std::unordered_map<std::string, Entry> all_entries() const;
std::pair<const void*, size_t> get(const std::string& name) const;
std::string get_copy(const std::string& name) const;
StringReader get_reader(const std::string& name) const;
private:
std::shared_ptr<const std::string> data;
std::unordered_map<std::string, Entry> entries;
};
+8 -8
View File
@@ -10,24 +10,24 @@ using namespace std;
LevelTable::LevelTable(const string& filename, bool compressed) {
string data = load_file(filename);
LevelTable::LevelTable(shared_ptr<const string> data, bool compressed) {
if (compressed) {
data = prs_decompress(data);
this->data.reset(new string(prs_decompress(*data)));
} else {
this->data = data;
}
if (data.size() < sizeof(*this)) {
if (this->data->size() < sizeof(Table)) {
throw invalid_argument("level table size is incorrect");
}
memcpy(this, data.data(), sizeof(*this));
this->table = reinterpret_cast<const Table*>(this->data->data());
}
const PlayerStats& LevelTable::base_stats_for_class(uint8_t char_class) const {
if (char_class >= 12) {
throw out_of_range("invalid character class");
}
return this->base_stats[char_class];
return this->table->base_stats[char_class];
}
const LevelTable::LevelStats& LevelTable::stats_for_level(uint8_t char_class,
@@ -38,7 +38,7 @@ const LevelTable::LevelStats& LevelTable::stats_for_level(uint8_t char_class,
if (level >= 200) {
throw invalid_argument("invalid character level");
}
return this->levels[char_class][level];
return this->table->levels[char_class][level];
}
void LevelTable::LevelStats::apply(PlayerStats& ps) const {
+13 -6
View File
@@ -19,7 +19,8 @@ struct PlayerStats {
PlayerStats() noexcept;
} __attribute__((packed));
struct LevelTable { // from PlyLevelTbl.prs
class LevelTable { // from PlyLevelTbl.prs
public:
struct LevelStats {
uint8_t atp;
uint8_t mst;
@@ -34,12 +35,18 @@ struct LevelTable { // from PlyLevelTbl.prs
void apply(PlayerStats& ps) const;
} __attribute__((packed));
PlayerStats base_stats[12];
le_uint32_t unknown[12];
LevelStats levels[12][200];
struct Table {
PlayerStats base_stats[12];
le_uint32_t unknown[12];
LevelStats levels[12][200];
} __attribute__((packed));
LevelTable(const std::string& filename, bool compressed);
LevelTable(std::shared_ptr<const std::string> data, bool compressed);
const PlayerStats& base_stats_for_class(uint8_t char_class) const;
const LevelStats& stats_for_level(uint8_t char_class, uint8_t level) const;
} __attribute__((packed));
private:
std::shared_ptr<const std::string> data;
const Table* table;
};
-1
View File
@@ -54,7 +54,6 @@ struct Lobby {
uint8_t area;
};
std::vector<PSOEnemy> enemies;
std::shared_ptr<const RareItemSet> rare_item_set;
std::array<uint32_t, 12> next_item_id;
uint32_t next_game_item_id;
PlayerInventoryItem next_drop_item;
+52 -15
View File
@@ -252,6 +252,9 @@ Specifically:\n\
session. This is used for regression testing, to make sure client\n\
sessions are repeatable and code changes don\'t affect existing (working)\n\
functionality.\n\
--extract-gsl=FILENAME\n\
This option makes newserv extract all files from a GSL archive and place\n\
them in the current directory.\n\
\n\
A few options apply to multiple modes described above:\n\
--parse-data\n\
@@ -268,6 +271,7 @@ enum class Behavior {
ENCRYPT_DATA,
DECODE_QUEST_FILE,
DECODE_SJIS,
EXTRACT_GSL,
REPLAY_LOG,
CAT_CLIENT,
};
@@ -289,6 +293,7 @@ int main(int argc, char** argv) {
bool parse_data = false;
bool byteswap_data = false;
const char* replay_log_filename = nullptr;
const char* extract_gsl_filename = nullptr;
const char* replay_required_access_key = "";
const char* replay_required_password = "";
struct sockaddr_storage cat_client_remote;
@@ -340,6 +345,9 @@ int main(int argc, char** argv) {
} else if (!strncmp(argv[x], "--replay-log=", 13)) {
behavior = Behavior::REPLAY_LOG;
replay_log_filename = &argv[x][13];
} else if (!strncmp(argv[x], "--extract-gsl=", 14)) {
behavior = Behavior::EXTRACT_GSL;
extract_gsl_filename = &argv[x][14];
} else if (!strncmp(argv[x], "--require-password=", 19)) {
replay_required_password = &argv[x][19];
} else if (!strncmp(argv[x], "--require-access-key=", 21)) {
@@ -438,6 +446,17 @@ int main(int argc, char** argv) {
break;
}
case Behavior::EXTRACT_GSL: {
shared_ptr<string> data(new string(load_file(extract_gsl_filename)));
GSLArchive gsl(data);
for (const auto& entry_it : gsl.all_entries()) {
auto e = gsl.get(entry_it.first);
save_file(entry_it.first, e.first, e.second);
fprintf(stderr, "... %s\n", entry_it.first.c_str());
}
break;
}
case Behavior::CAT_CLIENT: {
shared_ptr<PSOBBEncryption::KeyFile> key;
if (cli_version == GameVersion::BB) {
@@ -483,11 +502,42 @@ int main(int argc, char** argv) {
state->license_manager.reset(new LicenseManager());
}
if (isdir("system/patch-pc")) {
config_log.info("Indexing PSO PC patch files");
state->pc_patch_file_index.reset(new PatchFileIndex("system/patch-pc"));
} else {
config_log.info("PSO PC patch files not present");
}
if (isdir("system/patch-bb")) {
config_log.info("Indexing PSO BB patch files");
state->bb_patch_file_index.reset(new PatchFileIndex("system/patch-bb"));
try {
auto gsl_file = state->bb_patch_file_index->get("./data/data.gsl");
state->bb_data_gsl.reset(new GSLArchive(gsl_file->data));
config_log.info("data.gsl found in BB patch files");
} catch (const out_of_range&) {
config_log.info("data.gsl is not present in BB patch files");
}
} else {
config_log.info("PSO BB patch files not present");
}
config_log.info("Loading battle parameters");
state->battle_params.reset(new BattleParamsIndex("system/blueburst/BattleParamEntry"));
state->battle_params.reset(new BattleParamsIndex(
state->load_bb_file("BattleParamEntry_on.dat"),
state->load_bb_file("BattleParamEntry_lab_on.dat"),
state->load_bb_file("BattleParamEntry_ep4_on.dat"),
state->load_bb_file("BattleParamEntry.dat"),
state->load_bb_file("BattleParamEntry_lab.dat"),
state->load_bb_file("BattleParamEntry_ep4.dat")));
config_log.info("Loading level table");
state->level_table.reset(new LevelTable("system/blueburst/PlyLevelTbl.prs", true));
state->level_table.reset(new LevelTable(
state->load_bb_file("PlyLevelTbl.prs"), true));
config_log.info("Loading rare table");
state->rare_item_set.reset(new RareItemSet(
state->load_bb_file("ItemRT.rel")));
config_log.info("Collecting Episode 3 data");
state->ep3_data_index.reset(new Ep3DataIndex("system/ep3"));
@@ -505,19 +555,6 @@ int main(int argc, char** argv) {
state->dol_file_index.reset(new DOLFileIndex());
}
if (isdir("system/patch-pc")) {
config_log.info("Indexing PSO PC patch files");
state->pc_patch_file_index.reset(new PatchFileIndex("system/patch-pc"));
} else {
config_log.info("PSO PC patch files not present");
}
if (isdir("system/patch-bb")) {
config_log.info("Indexing PSO BB patch files");
state->bb_patch_file_index.reset(new PatchFileIndex("system/patch-bb"));
} else {
config_log.info("PSO BB patch files not present");
}
config_log.info("Creating menus");
state->create_menus(config_json);
+370 -114
View File
@@ -10,36 +10,35 @@ using namespace std;
BattleParamsIndex::BattleParamsIndex(const char* prefix) {
BattleParamsIndex::BattleParamsIndex(
shared_ptr<const string> data_on_ep1,
shared_ptr<const string> data_on_ep2,
shared_ptr<const string> data_on_ep4,
shared_ptr<const string> data_off_ep1,
shared_ptr<const string> data_off_ep2,
shared_ptr<const string> data_off_ep4) {
this->files[0][0].data = data_on_ep1;
this->files[0][1].data = data_on_ep2;
this->files[0][2].data = data_on_ep4;
this->files[1][0].data = data_off_ep1;
this->files[1][1].data = data_off_ep2;
this->files[1][2].data = data_off_ep4;
for (uint8_t is_solo = 0; is_solo < 2; is_solo++) {
for (uint8_t episode = 0; episode < 3; episode++) {
string filename = prefix;
if (episode == 1) {
filename += "_lab";
} else if (episode == 2) {
filename += "_ep4";
auto& file = this->files[is_solo][episode];
if (file.data->size() < sizeof(Table)) {
throw runtime_error(string_printf(
"battle params table size is incorrect (expected %zX bytes, have %zX bytes; is_solo=%hhu, episode=%hhu)",
sizeof(Table), file.data->size(), is_solo, episode));
}
if (!is_solo) {
filename += "_on";
}
filename += ".dat";
this->entries[is_solo][episode][0].reset(new TableT());
this->entries[is_solo][episode][1].reset(new TableT());
this->entries[is_solo][episode][2].reset(new TableT());
this->entries[is_solo][episode][3].reset(new TableT());
scoped_fd fd(filename, O_RDONLY);
readx(fd, this->entries[is_solo][episode][0].get(), sizeof(TableT));
readx(fd, this->entries[is_solo][episode][1].get(), sizeof(TableT));
readx(fd, this->entries[is_solo][episode][2].get(), sizeof(TableT));
readx(fd, this->entries[is_solo][episode][3].get(), sizeof(TableT));
file.table = reinterpret_cast<const Table*>(file.data->data());
}
}
}
const BattleParams& BattleParamsIndex::get(bool solo, uint8_t episode,
uint8_t difficulty, uint8_t monster_type) const {
const BattleParamsIndex::Entry& BattleParamsIndex::get(
bool solo, uint8_t episode, uint8_t difficulty, uint8_t monster_type) const {
if (episode > 3) {
throw invalid_argument("incorrect episode");
}
@@ -49,19 +48,7 @@ const BattleParams& BattleParamsIndex::get(bool solo, uint8_t episode,
if (monster_type > 0x60) {
throw invalid_argument("incorrect monster type");
}
return (*this->entries[!!solo][episode][difficulty])[monster_type];
}
shared_ptr<const BattleParamsIndex::TableT>
BattleParamsIndex::get_subtable(
bool solo, uint8_t episode, uint8_t difficulty) const {
if (episode > 3) {
throw invalid_argument("incorrect episode");
}
if (difficulty > 4) {
throw invalid_argument("incorrect difficulty");
}
return this->entries[!!solo][episode][difficulty];
return this->files[!!solo][episode].table->difficulty[difficulty][monster_type];
}
@@ -102,16 +89,16 @@ struct EnemyEntry {
static uint64_t next_enemy_id = 1;
vector<PSOEnemy> parse_map(
shared_ptr<const BattleParamsIndex> battle_params,
bool is_solo,
uint8_t episode,
uint8_t difficulty,
shared_ptr<const BattleParamsIndex::TableT> battle_params_table,
const void* data,
size_t size,
shared_ptr<const string> data,
bool alt_enemies) {
const auto* map = reinterpret_cast<const EnemyEntry*>(data);
size_t entry_count = size / sizeof(EnemyEntry);
if (size != entry_count * sizeof(EnemyEntry)) {
const auto* map = reinterpret_cast<const EnemyEntry*>(data->data());
size_t entry_count = data->size() / sizeof(EnemyEntry);
if (data->size() != entry_count * sizeof(EnemyEntry)) {
throw runtime_error("data size is not a multiple of entry size");
}
@@ -122,7 +109,10 @@ vector<PSOEnemy> parse_map(
}
};
const auto& battle_params = *battle_params_table;
auto get_battle_params = [&](uint8_t type) -> const BattleParamsIndex::Entry& {
return battle_params->get(is_solo, episode, difficulty, type);
};
for (size_t y = 0; y < entry_count; y++) {
const auto& e = map[y];
size_t num_clones = e.num_clones;
@@ -130,101 +120,101 @@ vector<PSOEnemy> parse_map(
switch (e.base) {
case 0x40: // Hildebear and Hildetorr
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x49 + (e.skin & 0x01)].experience,
get_battle_params(0x49 + (e.skin & 0x01)).experience,
0x01 + (e.skin & 0x01));
break;
case 0x41: // Rappies
if (episode == 3) { // Del Rappy and Sand Rappy
if (alt_enemies) {
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x17 + (e.skin & 0x01)].experience,
get_battle_params(0x17 + (e.skin & 0x01)).experience,
17 + (e.skin & 0x01));
} else {
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x05 + (e.skin & 0x01)].experience,
get_battle_params(0x05 + (e.skin & 0x01)).experience,
17 + (e.skin & 0x01));
}
} else { // Rag Rappy and Al Rappy (Love for Episode II)
if (e.skin & 0x01) {
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x18 + (e.skin & 0x01)].experience,
get_battle_params(0x18 + (e.skin & 0x01)).experience,
0xFF); // Don't know (yet) which rare Rappy it is
} else {
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x18 + (e.skin & 0x01)].experience,
get_battle_params(0x18 + (e.skin & 0x01)).experience,
5);
}
}
break;
case 0x42: // Monest + 30 Mothmants
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x01].experience, 4);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x01).experience, 4);
for (size_t x = 0; x < 30; x++) {
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x00].experience, 3);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x00).experience, 3);
}
break;
case 0x43: // Savage Wolf and Barbarous Wolf
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x02 + ((e.reserved[10] & 0x800000) ? 1 : 0)].experience,
get_battle_params(0x02 + ((e.reserved[10] & 0x800000) ? 1 : 0)).experience,
7 + ((e.reserved[10] & 0x800000) ? 1 : 0));
break;
case 0x44: // Booma family
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x4B + (e.skin % 3)].experience,
get_battle_params(0x4B + (e.skin % 3)).experience,
9 + (e.skin % 3));
break;
case 0x60: // Grass Assassin
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x4E].experience, 12);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x4E).experience, 12);
break;
case 0x61: // Del Lily, Poison Lily, Nar Lily
if ((episode == 2) && (alt_enemies)) {
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x25].experience, 83);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x25).experience, 83);
} else {
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x04 + ((e.reserved[10] & 0x800000) ? 1 : 0)].experience,
get_battle_params(0x04 + ((e.reserved[10] & 0x800000) ? 1 : 0)).experience,
13 + ((e.reserved[10] & 0x800000) ? 1 : 0));
}
break;
case 0x62: // Nano Dragon
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x1A].experience, 15);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x1A).experience, 15);
break;
case 0x63: // Shark family
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x4F + (e.skin % 3)].experience,
get_battle_params(0x4F + (e.skin % 3)).experience,
16 + (e.skin % 3));
break;
case 0x64: // Slime + 4 clones
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x2F + ((e.reserved[10] & 0x800000) ? 0 : 1)].experience,
get_battle_params(0x2F + ((e.reserved[10] & 0x800000) ? 0 : 1)).experience,
19 + ((e.reserved[10] & 0x800000) ? 1 : 0));
for (size_t x = 0; x < 4; x++) {
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x30].experience, 19);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x30).experience, 19);
}
break;
case 0x65: // Pan Arms, Migium, Hidoom
for (size_t x = 0; x < 3; x++) {
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x31 + x].experience, 21 + x);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x31 + x).experience, 21 + x);
}
break;
case 0x80: // Dubchic and Gillchic
if (e.skin & 0x01) {
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x1B + (e.skin & 0x01)].experience, 50);
get_battle_params(0x1B + (e.skin & 0x01)).experience, 50);
} else {
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x1B + (e.skin & 0x01)].experience, 24);
get_battle_params(0x1B + (e.skin & 0x01)).experience, 24);
}
break;
case 0x81: // Garanz
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x1D].experience, 25);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x1D).experience, 25);
break;
case 0x82: // Sinow Beat and Gold
if (e.reserved[10] & 0x800000) {
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x13].experience,
get_battle_params(0x13).experience,
26 + ((e.reserved[10] & 0x800000) ? 1 : 0));
} else {
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x06].experience,
get_battle_params(0x06).experience,
26 + ((e.reserved[10] & 0x800000) ? 1 : 0));
}
if (e.num_clones == 0) {
@@ -232,193 +222,193 @@ vector<PSOEnemy> parse_map(
}
break;
case 0x83: // Canadine
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x07].experience, 28);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x07).experience, 28);
break;
case 0x84: // Canadine Group
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x09].experience, 29);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x09).experience, 29);
for (size_t x = 0; x < 8; x++) {
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x08].experience, 28);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x08).experience, 28);
}
break;
case 0x85: // Dubwitch
break;
case 0xA0: // Delsaber
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x52].experience, 30);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x52).experience, 30);
break;
case 0xA1: // Chaos Sorcerer + 2 Bits
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x0A].experience, 31);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x0A).experience, 31);
create_clones(2);
break;
case 0xA2: // Dark Gunner
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x1E].experience, 34);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x1E).experience, 34);
break;
case 0xA4: // Chaos Bringer
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x0D].experience, 36);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x0D).experience, 36);
break;
case 0xA5: // Dark Belra
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x0E].experience, 37);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x0E).experience, 37);
break;
case 0xA6: // Dimenian family
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x53 + (e.skin % 3)].experience, 41 + (e.skin % 3));
get_battle_params(0x53 + (e.skin % 3)).experience, 41 + (e.skin % 3));
break;
case 0xA7: // Bulclaw + 4 claws
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x1F].experience, 40);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x1F).experience, 40);
for (size_t x = 0; x < 4; x++) {
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x20].experience, 38);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x20).experience, 38);
}
break;
case 0xA8: // Claw
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x20].experience, 38);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x20).experience, 38);
break;
case 0xC0: // Dragon or Gal Gryphon
if (episode == 1) {
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x12].experience, 44);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x12).experience, 44);
} else if (episode == 2) {
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x1E].experience, 77);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x1E).experience, 77);
}
break;
case 0xC1: // De Rol Le
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x0F].experience, 45);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x0F).experience, 45);
break;
case 0xC2: // Vol Opt form 1
break;
case 0xC5: // Vol Opt form 2
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x25].experience, 46);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x25).experience, 46);
break;
case 0xC8: // Dark Falz + 510 Helpers
if (difficulty) {
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x38].experience, 47); // Final form
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x38).experience, 47); // Final form
} else {
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x37].experience, 47); // Second form
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x37).experience, 47); // Second form
}
for (size_t x = 0; x < 510; x++) {
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x35].experience, 0);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x35).experience, 0);
}
break;
case 0xCA: // Olga Flow
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x2C].experience, 78);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x2C).experience, 78);
create_clones(0x200);
break;
case 0xCB: // Barba Ray
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x0F].experience, 73);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x0F).experience, 73);
create_clones(0x2F);
break;
case 0xCC: // Gol Dragon
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x12].experience, 76);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x12).experience, 76);
create_clones(5);
break;
case 0xD4: // Sinows Berill & Spigell
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[(e.reserved[10] & 0x800000) ? 0x13 : 0x06].experience,
get_battle_params((e.reserved[10] & 0x800000) ? 0x13 : 0x06).experience,
62 + ((e.reserved[10] & 0x800000) ? 1 : 0));
create_clones(4);
break;
case 0xD5: // Merillia & Meriltas
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x4B + (e.skin & 0x01)].experience,
get_battle_params(0x4B + (e.skin & 0x01)).experience,
52 + (e.skin & 0x01));
break;
case 0xD6: // Mericus, Merikle, & Mericarol
if (e.skin) {
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x44 + (e.skin % 3)].experience, 56 + (e.skin % 3));
get_battle_params(0x44 + (e.skin % 3)).experience, 56 + (e.skin % 3));
} else {
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x3A].experience, 56 + (e.skin % 3));
get_battle_params(0x3A).experience, 56 + (e.skin % 3));
}
break;
case 0xD7: // Ul Gibbon and Zol Gibbon
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x3B + (e.skin & 0x01)].experience,
get_battle_params(0x3B + (e.skin & 0x01)).experience,
59 + (e.skin & 0x01));
break;
case 0xD8: // Gibbles
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x3D].experience, 61);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x3D).experience, 61);
break;
case 0xD9: // Gee
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x07].experience, 54);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x07).experience, 54);
break;
case 0xDA: // Gi Gue
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x1A].experience, 55);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x1A).experience, 55);
break;
case 0xDB: // Deldepth
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x30].experience, 71);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x30).experience, 71);
break;
case 0xDC: // Delbiter
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x0D].experience, 72);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x0D).experience, 72);
break;
case 0xDD: // Dolmolm and Dolmdarl
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x4F + (e.skin & 0x01)].experience,
get_battle_params(0x4F + (e.skin & 0x01)).experience,
64 + (e.skin & 0x01));
break;
case 0xDE: // Morfos
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x40].experience, 66);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x40).experience, 66);
break;
case 0xDF: // Recobox & Recons
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x41].experience, 67);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x41).experience, 67);
for (size_t x = 0; x < e.num_clones; x++) {
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x42].experience, 68);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x42).experience, 68);
}
break;
case 0xE0: // Epsilon, Sinow Zoa and Zele
if ((episode == 2) && (alt_enemies)) {
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x23].experience, 84);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x23).experience, 84);
create_clones(4);
} else {
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x43 + (e.skin & 0x01)].experience,
get_battle_params(0x43 + (e.skin & 0x01)).experience,
69 + (e.skin & 0x01));
}
break;
case 0xE1: // Ill Gill
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x26].experience, 82);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x26).experience, 82);
break;
case 0x0110: // Astark
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x09].experience, 1);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x09).experience, 1);
break;
case 0x0111: // Satellite Lizard and Yowie
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x0D + ((e.reserved[10] & 0x800000) ? 1 : 0) + (alt_enemies ? 0x10 : 0)].experience,
get_battle_params(0x0D + ((e.reserved[10] & 0x800000) ? 1 : 0) + (alt_enemies ? 0x10 : 0)).experience,
2 + ((e.reserved[10] & 0x800000) ? 0 : 1));
break;
case 0x0112: // Merissa A/AA
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x19 + (e.skin & 0x01)].experience,
get_battle_params(0x19 + (e.skin & 0x01)).experience,
4 + (e.skin & 0x01));
break;
case 0x0113: // Girtablulu
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x1F].experience, 6);
enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x1F).experience, 6);
break;
case 0x0114: // Zu and Pazuzu
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x0B + (e.skin & 0x01) + (alt_enemies ? 0x14: 0x00)].experience,
get_battle_params(0x0B + (e.skin & 0x01) + (alt_enemies ? 0x14: 0x00)).experience,
7 + (e.skin & 0x01));
break;
case 0x0115: // Boota family
if (e.skin & 2) {
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x03].experience, 9 + (e.skin % 3));
get_battle_params(0x03).experience, 9 + (e.skin % 3));
} else {
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x00 + (e.skin % 3)].experience,
get_battle_params(0x00 + (e.skin % 3)).experience,
9 + (e.skin % 3));
}
break;
case 0x0116: // Dorphon and Eclair
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x0F + (e.skin & 0x01)].experience,
get_battle_params(0x0F + (e.skin & 0x01)).experience,
12 + (e.skin & 0x01));
break;
case 0x0117: // Goran family
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x11 + (e.skin % 3)].experience,
get_battle_params(0x11 + (e.skin % 3)).experience,
(e.skin & 2) ? 15 : ((e.skin & 1) ? 16 : 14));
break;
case 0x0119: // Saint Million, Shambertin, Kondrieu
enemies.emplace_back(next_enemy_id++, e.base,
battle_params[0x22].experience,
get_battle_params(0x22).experience,
(e.reserved[10] & 0x800000) ? 21 : (19 + (e.skin & 0x01)));
break;
default:
@@ -433,3 +423,269 @@ vector<PSOEnemy> parse_map(
return enemies;
}
SetDataTable::SetDataTable(shared_ptr<const string> data, bool big_endian) {
if (big_endian) {
this->load_table_t<be_uint32_t>(data);
} else {
this->load_table_t<le_uint32_t>(data);
}
}
template <typename U32T>
void SetDataTable::load_table_t(shared_ptr<const string> data) {
StringReader r(*data);
struct Footer {
U32T table3_offset;
U32T table3_count; // In le_uint16_ts (so *2 for size in bytes)
U32T unknown_a3; // == 1
U32T unknown_a4; // == 0
U32T root_table_offset_offset;
U32T unknown_a6; // == 0
U32T unknown_a7; // == 0
U32T unknown_a8; // == 0
} __attribute__((packed));
if (r.size() < sizeof(Footer)) {
throw runtime_error("set data table is too small");
}
auto& footer = r.pget<Footer>(r.size() - sizeof(Footer));
uint32_t root_table_offset = r.pget<U32T>(footer.root_table_offset_offset);
auto root_r = r.sub(root_table_offset, footer.root_table_offset_offset - root_table_offset);
while (!root_r.eof()) {
auto& var1_v = this->entries.emplace_back();
uint32_t var1_table_offset = root_r.template get<U32T>();
uint32_t var1_table_count = root_r.template get<U32T>();
auto var1_r = r.sub(var1_table_offset, var1_table_count * 0x08);
while (!var1_r.eof()) {
auto& var2_v = var1_v.emplace_back();
uint32_t var2_table_offset = var1_r.get<U32T>();
uint32_t var2_table_count = var1_r.get<U32T>();
auto var2_r = r.sub(var2_table_offset, var2_table_count * 0x0C);
while (!var2_r.eof()) {
auto& entry = var2_v.emplace_back();
entry.name1 = r.pget_cstr(var2_r.get<U32T>());
entry.enemy_list_basename = r.pget_cstr(var2_r.get<U32T>());
entry.name3 = r.pget_cstr(var2_r.get<U32T>());
}
}
}
}
void SetDataTable::print(FILE* stream) const {
for (size_t a = 0; a < this->entries.size(); a++) {
const auto& v1_v = this->entries[a];
for (size_t v1 = 0; v1 < v1_v.size(); v1++) {
const auto& v2_v = v1_v[v1];
for (size_t v2 = 0; v2 < v2_v.size(); v2++) {
const auto& e = v2_v[v2];
fprintf(stream, "[%02zX/%02zX/%02zX] %s %s %s\n", a, v1, v2, e.name1.c_str(), e.enemy_list_basename.c_str(), e.name3.c_str());
}
}
}
}
struct AreaMapFileIndex {
const char* name_token;
vector<uint32_t> variation1_values;
vector<uint32_t> variation2_values;
AreaMapFileIndex(
const char* name_token,
vector<uint32_t> variation1_values,
vector<uint32_t> variation2_values)
: name_token(name_token),
variation1_values(variation1_values),
variation2_values(variation2_values) { }
};
// These are indexed as [episode][is_solo][area]
// (Note that Lobby::episode is 1-3, so we actually use episode - 1)
static const vector<vector<vector<AreaMapFileIndex>>> map_file_info = {
{ // Episode 1
{ // Non-solo
{"city00", {}, {0}},
{"forest01", {}, {0, 1, 2, 3, 4}},
{"forest02", {}, {0, 1, 2, 3, 4}},
{"cave01", {0, 1, 2}, {0, 1}},
{"cave02", {0, 1, 2}, {0, 1}},
{"cave03", {0, 1, 2}, {0, 1}},
{"machine01", {0, 1, 2}, {0, 1}},
{"machine02", {0, 1, 2}, {0, 1}},
{"ancient01", {0, 1, 2}, {0, 1}},
{"ancient02", {0, 1, 2}, {0, 1}},
{"ancient03", {0, 1, 2}, {0, 1}},
{"boss01", {}, {}},
{"boss02", {}, {}},
{"boss03", {}, {}},
{"boss04", {}, {}},
{nullptr, {}, {}},
},
{ // Solo
{"city00", {}, {0}},
{"forest01", {}, {0, 2, 4}},
{"forest02", {}, {0, 3, 4}},
{"cave01", {0, 1, 2}, {0}},
{"cave02", {0, 1, 2}, {0}},
{"cave03", {0, 1, 2}, {0}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
},
},
{ // Episode 2
{ // Non-solo
{"labo00", {}, {0}},
{"ruins01", {0, 1}, {0}},
{"ruins02", {0, 1}, {0}},
{"space01", {0, 1}, {0}},
{"space02", {0, 1}, {0}},
{"jungle01", {}, {0, 1, 2}},
{"jungle02", {}, {0, 1, 2}},
{"jungle03", {}, {0, 1, 2}},
{"jungle04", {0, 1}, {0, 1}},
{"jungle05", {}, {0, 1, 2}},
{"seabed01", {0, 1}, {0, 1}},
{"seabed02", {0, 1}, {0, 1}},
{"boss05", {}, {}},
{"boss06", {}, {}},
{"boss07", {}, {}},
{"boss08", {}, {}},
},
{ // Solo
{"labo00", {}, {0}},
{"ruins01", {0, 1}, {0}},
{"ruins02", {0, 1}, {0}},
{"space01", {0, 1}, {0}},
{"space02", {0, 1}, {0}},
{"jungle01", {}, {0, 1, 2}},
{"jungle02", {}, {0, 1, 2}},
{"jungle03", {}, {0, 1, 2}},
{"jungle04", {0, 1}, {0, 1}},
{"jungle05", {}, {0, 1, 2}},
{"seabed01", {0, 1}, {0}},
{"seabed02", {0, 1}, {0}},
{"boss05", {}, {}},
{"boss06", {}, {}},
{"boss07", {}, {}},
{"boss08", {}, {}},
},
},
{ // Episode 4
{ // Non-solo
{"city02", {0}, {0}},
{"wilds01", {0}, {0, 1, 2}},
{"wilds01", {1}, {0, 1, 2}},
{"wilds01", {2}, {0, 1, 2}},
{"wilds01", {3}, {0, 1, 2}},
{"crater01", {0}, {0, 1, 2}},
{"desert01", {0, 1, 2}, {0}},
{"desert02", {0}, {0, 1, 2}},
{"desert03", {0, 1, 2}, {0}},
{"boss09", {0}, {0}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
},
{ // Solo
{"city02", {0}, {0}},
{"wilds01", {0}, {0, 1, 2}},
{"wilds01", {1}, {0, 1, 2}},
{"wilds01", {2}, {0, 1, 2}},
{"wilds01", {3}, {0, 1, 2}},
{"crater01", {0}, {0, 1, 2}},
{"desert01", {0, 1, 2}, {0}},
{"desert02", {0}, {0, 1, 2}},
{"desert03", {0, 1, 2}, {0}},
{"boss09", {0}, {0}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
},
},
};
void generate_variations(
parray<le_uint32_t, 0x20>& variations,
shared_ptr<mt19937> random,
uint8_t episode,
bool is_solo) {
const auto& ep_index = map_file_info.at(episode - 1);
for (size_t z = 0; z < 0x10; z++) {
const AreaMapFileIndex* a = nullptr;
if (is_solo) {
a = &ep_index.at(true).at(z);
}
if (!a || !a->name_token) {
a = &ep_index.at(false).at(z);
}
if (!a->name_token) {
variations[z * 2 + 0] = 0;
variations[z * 2 + 1] = 0;
} else {
variations[z * 2 + 0] = (a->variation1_values.size() < 2) ? 0 :
((*random)() % a->variation1_values.size());
variations[z * 2 + 1] = (a->variation2_values.size() < 2) ? 0 :
((*random)() % a->variation2_values.size());
}
}
}
vector<string> map_filenames_for_variation(
uint8_t episode, bool is_solo, uint8_t area, uint32_t var1, uint32_t var2) {
// Map filenames are like map_<name_token>[_VV][_VV][_off]<e|o>[_s].dat
// name_token comes from AreaMapFileIndex
// _VV are the values from the variation<1|2>_values vector (in contrast to
// the values sent in the 64 command, which are INDEXES INTO THAT VECTOR)
// _off or _s are used for solo mode (try both - city uses _s whereas levels
// use _off apparently)
// e|o specifies what kind of data: e = enemies, o = objects
const auto& ep_index = map_file_info.at(episode - 1);
const AreaMapFileIndex* a = nullptr;
if (is_solo) {
a = &ep_index.at(true).at(area);
}
if (!a || !a->name_token) {
a = &ep_index.at(false).at(area);
}
if (!a->name_token) {
return vector<string>();
}
string filename = "map_";
filename += a->name_token;
if (!a->variation1_values.empty()) {
filename += string_printf("_%02" PRIX32, a->variation1_values.at(var1));
}
if (!a->variation2_values.empty()) {
filename += string_printf("_%02" PRIX32, a->variation2_values.at(var2));
}
vector<string> ret;
if (is_solo) {
// Try both _offe.dat and e_s.dat suffixes
ret.emplace_back(filename + "_offe.dat");
ret.emplace_back(filename + "e_s.dat");
} else {
ret.emplace_back(filename + "e.dat");
}
return ret;
}
+69 -15
View File
@@ -3,14 +3,17 @@
#include <inttypes.h>
#include <phosg/Encoding.hh>
#include <vector>
#include <random>
#include <string>
#include <vector>
#include "Text.hh"
struct BattleParams {
class BattleParamsIndex {
public:
struct Entry {
le_uint16_t atp; // attack power
le_uint16_t psv; // perseverance (intelligence?)
le_uint16_t evp; // evasion
@@ -22,22 +25,31 @@ struct BattleParams {
uint8_t unknown_a1[0x0C];
le_uint32_t experience;
le_uint32_t difficulty;
} __attribute__((packed));
} __attribute__((packed));
class BattleParamsIndex {
public:
using TableT = parray<BattleParams, 0x60>;
struct Table {
parray<parray<Entry, 0x60>, 4> difficulty;
} __attribute__((packed));
BattleParamsIndex(const char* filename_prefix);
BattleParamsIndex(
std::shared_ptr<const std::string> data_on_ep1, // BattleParamEntry_on.dat
std::shared_ptr<const std::string> data_on_ep2, // BattleParamEntry_lab_on.dat
std::shared_ptr<const std::string> data_on_ep4, // BattleParamEntry_ep4_on.dat
std::shared_ptr<const std::string> data_off_ep1, // BattleParamEntry.dat
std::shared_ptr<const std::string> data_off_ep2, // BattleParamEntry_lab.dat
std::shared_ptr<const std::string> data_off_ep4); // BattleParamEntry_ep4.dat
const BattleParams& get(bool solo, uint8_t episode, uint8_t difficulty,
const Entry& get(bool solo, uint8_t episode, uint8_t difficulty,
uint8_t monster_type) const;
std::shared_ptr<const TableT> get_subtable(
bool solo, uint8_t episode, uint8_t difficulty) const;
private:
// online/offline, episode, difficulty
std::shared_ptr<TableT> entries[2][3][4];
struct LoadedFile {
std::shared_ptr<const std::string> data;
const Table* table;
};
// online/offline, episode
LoadedFile files[2][3];
};
@@ -57,9 +69,51 @@ struct PSOEnemy {
} __attribute__((packed));
std::vector<PSOEnemy> parse_map(
std::shared_ptr<const BattleParamsIndex> battle_params,
bool is_solo,
uint8_t episode,
uint8_t difficulty,
std::shared_ptr<const BattleParamsIndex::TableT> battle_params,
const void* data,
size_t size,
std::shared_ptr<const std::string> data,
bool alt_enemies);
// TODO: This class is currently unused. It would be nice if we could use this
// to generate variations and link to the corresponding map filenames, but it
// seems that SetDataTable.rel files link to map filenames that don't actually
// exist in some cases, so we can't just directly use this data structure.
class SetDataTable {
public:
struct SetEntry {
std::string name1;
std::string enemy_list_basename;
std::string name3;
};
SetDataTable(std::shared_ptr<const std::string> data, bool big_endian);
inline const std::vector<std::vector<std::vector<SetEntry>>> get() const {
return this->entries;
}
void print(FILE* stream) const;
private:
template <typename U32T>
void load_table_t(std::shared_ptr<const std::string> data);
// Indexes are [area_id][variation1][variation2]
// area_id is cumulative per episode, so Ep2 starts at area_id=18.
std::vector<std::vector<std::vector<SetEntry>>> entries;
};
void generate_variations(
parray<le_uint32_t, 0x20>& variations,
std::shared_ptr<std::mt19937> random,
uint8_t episode,
bool is_solo);
std::vector<std::string> map_filenames_for_variation(
uint8_t episode, bool is_solo, uint8_t area, uint32_t var1, uint32_t var2);
void load_map_files();
View File
+5
View File
@@ -0,0 +1,5 @@
psobb file lookups
1. look in system/patch-bb/data/<filename>
2. look in system/patch-bb/data/data.gsl :: <filename>
3. look in system/blueburst/<filename>
+25 -11
View File
@@ -15,17 +15,18 @@ using namespace std;
PatchFileIndex::File::File() : crc32(0) { }
void PatchFileIndex::File::load_data(const string& root_dir) {
string full_path = root_dir + '/' + join(this->path_directories, "/") + '/' + this->name;
auto f = fopen_unique(full_path, "rb");
this->size = 0;
this->crc32 = 0;
while (!feof(f.get())) {
auto& chunk = this->chunks.emplace_back();
chunk.data = fread(f.get(), 0x4000);
this->size += chunk.data.size();
this->crc32 = ::crc32(chunk.data.data(), chunk.data.size(), this->crc32);
string file_data = load_file(full_path);
this->data.reset(new string(move(file_data)));
this->crc32 = ::crc32(this->data->data(), this->data->size());
for (size_t x = 0; x < this->data->size(); x += 0x4000) {
size_t chunk_bytes = min<size_t>(this->data->size() - x, 0x4000);
this->chunk_crcs.emplace_back(::crc32(this->data->data() + x, chunk_bytes));
}
}
@@ -36,7 +37,8 @@ PatchFileIndex::PatchFileIndex(const string& root_dir)
function<void(const string&)> collect_dir = [&](const string& dir) -> void {
path_directories.emplace_back(dir);
string full_dir_path = root_dir + '/' + join(path_directories, "/");
string relative_dirs = join(path_directories, "/");
string full_dir_path = root_dir + '/' + relative_dirs;
patch_index_log.info("Listing directory %s", full_dir_path.c_str());
for (const auto& item : list_directory(full_dir_path)) {
@@ -45,7 +47,8 @@ PatchFileIndex::PatchFileIndex(const string& root_dir)
continue;
}
string full_item_path = full_dir_path + '/' + item;
string relative_item_path = relative_dirs + '/' + item;
string full_item_path = root_dir + '/' + relative_item_path;
if (isdir(full_item_path)) {
collect_dir(item);
} else if (isfile(full_item_path)) {
@@ -53,9 +56,10 @@ PatchFileIndex::PatchFileIndex(const string& root_dir)
f->path_directories = path_directories;
f->name = item;
f->load_data(root_dir);
this->files.emplace_back(f);
this->files_by_patch_order.emplace_back(f);
this->files_by_name.emplace(relative_item_path, f);
patch_index_log.info("Added file %s (%zu bytes; %zu chunks; %08" PRIX32 ")",
full_item_path.c_str(), f->size, f->chunks.size(), f->crc32);
full_item_path.c_str(), f->data->size(), f->chunk_crcs.size(), f->crc32);
}
}
@@ -64,3 +68,13 @@ PatchFileIndex::PatchFileIndex(const string& root_dir)
collect_dir(".");
}
const vector<shared_ptr<const PatchFileIndex::File>>&
PatchFileIndex::all_files() const {
return this->files_by_patch_order;
}
shared_ptr<const PatchFileIndex::File> PatchFileIndex::get(
const string& filename) const {
return this->files_by_name.at(filename);
}
+10 -9
View File
@@ -14,21 +14,22 @@ struct PatchFileIndex {
explicit PatchFileIndex(const std::string& root_dir);
struct File {
struct Chunk {
std::string data;
uint32_t crc32;
};
std::vector<std::string> path_directories;
std::string name;
std::vector<Chunk> chunks;
size_t size;
std::shared_ptr<const std::string> data;
std::vector<uint32_t> chunk_crcs;
uint32_t crc32;
File() : size(0), crc32(0) { }
File();
void load_data(const std::string& root_dir);
};
std::vector<std::shared_ptr<File>> files;
const std::vector<std::shared_ptr<const File>>& all_files() const;
std::shared_ptr<const File> get(const std::string& filename) const;
private:
std::vector<std::shared_ptr<const File>> files_by_patch_order;
std::unordered_map<std::string, std::shared_ptr<const File>> files_by_name;
std::string root_dir;
};
@@ -43,6 +44,6 @@ struct PatchFileChecksumRequest {
inline bool needs_update() const {
return !this->response_received ||
(this->crc32 != this->file->crc32) ||
(this->size != this->file->size);
(this->size != this->file->data->size());
}
};
+20 -6
View File
@@ -7,14 +7,28 @@ using namespace std;
RareItemSet::RareItemSet(const char* filename, uint8_t episode,
uint8_t difficulty, uint8_t secid) {
scoped_fd fd(filename, O_RDONLY);
size_t offset = (episode * 0x6400) + (difficulty * 0x1900) + (secid * 0x0280);
preadx(fd, this, sizeof(*this), offset);
RareItemSet::RareItemSet(shared_ptr<const string> data) : data(data) {
if (this->data->size() != sizeof(Table) * 10 * 4 * 3) {
throw runtime_error("data file size is incorrect");
}
this->tables = reinterpret_cast<const Table*>(this->data->data());
}
bool sample_rare_item(mt19937& random, uint8_t pc) {
const RareItemSet::Table& RareItemSet::get_table(
uint8_t episode, uint8_t difficulty, uint8_t secid) const {
if (episode > 2) {
throw logic_error("incorrect episode number");
}
if (difficulty > 3) {
throw logic_error("incorrect difficulty");
}
if (secid > 10) {
throw logic_error("incorrect section id");
}
return this->tables[(episode * 10 * 4) + (difficulty * 10) + secid];
}
bool RareItemSet::sample(mt19937& random, uint8_t pc) {
int8_t shift = ((pc >> 3) & 0x1F) - 4;
if (shift < 0) {
shift = 0;
+25 -18
View File
@@ -6,24 +6,31 @@
struct RareItemDrop {
uint8_t probability;
uint8_t item_code[3];
} __attribute__((packed));
class RareItemSet {
public:
struct Table {
// TODO: It looks like this structure can actually vary. We see the offsets
// 0194 and 01B2 in the unused section, along with the value 1E (number of
// box rares). In PSOGC, these all appear to be the same size/format, but
// that's probably not strictly required to be the case.
// 0x280 in size; describes one difficulty, section ID, and episode
struct Drop {
uint8_t probability;
uint8_t item_code[3];
} __attribute__((packed));
Drop monster_rares[0x65]; // 0000 - 0194 in file
uint8_t box_areas[0x1E]; // 0194 - 01B2 in file
Drop box_rares[0x1E]; // 01B2 - 022A in file
uint8_t unused[0x56];
} __attribute__((packed));
struct RareItemSet {
// TODO: It looks like this structure can actually vary. We see the offsets
// 0194 and 01B2 in the unused section, along with the value 1E (number of box
// rares). In PSOGC, these all appear to be the same size/format, but that's
// probably not strictly required to be the case.
// 0x280 in size; describes one difficulty, section ID, and episode
RareItemDrop rares[0x65]; // 0000 - 0194 in file
uint8_t box_areas[0x1E]; // 0194 - 01B2 in file
RareItemDrop box_rares[0x1E]; // 01B2 - 022A in file
uint8_t unused[0x56];
RareItemSet(std::shared_ptr<const std::string> data);
RareItemSet(const char* filename, uint8_t episode, uint8_t difficulty,
uint8_t secid);
} __attribute__((packed));
const Table& get_table(uint8_t episode, uint8_t difficulty, uint8_t secid) const;
bool sample_rare_item(std::mt19937& rand, uint8_t pc);
static bool sample(std::mt19937& rand, uint8_t probability);
private:
std::shared_ptr<const std::string> data;
const Table* tables;
};
+54 -46
View File
@@ -2185,62 +2185,66 @@ static shared_ptr<Lobby> create_game_generic(shared_ptr<ServerState> s,
game->min_level = min_level;
game->max_level = 0xFFFFFFFF;
if (game->version == GameVersion::BB) {
// TODO: Cache these somewhere so we don't read the file every time
game->rare_item_set.reset(new RareItemSet("system/blueburst/ItemRT.rel",
game->episode - 1, game->difficulty, game->section_id));
bool is_solo = (game->flags & Lobby::Flag::SOLO_MODE);
// Generate the map variations
if (is_ep3) {
game->variations.clear(0);
} else {
generate_variations(game->variations, game->random, game->episode, is_solo);
}
if (game->version == GameVersion::BB) {
for (size_t x = 0; x < 4; x++) {
game->next_item_id[x] = (0x00200000 * x) + 0x00010000;
}
game->next_game_item_id = 0x00810000;
bool is_solo = (game->flags & Lobby::Flag::SOLO_MODE);
auto bp_subtable = s->battle_params->get_subtable(
is_solo, game->episode - 1, game->difficulty);
for (size_t area = 0; area < 0x10; area++) {
auto filenames = map_filenames_for_variation(
game->episode,
is_solo,
area,
game->variations[area * 2],
game->variations[area * 2 + 1]);
generate_variations(game->variations, game->random, game->episode, is_solo);
for (size_t x = 0; x < 0x10; x++) {
try {
auto file = map_data_for_variation(
game->episode,
is_solo,
x,
game->variations[x * 2 + 0],
game->variations[x * 2 + 1]);
auto area_enemies = parse_map(
game->episode,
game->difficulty,
bp_subtable,
file->data.data(),
file->data.size(),
false);
game->enemies.insert(
game->enemies.end(),
area_enemies.begin(),
area_enemies.end());
c->log.info("Loaded map for area %zu (%zu entries)", x, area_enemies.size());
for (size_t z = 0; z < area_enemies.size(); z++) {
string e_str = area_enemies[z].str();
static_game_data_log.info("(Entry %zu) %s", z, e_str.c_str());
if (filenames.empty()) {
c->log.info("[Map/%zu] No file to load", area);
continue;
}
bool any_map_loaded = false;
for (const string& filename : filenames) {
try {
auto map_data = s->load_bb_file(filename, "", "map/" + filename);
std::vector<PSOEnemy> area_enemies = parse_map(
s->battle_params,
is_solo,
game->episode,
game->difficulty,
map_data,
false);
game->enemies.insert(
game->enemies.end(),
area_enemies.begin(),
area_enemies.end());
c->log.info("[Map/%zu] Loaded %s (%zu entries)",
area, filename.c_str(), area_enemies.size());
for (size_t z = 0; z < area_enemies.size(); z++) {
string e_str = area_enemies[z].str();
static_game_data_log.info("(Entry %zX) %s", z, e_str.c_str());
}
any_map_loaded = true;
break;
} catch (const exception& e) {
c->log.info("[Map/%zu] Failed to load %s: %s", area, filename.c_str(), e.what());
}
} catch (const exception& e) {
c->log.warning("Failed to load map for area %zu: %s", x, e.what());
}
if (!any_map_loaded) {
throw runtime_error(string_printf("no maps loaded for area %zu", area));
}
}
if (game->enemies.empty()) {
throw runtime_error("failed to load any map data");
}
c->log.info("Loaded maps contain %zu entries overall", game->enemies.size());
} else if (is_ep3) {
game->variations.clear(0);
} else {
// In non-BB non-Ep3 games, just set the variations (we don't track enemies)
generate_variations(game->variations, game->random, game->episode, false);
}
s->change_client_lobby(c, game);
@@ -2547,7 +2551,7 @@ static void on_login_patch(shared_ptr<ServerState> s, shared_ptr<Client> c,
send_command(c, 0x0B, 0x00); // Start patch session; go to root directory
vector<string> path_directories;
for (const auto& file : index->files) {
for (const auto& file : index->all_files()) {
change_to_directory_patch(c, path_directories, file->path_directories);
S_FileChecksumRequest_Patch_0C req = {
@@ -2590,8 +2594,12 @@ static void on_checksums_done_patch(shared_ptr<ServerState>,
throw runtime_error("client did not respond to checksum request");
}
if (req.needs_update()) {
start_cmd.total_bytes += req.file->size;
c->log.info("File %s needs update (CRC: %08" PRIX32 "/%08" PRIX32 ", size: %zu/%" PRIu32 ")",
req.file->name.c_str(), req.file->crc32, req.crc32, req.file->data->size(), req.size);
start_cmd.total_bytes += req.file->data->size();
start_cmd.num_files++;
} else {
c->log.info("File %s is up to date", req.file->name.c_str());
}
}
+20 -19
View File
@@ -684,6 +684,7 @@ static void on_subcommand_sort_inventory_bb(shared_ptr<ServerState>,
// EXP/Drop Item commands
static bool drop_item(
std::shared_ptr<ServerState> s,
std::shared_ptr<Lobby> l,
int64_t enemy_id,
uint8_t area,
@@ -704,35 +705,35 @@ static bool drop_item(
throw runtime_error("received box drop subcommand without item creator present");
}
const RareItemDrop* rare_drop = nullptr;
if (l->rare_item_set) {
const RareItemSet::Table::Drop* drop = nullptr;
if (s->rare_item_set) {
const auto& table = s->rare_item_set->get_table(
l->episode - 1, l->difficulty, l->section_id);
if (enemy_id < 0) {
for (size_t z = 0; z < 30; z++) {
if (l->rare_item_set->box_areas[z] != area) {
if (table.box_areas[z] != area) {
continue;
}
if (sample_rare_item(
*l->random, l->rare_item_set->box_rares[z].probability)) {
rare_drop = &l->rare_item_set->box_rares[z];
if (RareItemSet::sample(*l->random, table.box_rares[z].probability)) {
drop = &table.box_rares[z];
break;
}
}
} else {
if ((enemy_id <= 0x65) &&
sample_rare_item(
*l->random, l->rare_item_set->rares[enemy_id].probability)) {
rare_drop = &l->rare_item_set->rares[enemy_id];
RareItemSet::sample(*l->random, table.monster_rares[enemy_id].probability)) {
drop = &table.monster_rares[enemy_id];
}
}
}
if (rare_drop) {
item.data.data1[0] = rare_drop->item_code[0];
item.data.data1[1] = rare_drop->item_code[1];
item.data.data1[2] = rare_drop->item_code[2];
// TODO: Add random percentages
if (drop) {
item.data.data1[0] = drop->item_code[0];
item.data.data1[1] = drop->item_code[1];
item.data.data1[2] = drop->item_code[2];
// TODO: Add random percentages / modifiers
if (item.data.data1d[0] == 0) {
item.data.data1[4] |= 0x80; // make it unidentified if it's a weapon
item.data.data1[4] |= 0x80; // Make it unidentified if it's a weapon
}
} else {
try {
@@ -759,7 +760,7 @@ static bool drop_item(
return true;
}
static void on_subcommand_enemy_drop_item_request(shared_ptr<ServerState>,
static void on_subcommand_enemy_drop_item_request(shared_ptr<ServerState> s,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
if (!l->is_game()) {
@@ -769,12 +770,12 @@ static void on_subcommand_enemy_drop_item_request(shared_ptr<ServerState>,
const auto* cmd = check_size_sc<G_EnemyDropItemRequest_DC_6x60>(data,
sizeof(G_EnemyDropItemRequest_DC_6x60),
sizeof(G_EnemyDropItemRequest_PC_V3_BB_6x60));
if (!drop_item(l, cmd->enemy_id, cmd->area, cmd->x, cmd->z, cmd->request_id)) {
if (!drop_item(s, l, cmd->enemy_id, cmd->area, cmd->x, cmd->z, cmd->request_id)) {
forward_subcommand(l, c, command, flag, data);
}
}
static void on_subcommand_box_drop_item_request(shared_ptr<ServerState>,
static void on_subcommand_box_drop_item_request(shared_ptr<ServerState> s,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
if (!l->is_game()) {
@@ -782,7 +783,7 @@ static void on_subcommand_box_drop_item_request(shared_ptr<ServerState>,
}
const auto* cmd = check_size_sc<G_BoxItemDropRequest_6xA2>(data);
if (!drop_item(l, -1, cmd->area, cmd->x, cmd->z, cmd->request_id)) {
if (!drop_item(s, l, -1, cmd->area, cmd->x, cmd->z, cmd->request_id)) {
forward_subcommand(l, c, command, flag, data);
}
}
+8 -6
View File
@@ -466,20 +466,22 @@ void send_enter_directory_patch(shared_ptr<Client> c, const string& dir) {
}
void send_patch_file(shared_ptr<Client> c, shared_ptr<const PatchFileIndex::File> f) {
S_OpenFile_Patch_06 open_cmd = {0, f->size, f->name};
S_OpenFile_Patch_06 open_cmd = {0, f->data->size(), f->name};
send_command_t(c, 0x06, 0x00, open_cmd);
for (size_t x = 0; x < f->chunks.size(); x++) {
const auto& chunk = f->chunks[x];
for (size_t x = 0; x < f->chunk_crcs.size(); x++) {
// TODO: The use of StringWriter here is... unfortunate. Write a version of
// Channel::send that takes iovecs or something to avoid these dumb massive
// string copies.
StringWriter w;
size_t chunk_size = min<uint32_t>(f->data->size() - x * 0x4000, 0x4000);
S_WriteFileHeader_Patch_07 write_cmd_header = {
x, chunk.crc32, chunk.data.size()};
x, f->chunk_crcs[x], chunk_size};
w.put(write_cmd_header);
w.write(chunk.data);
w.write(f->data->data() + x * 0x4000, chunk_size);
while (w.size() & 7) {
w.put_u8(0);
}
send_command(c, 0x07, 0x00, w.str());
}
+1 -7
View File
@@ -61,7 +61,7 @@ Server commands:\n\
exit (or ctrl+d)\n\
Shut down the server.\n\
reload <item> ...\n\
Reload data. <item> can be licenses, battle-params, level-table, or quests.\n\
Reload data. <item> can be licenses or quests.\n\
Reloading will not affect items that are in use; for example, if a client\'s\n\
license is deleted by reloading, they will not be disconnected immediately.\n\
add-license <parameters>\n\
@@ -162,12 +162,6 @@ Proxy commands (these will only work when exactly one client is connected):\n\
if (type == "licenses") {
shared_ptr<LicenseManager> lm(new LicenseManager("system/licenses.nsi"));
this->state->license_manager = lm;
} else if (type == "battle-params") {
shared_ptr<BattleParamsIndex> bpt(new BattleParamsIndex("system/blueburst/BattleParamEntry"));
this->state->battle_params = bpt;
} else if (type == "level-table") {
shared_ptr<LevelTable> lt(new LevelTable("system/blueburst/PlyLevelTbl.prs", true));
this->state->level_table = lt;
} else if (type == "quests") {
shared_ptr<QuestIndex> qi(new QuestIndex("system/quests"));
this->state->quest_index = qi;
+63 -1
View File
@@ -5,8 +5,9 @@
#include <memory>
#include <phosg/Network.hh>
#include "Loggers.hh"
#include "FileContentsCache.hh"
#include "IPStackSimulator.hh"
#include "Loggers.hh"
#include "NetworkAddresses.hh"
#include "SendCommands.hh"
#include "Text.hh"
@@ -423,3 +424,64 @@ void ServerState::create_menus(shared_ptr<const JSONObject> config_json) {
this->bb_patch_server_message = decode_sjis(d.at("BBPatchServerMessage")->as_string());
} catch (const out_of_range&) { }
}
shared_ptr<const string> ServerState::load_bb_file(
const std::string& patch_index_filename,
const std::string& gsl_filename,
const std::string& bb_directory_filename) const {
if (this->bb_patch_file_index) {
// First, look in the patch tree's data directory
string patch_index_path = "./data/" + patch_index_filename;
try {
auto ret = this->bb_patch_file_index->get(patch_index_path)->data;
static_game_data_log.info("Loaded %s from file in BB patch tree", patch_index_path.c_str());
return ret;
} catch (const out_of_range&) {
static_game_data_log.info("%s missing from BB patch tree", patch_index_path.c_str());
}
}
if (this->bb_data_gsl) {
// Second, look in the patch tree's data.gsl file
const string& effective_gsl_filename = gsl_filename.empty() ? patch_index_filename : gsl_filename;
try {
// TODO: It's kinda not great that we copy the data here; find a way to
// avoid doing this (also in the below case)
shared_ptr<string> ret(new string(this->bb_data_gsl->get_copy(effective_gsl_filename)));
static_game_data_log.info("Loaded %s from data.gsl in BB patch tree", effective_gsl_filename.c_str());
return ret;
} catch (const out_of_range&) {
static_game_data_log.info("%s missing from data.gsl in BB patch tree", effective_gsl_filename.c_str());
}
// Third, look in data.gsl without the filename extension
size_t dot_offset = effective_gsl_filename.rfind('.');
if (dot_offset != string::npos) {
string no_ext_gsl_filename = effective_gsl_filename.substr(0, dot_offset);
try {
shared_ptr<string> ret(new string(this->bb_data_gsl->get_copy(no_ext_gsl_filename)));
static_game_data_log.info("Loaded %s from data.gsl in BB patch tree", no_ext_gsl_filename.c_str());
return ret;
} catch (const out_of_range&) {
static_game_data_log.info("%s missing from data.gsl in BB patch tree", no_ext_gsl_filename.c_str());
}
}
}
// Finally, look in system/blueburst
const string& effective_bb_directory_filename = bb_directory_filename.empty() ? patch_index_filename : bb_directory_filename;
static FileContentsCache cache(60 * 60 * 1000 * 1000); // 1 hour
try {
auto ret = cache.get_or_load("system/blueburst/" + effective_bb_directory_filename);
static_game_data_log.info("Loaded %s", effective_bb_directory_filename.c_str());
// TODO: It's also not great that we copy the data here... sigh
return shared_ptr<string>(new string(ret.file->data));
} catch (const exception& e) {
static_game_data_log.info("%s missing from system/blueburst", effective_bb_directory_filename.c_str());
static_game_data_log.error("%s not found in any source", patch_index_filename.c_str());
throw cannot_open_file(patch_index_filename);
}
}
+8
View File
@@ -11,6 +11,7 @@
#include "Client.hh"
#include "FunctionCompiler.hh"
#include "GSLArchive.hh"
#include "Items.hh"
#include "LevelTable.hh"
#include "License.hh"
@@ -59,6 +60,8 @@ struct ServerState {
std::shared_ptr<const LevelTable> level_table;
std::shared_ptr<const BattleParamsIndex> battle_params;
std::shared_ptr<const CommonItemData> common_item_data;
std::shared_ptr<const GSLArchive> bb_data_gsl;
std::shared_ptr<const RareItemSet> rare_item_set;
std::shared_ptr<LicenseManager> license_manager;
@@ -128,4 +131,9 @@ struct ServerState {
const std::vector<PortConfiguration>& port_configs);
void create_menus(std::shared_ptr<const JSONObject> config_json);
std::shared_ptr<const std::string> load_bb_file(
const std::string& patch_index_filename,
const std::string& gsl_filename = "",
const std::string& bb_directory_filename = "") const;
};
-204
View File
@@ -8,210 +8,6 @@ using namespace std;
struct AreaMapFileIndex {
const char* name_token;
std::vector<uint32_t> variation1_values;
std::vector<uint32_t> variation2_values;
AreaMapFileIndex(
const char* name_token,
std::vector<uint32_t> variation1_values,
std::vector<uint32_t> variation2_values)
: name_token(name_token),
variation1_values(variation1_values),
variation2_values(variation2_values) { }
};
// These are indexed as [episode][is_solo][area]
// (Note that Lobby::episode is 1-3, so we actually use episode - 1)
static const std::vector<std::vector<std::vector<AreaMapFileIndex>>> map_file_info = {
{ // Episode 1
{ // Non-solo
{"city00", {}, {0}},
{"forest01", {}, {0, 1, 2, 3, 4}},
{"forest02", {}, {0, 1, 2, 3, 4}},
{"cave01", {0, 1, 2}, {0, 1}},
{"cave02", {0, 1, 2}, {0, 1}},
{"cave03", {0, 1, 2}, {0, 1}},
{"machine01", {0, 1, 2}, {0, 1}},
{"machine02", {0, 1, 2}, {0, 1}},
{"ancient01", {0, 1, 2}, {0, 1}},
{"ancient02", {0, 1, 2}, {0, 1}},
{"ancient03", {0, 1, 2}, {0, 1}},
{"boss01", {}, {}},
{"boss02", {}, {}},
{"boss03", {}, {}},
{"boss04", {}, {}},
{nullptr, {}, {}},
},
{ // Solo
{"city00", {}, {0}},
{"forest01", {}, {0, 2, 4}},
{"forest02", {}, {0, 3, 4}},
{"cave01", {0, 1, 2}, {0}},
{"cave02", {0, 1, 2}, {0}},
{"cave03", {0, 1, 2}, {0}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
},
},
{ // Episode 2
{ // Non-solo
{"labo00", {}, {0}},
{"ruins01", {0, 1}, {0}},
{"ruins02", {0, 1}, {0}},
{"space01", {0, 1}, {0}},
{"space02", {0, 1}, {0}},
{"jungle01", {}, {0, 1, 2}},
{"jungle02", {}, {0, 1, 2}},
{"jungle03", {}, {0, 1, 2}},
{"jungle04", {0, 1}, {0, 1}},
{"jungle05", {}, {0, 1, 2}},
{"seabed01", {0, 1}, {0, 1}},
{"seabed02", {0, 1}, {0, 1}},
{"boss05", {}, {}},
{"boss06", {}, {}},
{"boss07", {}, {}},
{"boss08", {}, {}},
},
{ // Solo
{"labo00", {}, {0}},
{"ruins01", {0, 1}, {0}},
{"ruins02", {0, 1}, {0}},
{"space01", {0, 1}, {0}},
{"space02", {0, 1}, {0}},
{"jungle01", {}, {0, 1, 2}},
{"jungle02", {}, {0, 1, 2}},
{"jungle03", {}, {0, 1, 2}},
{"jungle04", {0, 1}, {0, 1}},
{"jungle05", {}, {0, 1, 2}},
{"seabed01", {0, 1}, {0}},
{"seabed02", {0, 1}, {0}},
{"boss05", {}, {}},
{"boss06", {}, {}},
{"boss07", {}, {}},
{"boss08", {}, {}},
},
},
{ // Episode 4
{ // Non-solo
{"city02", {0}, {0}},
{"wilds01", {0}, {0, 1, 2}},
{"wilds01", {1}, {0, 1, 2}},
{"wilds01", {2}, {0, 1, 2}},
{"wilds01", {3}, {0, 1, 2}},
{"crater01", {0}, {0, 1, 2}},
{"desert01", {0, 1, 2}, {0}},
{"desert02", {0}, {0, 1, 2}},
{"desert03", {0, 1, 2}, {0}},
{"boss09", {0}, {0}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
},
{ // Solo
{"city02", {0}, {0}},
{"wilds01", {0}, {0, 1, 2}},
{"wilds01", {1}, {0, 1, 2}},
{"wilds01", {2}, {0, 1, 2}},
{"wilds01", {3}, {0, 1, 2}},
{"crater01", {0}, {0, 1, 2}},
{"desert01", {0, 1, 2}, {0}},
{"desert02", {0}, {0, 1, 2}},
{"desert03", {0, 1, 2}, {0}},
{"boss09", {0}, {0}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
{nullptr, {}, {}},
},
},
};
void generate_variations(
parray<le_uint32_t, 0x20>& variations,
std::shared_ptr<std::mt19937> random,
uint8_t episode,
bool is_solo) {
const auto& ep_index = map_file_info.at(episode - 1);
for (size_t z = 0; z < 0x10; z++) {
const AreaMapFileIndex* a = nullptr;
if (is_solo) {
a = &ep_index.at(true).at(z);
}
if (!a || !a->name_token) {
a = &ep_index.at(false).at(z);
}
if (!a->name_token) {
variations[z * 2 + 0] = 0;
variations[z * 2 + 1] = 0;
} else {
variations[z * 2 + 0] = (a->variation1_values.size() < 2) ? 0 :
((*random)() % a->variation1_values.size());
variations[z * 2 + 1] = (a->variation2_values.size() < 2) ? 0 :
((*random)() % a->variation2_values.size());
}
}
}
std::shared_ptr<const FileContentsCache::File> map_data_for_variation(
uint8_t episode, bool is_solo, uint8_t area, uint32_t var1, uint32_t var2) {
static FileContentsCache cache(300 * 1000 * 1000);
// Map filenames are like map_<name_token>[_VV][_VV][_off]<e|o>[_s].dat
// name_token comes from AreaMapFileIndex
// _VV are the values from the variation<1|2>_values vector (in contrast to
// the values sent in the 64 command, which are INDEXES INTO THAT VECTOR)
// _off or _s are used for solo mode (try both - city uses _s whereas levels
// use _off apparently)
// e|o specifies what kind of data: e = enemies, o = objects
const auto& ep_index = map_file_info.at(episode - 1);
const AreaMapFileIndex* a = nullptr;
if (is_solo) {
a = &ep_index.at(true).at(area);
}
if (!a || !a->name_token) {
a = &ep_index.at(false).at(area);
}
if (!a->name_token) {
throw out_of_range("no map data for area");
}
string filename = "system/blueburst/map/map_";
filename += a->name_token;
if (!a->variation1_values.empty()) {
filename += string_printf("_%02" PRIX32, a->variation1_values.at(var1));
}
if (!a->variation2_values.empty()) {
filename += string_printf("_%02" PRIX32, a->variation2_values.at(var2));
}
if (is_solo) {
// Try both _offe.dat and e_s.dat suffixes
try {
return cache.get_or_load(filename + "_offe.dat").file;
} catch (const cannot_open_file&) {
return cache.get_or_load(filename + "e_s.dat").file;
}
} else {
return cache.get_or_load(filename + "e.dat").file;
}
}
const vector<string> section_id_to_name({
"Viridia", "Greennill", "Skyly", "Bluefull", "Purplenum", "Pinkal", "Redria",
"Oran", "Yellowboze", "Whitill"});
-10
View File
@@ -3,22 +3,12 @@
#include <stdint.h>
#include <unordered_map>
#include <random>
#include "FileContentsCache.hh"
#include "Player.hh"
void generate_variations(
parray<le_uint32_t, 0x20>& variations,
std::shared_ptr<std::mt19937> random,
uint8_t episode,
bool is_solo);
std::shared_ptr<const FileContentsCache::File> map_data_for_variation(
uint8_t episode, bool is_solo, uint8_t area, uint32_t var1, uint32_t var2);
void load_map_files();
size_t stack_size_for_item(uint8_t data0, uint8_t data1);
size_t stack_size_for_item(const ItemData& item);
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-105
View File
@@ -1,108 +1,3 @@
newserv> I 58384 2022-08-08 23:38:14 - [Server] Client connected: C-1 on fd 32 via 5 (T-11000-Patch-bb-patch-patch_server_bb)
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=02 flag=00)
0000 | 4C 00 02 00 50 61 74 63 68 20 53 65 72 76 65 72 | L Patch Server
0010 | 2E 20 43 6F 70 79 72 69 67 68 74 20 53 6F 6E 69 | . Copyright Soni
0020 | 63 54 65 61 6D 2C 20 4C 54 44 2E 20 32 30 30 31 | cTeam, LTD. 2001
0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0040 | 00 00 00 00 77 73 35 8D 0E E2 9C 02 | ws5
I 58384 2022-08-08 23:38:14 - [Commands] Received from C-1 (version=Patch command=02 flag=00)
0000 | 04 00 02 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=04 flag=00)
0000 | 04 00 04 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Received from C-1 (version=Patch command=04 flag=00)
0000 | 70 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 | p
0010 | 74 65 73 74 2D 75 73 65 72 00 00 00 00 00 00 00 | test-user
0020 | 70 61 73 73 77 6F 72 64 00 00 00 00 00 00 00 00 | password
0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0040 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0050 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0060 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=13 flag=00)
0000 | 70 01 13 00 09 00 43 00 37 00 6E 00 65 00 77 00 | p C 7 n e w
0010 | 73 00 65 00 72 00 76 00 20 00 70 00 61 00 74 00 | s e r v p a t
0020 | 63 00 68 00 20 00 73 00 65 00 72 00 76 00 65 00 | c h s e r v e
0030 | 72 00 0A 00 0A 00 54 00 68 00 69 00 73 00 20 00 | r T h i s
0040 | 73 00 65 00 72 00 76 00 65 00 72 00 20 00 69 00 | s e r v e r i
0050 | 73 00 20 00 6E 00 6F 00 74 00 20 00 61 00 66 00 | s n o t a f
0060 | 66 00 69 00 6C 00 69 00 61 00 74 00 65 00 64 00 | f i l i a t e d
0070 | 20 00 77 00 69 00 74 00 68 00 2C 00 20 00 73 00 | w i t h , s
0080 | 70 00 6F 00 6E 00 73 00 6F 00 72 00 65 00 64 00 | p o n s o r e d
0090 | 20 00 62 00 79 00 2C 00 20 00 6F 00 72 00 20 00 | b y , o r
00A0 | 69 00 6E 00 20 00 61 00 6E 00 79 00 0A 00 6F 00 | i n a n y o
00B0 | 74 00 68 00 65 00 72 00 20 00 77 00 61 00 79 00 | t h e r w a y
00C0 | 20 00 63 00 6F 00 6E 00 6E 00 65 00 63 00 74 00 | c o n n e c t
00D0 | 65 00 64 00 20 00 74 00 6F 00 20 00 53 00 45 00 | e d t o S E
00E0 | 47 00 41 00 20 00 6F 00 72 00 20 00 53 00 6F 00 | G A o r S o
00F0 | 6E 00 69 00 63 00 20 00 54 00 65 00 61 00 6D 00 | n i c T e a m
0100 | 2C 00 20 00 61 00 6E 00 64 00 20 00 69 00 73 00 | , a n d i s
0110 | 20 00 6F 00 77 00 6E 00 65 00 64 00 0A 00 61 00 | o w n e d a
0120 | 6E 00 64 00 20 00 6F 00 70 00 65 00 72 00 61 00 | n d o p e r a
0130 | 74 00 65 00 64 00 20 00 63 00 6F 00 6D 00 70 00 | t e d c o m p
0140 | 6C 00 65 00 74 00 65 00 6C 00 79 00 20 00 69 00 | l e t e l y i
0150 | 6E 00 64 00 65 00 70 00 65 00 6E 00 64 00 65 00 | n d e p e n d e
0160 | 6E 00 74 00 6C 00 79 00 2E 00 00 00 00 00 00 00 | n t l y .
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=0B flag=00)
0000 | 04 00 0B 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=09 flag=00)
0000 | 44 00 09 00 2E 00 00 00 00 00 00 00 00 00 00 00 | D .
0010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0040 | 00 00 00 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=09 flag=00)
0000 | 44 00 09 00 64 61 74 61 00 00 00 00 00 00 00 00 | D data
0010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0040 | 00 00 00 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=0C flag=00)
0000 | 28 00 0C 00 00 00 00 00 6E 65 77 73 65 72 76 2D | ( newserv-
0010 | 74 65 73 74 2D 62 62 2E 74 78 74 00 00 00 00 00 | test-bb.txt
0020 | 00 00 00 00 00 00 00 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=0A flag=00)
0000 | 04 00 0A 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=0A flag=00)
0000 | 04 00 0A 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=0D flag=00)
0000 | 04 00 0D 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Received from C-1 (version=Patch command=0F flag=00)
0000 | 10 00 0F 00 00 00 00 00 00 00 00 00 00 00 00 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Received from C-1 (version=Patch command=10 flag=00)
0000 | 04 00 10 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=11 flag=00)
0000 | 0C 00 11 00 33 00 00 00 01 00 00 00 | 3
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=09 flag=00)
0000 | 44 00 09 00 2E 00 00 00 00 00 00 00 00 00 00 00 | D .
0010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0040 | 00 00 00 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=09 flag=00)
0000 | 44 00 09 00 64 61 74 61 00 00 00 00 00 00 00 00 | D data
0010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0040 | 00 00 00 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=06 flag=00)
0000 | 3C 00 06 00 00 00 00 00 33 00 00 00 6E 65 77 73 | < 3 news
0010 | 65 72 76 2D 74 65 73 74 2D 62 62 2E 74 78 74 00 | erv-test-bb.txt
0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0030 | 00 00 00 00 00 00 00 00 00 00 00 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=07 flag=00)
0000 | 44 00 07 00 00 00 00 00 00 00 00 00 33 00 00 00 | D 3
0010 | 54 68 69 73 20 66 69 6C 65 20 65 78 69 73 74 73 | This file exists
0020 | 20 74 6F 20 74 65 73 74 20 74 68 65 20 70 61 74 | to test the pat
0030 | 63 68 20 64 6F 77 6E 6C 6F 61 64 20 73 79 73 74 | ch download syst
0040 | 65 6D 2E 00 | em.
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=08 flag=00)
0000 | 08 00 08 00 00 00 00 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=0A flag=00)
0000 | 04 00 0A 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=0A flag=00)
0000 | 04 00 0A 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=12 flag=00)
0000 | 04 00 12 00 |
I 58384 2022-08-08 23:38:14 - [Server] Client disconnected: C-1 on fd 32
I 80350 2022-07-07 23:26:22 - [Server] Client connected: C-3 on fd 29 via 26 (T-13000-BB-bb-init2-data_server_bb)
I 80350 2022-07-07 23:26:22 - [Commands] Sending to C-3 (version=BB command=0003 flag=00000000)
0000 | 88 01 03 00 00 00 00 00 50 68 61 6E 74 61 73 79 | Phantasy
+1 -1
View File
@@ -89,7 +89,7 @@ I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=0
0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0030 | 00 00 00 00 00 00 00 00 00 00 00 00 |
I 58384 2022-08-08 23:38:14 - [Commands] Sending to C-1 (version=Patch command=07 flag=00)
0000 | 44 00 07 00 00 00 00 00 00 00 00 00 33 00 00 00 | D 3
0000 | 44 00 07 00 00 00 00 00 B0 5F 40 D1 33 00 00 00 | D _@ 3
0010 | 54 68 69 73 20 66 69 6C 65 20 65 78 69 73 74 73 | This file exists
0020 | 20 74 6F 20 74 65 73 74 20 74 68 65 20 70 61 74 | to test the pat
0030 | 63 68 20 64 6F 77 6E 6C 6F 61 64 20 73 79 73 74 | ch download syst