reorganize BB file loading abstractions
This commit is contained in:
@@ -29,3 +29,6 @@ old-khyller
|
||||
old-newserv
|
||||
release
|
||||
release.zip
|
||||
system/patch-bb/data
|
||||
system/patch-bb/psobb.pat
|
||||
system/dol
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
+10
-3
@@ -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));
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
+22
-15
@@ -6,24 +6,31 @@
|
||||
|
||||
|
||||
|
||||
struct RareItemDrop {
|
||||
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));
|
||||
|
||||
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
|
||||
} __attribute__((packed));
|
||||
Drop monster_rares[0x65]; // 0000 - 0194 in file
|
||||
uint8_t box_areas[0x1E]; // 0194 - 01B2 in file
|
||||
RareItemDrop box_rares[0x1E]; // 01B2 - 022A in file
|
||||
Drop box_rares[0x1E]; // 01B2 - 022A in file
|
||||
uint8_t unused[0x56];
|
||||
} __attribute__((packed));
|
||||
|
||||
RareItemSet(const char* filename, uint8_t episode, uint8_t difficulty,
|
||||
uint8_t secid);
|
||||
} __attribute__((packed));
|
||||
RareItemSet(std::shared_ptr<const std::string> data);
|
||||
|
||||
bool sample_rare_item(std::mt19937& rand, uint8_t pc);
|
||||
const Table& get_table(uint8_t episode, uint8_t difficulty, uint8_t secid) const;
|
||||
|
||||
static bool sample(std::mt19937& rand, uint8_t probability);
|
||||
|
||||
private:
|
||||
std::shared_ptr<const std::string> data;
|
||||
const Table* tables;
|
||||
};
|
||||
|
||||
+43
-35
@@ -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);
|
||||
|
||||
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(
|
||||
for (size_t area = 0; area < 0x10; area++) {
|
||||
auto filenames = map_filenames_for_variation(
|
||||
game->episode,
|
||||
is_solo,
|
||||
x,
|
||||
game->variations[x * 2 + 0],
|
||||
game->variations[x * 2 + 1]);
|
||||
auto area_enemies = parse_map(
|
||||
area,
|
||||
game->variations[area * 2],
|
||||
game->variations[area * 2 + 1]);
|
||||
|
||||
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,
|
||||
bp_subtable,
|
||||
file->data.data(),
|
||||
file->data.size(),
|
||||
map_data,
|
||||
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());
|
||||
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 %zu) %s", z, e_str.c_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.warning("Failed to load map for area %zu: %s", x, e.what());
|
||||
c->log.info("[Map/%zu] Failed to load %s: %s", area, filename.c_str(), 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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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"});
|
||||
|
||||
@@ -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.
Executable
BIN
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user