Merge branch 'fuzziqersoftware:master' into master
This commit is contained in:
+2
-2
@@ -123,10 +123,10 @@ set(SOURCES
|
||||
src/ServerState.cc
|
||||
src/Shell.cc
|
||||
src/StaticGameData.cc
|
||||
src/StepGraph.cc
|
||||
src/TeamIndex.cc
|
||||
src/Text.cc
|
||||
src/TextArchive.cc
|
||||
src/UnicodeTextSet.cc
|
||||
src/TextIndex.cc
|
||||
src/Version.cc
|
||||
src/WordSelectTable.cc
|
||||
)
|
||||
|
||||
@@ -286,6 +286,7 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
* `$si` (game server only): Shows basic information about the server.
|
||||
* `$ping`: Shows round-trip ping time from the server to you. On the proxy server, shows the ping time from you to the proxy and from the proxy to the server.
|
||||
* `$matcount` (game server only): Shows how many of each type of material you've used.
|
||||
* `$rarenotifs` (game server only): Enables or disables rare drop notifications. When enabled, you'll see a message whenever a rare item drops. In private drop mode, you will only see a notification if the item is visible to you; you won't be notified of other players' rare drops.
|
||||
* `$what` (game server only): Shows the type, name, and stats of the nearest item on the ground.
|
||||
* `$where` (game server only): Shows your current floor number and coordinates. Mainly useful for debugging.
|
||||
|
||||
|
||||
+10
-3
@@ -295,7 +295,7 @@ static void server_command_quest(shared_ptr<Client> c, const std::string& args)
|
||||
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
auto q = s->quest_index_for_version(effective_version)->get(stoul(args));
|
||||
auto q = s->quest_index(effective_version)->get(stoul(args));
|
||||
if (!q) {
|
||||
send_text_message(c, "$C6Quest not found");
|
||||
} else {
|
||||
@@ -1478,6 +1478,12 @@ static void proxy_command_song(shared_ptr<ProxyServer::LinkedSession> ses, const
|
||||
send_ep3_change_music(ses->client_channel, song);
|
||||
}
|
||||
|
||||
static void server_command_rare_notifs(shared_ptr<Client> c, const std::string&) {
|
||||
c->config.toggle_flag(Client::Flag::RARE_DROP_NOTIFICATIONS_ENABLED);
|
||||
bool enabled = c->config.check_flag(Client::Flag::RARE_DROP_NOTIFICATIONS_ENABLED);
|
||||
send_text_message_printf(c, "$C6Rare notifications\n%s", enabled ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
static void server_command_infinite_hp(shared_ptr<Client> c, const std::string&) {
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
@@ -1611,7 +1617,7 @@ static void server_command_item(shared_ptr<Client> c, const std::string& args) {
|
||||
check_is_game(l, true);
|
||||
check_cheats_enabled(l, c);
|
||||
|
||||
ItemData item = s->item_name_index->parse_item_description(c->version(), args);
|
||||
ItemData item = s->parse_item_description(c->version(), args);
|
||||
item.id = l->generate_item_id(c->lobby_client_id);
|
||||
|
||||
if ((l->drop_mode == Lobby::DropMode::SERVER_PRIVATE) || (l->drop_mode == Lobby::DropMode::SERVER_DUPLICATE)) {
|
||||
@@ -1644,7 +1650,7 @@ static void proxy_command_item(shared_ptr<ProxyServer::LinkedSession> ses, const
|
||||
|
||||
bool set_drop = (!args.empty() && (args[0] == '!'));
|
||||
|
||||
ItemData item = s->item_name_index->parse_item_description(ses->version(), (set_drop ? args.substr(1) : args));
|
||||
ItemData item = s->parse_item_description(ses->version(), (set_drop ? args.substr(1) : args));
|
||||
item.id = random_object<uint32_t>() | 0x80000000;
|
||||
|
||||
if (set_drop) {
|
||||
@@ -1994,6 +2000,7 @@ static const unordered_map<string, ChatCommandDefinition> chat_commands({
|
||||
{"$qsyncall", {server_command_qsyncall, proxy_command_qsyncall}},
|
||||
{"$quest", {server_command_quest, nullptr}},
|
||||
{"$rand", {server_command_rand, proxy_command_rand}},
|
||||
{"$rarenotifs", {server_command_rare_notifs, nullptr}},
|
||||
{"$save", {server_command_save, nullptr}},
|
||||
{"$savechar", {server_command_savechar, nullptr}},
|
||||
{"$saverec", {server_command_saverec, nullptr}},
|
||||
|
||||
@@ -195,6 +195,9 @@ Client::Client(
|
||||
external_bank_character_index(-1),
|
||||
last_play_time_update(0) {
|
||||
this->config.set_flags_for_version(version, -1);
|
||||
if (server->get_state()->default_rare_notifs_enabled) {
|
||||
this->config.set_flag(Flag::RARE_DROP_NOTIFICATIONS_ENABLED);
|
||||
}
|
||||
this->config.specific_version = default_specific_version_for_version(version, -1);
|
||||
|
||||
this->last_switch_enabled_command.header.subcommand = 0;
|
||||
@@ -1021,3 +1024,46 @@ void Client::use_character_bank(int8_t index) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Client::print_inventory(FILE* stream) const {
|
||||
auto p = this->character();
|
||||
shared_ptr<const ItemNameIndex> name_index;
|
||||
try {
|
||||
name_index = this->require_server_state()->item_name_index(this->version());
|
||||
} catch (const runtime_error&) {
|
||||
}
|
||||
fprintf(stream, "[PlayerInventory] Meseta: %" PRIu32 "\n", p->disp.stats.meseta.load());
|
||||
fprintf(stream, "[PlayerInventory] %hhu items\n", p->inventory.num_items);
|
||||
for (size_t x = 0; x < p->inventory.num_items; x++) {
|
||||
const auto& item = p->inventory.items[x];
|
||||
auto hex = item.data.hex();
|
||||
if (name_index) {
|
||||
auto name = name_index->describe_item(item.data);
|
||||
fprintf(stream, "[PlayerInventory] %2zu: [+%08" PRIX32 "] %s (%s)\n", x, item.flags.load(), hex.c_str(), name.c_str());
|
||||
} else {
|
||||
fprintf(stream, "[PlayerInventory] %2zu: [+%08" PRIX32 "] %s\n", x, item.flags.load(), hex.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Client::print_bank(FILE* stream) const {
|
||||
auto p = this->character();
|
||||
shared_ptr<const ItemNameIndex> name_index;
|
||||
try {
|
||||
name_index = this->require_server_state()->item_name_index(this->version());
|
||||
} catch (const runtime_error&) {
|
||||
}
|
||||
fprintf(stream, "[PlayerBank] Meseta: %" PRIu32 "\n", p->bank.meseta.load());
|
||||
fprintf(stream, "[PlayerBank] %" PRIu32 " items\n", p->bank.num_items.load());
|
||||
for (size_t x = 0; x < p->bank.num_items; x++) {
|
||||
const auto& item = p->bank.items[x];
|
||||
const char* present_token = item.present ? "" : " (missing present flag)";
|
||||
auto hex = item.data.hex();
|
||||
if (name_index) {
|
||||
auto name = name_index->describe_item(item.data);
|
||||
fprintf(stream, "[PlayerBank] %3zu: %s (%s) (x%hu)%s\n", x, hex.c_str(), name.c_str(), item.amount.load(), present_token);
|
||||
} else {
|
||||
fprintf(stream, "[PlayerBank] %3zu: %s (x%hu)%s\n", x, hex.c_str(), item.amount.load(), present_token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -68,10 +68,11 @@ public:
|
||||
SHOULD_SEND_ENABLE_SAVE = 0x0004000000000000,
|
||||
SWITCH_ASSIST_ENABLED = 0x0000000100000000,
|
||||
|
||||
// Cheat mode flags
|
||||
// Cheat mode and option flags
|
||||
INFINITE_HP_ENABLED = 0x0000000200000000,
|
||||
INFINITE_TP_ENABLED = 0x0000000400000000,
|
||||
DEBUG_ENABLED = 0x0000000800000000,
|
||||
RARE_DROP_NOTIFICATIONS_ENABLED = 0x0010000000000000,
|
||||
|
||||
// Proxy option flags
|
||||
PROXY_SAVE_FILES = 0x0000001000000000,
|
||||
@@ -349,6 +350,9 @@ public:
|
||||
void use_character_bank(int8_t bb_character_index);
|
||||
void use_default_bank();
|
||||
|
||||
void print_inventory(FILE* stream) const;
|
||||
void print_bank(FILE* stream) const;
|
||||
|
||||
private:
|
||||
// The overlay character data is used in battle and challenge modes, when
|
||||
// character data is temporarily replaced in-game. In other play modes and in
|
||||
|
||||
@@ -4369,13 +4369,13 @@ struct G_SwitchInteraction_6x50 {
|
||||
|
||||
// 6x51: Invalid subcommand
|
||||
|
||||
// 6x52: Toggle counter (shop/bank) interaction (protected on V3/V4)
|
||||
// 6x52: Set animation state (protected on V3/V4)
|
||||
|
||||
struct G_ToggleCounterInteraction_6x52 {
|
||||
struct G_SetAnimationState_6x52 {
|
||||
G_ClientIDHeader header;
|
||||
le_uint16_t unknown_a1 = 0;
|
||||
le_uint16_t animation = 0;
|
||||
le_uint16_t unknown_a2 = 0;
|
||||
le_uint32_t unknown_a3 = 0;
|
||||
le_uint32_t angle = 0;
|
||||
} __packed__;
|
||||
|
||||
// 6x53: Unknown (supported; game only) (protected on V3/V4)
|
||||
|
||||
@@ -764,7 +764,7 @@ string CardDefinition::Effect::str_for_arg(const string& arg) {
|
||||
}
|
||||
}
|
||||
|
||||
string CardDefinition::Effect::str(const char* separator, const TextArchive* text_archive) const {
|
||||
string CardDefinition::Effect::str(const char* separator, const TextSet* text_archive) const {
|
||||
vector<string> tokens;
|
||||
tokens.emplace_back(string_printf("%hhu:", this->effect_num));
|
||||
{
|
||||
@@ -802,7 +802,7 @@ string CardDefinition::Effect::str(const char* separator, const TextArchive* tex
|
||||
const char* name = nullptr;
|
||||
if (this->name_index && text_archive) {
|
||||
try {
|
||||
name = text_archive->get_string(45, this->name_index).c_str();
|
||||
name = text_archive->get(45, this->name_index).c_str();
|
||||
} catch (const exception&) {
|
||||
}
|
||||
}
|
||||
@@ -1061,7 +1061,7 @@ static const char* name_for_assist_ai_param_target(uint8_t target) {
|
||||
}
|
||||
}
|
||||
|
||||
string CardDefinition::str(bool single_line, const TextArchive* text_archive) const {
|
||||
string CardDefinition::str(bool single_line, const TextSet* text_archive) const {
|
||||
string type_str;
|
||||
try {
|
||||
type_str = name_for_card_type(this->type);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
#include "../PlayerSubordinates.hh"
|
||||
#include "../Text.hh"
|
||||
#include "../TextArchive.hh"
|
||||
#include "../TextIndex.hh"
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
@@ -514,7 +514,7 @@ struct CardDefinition {
|
||||
|
||||
bool is_empty() const;
|
||||
static std::string str_for_arg(const std::string& arg);
|
||||
std::string str(const char* separator = ", ", const TextArchive* text_archive = nullptr) const;
|
||||
std::string str(const char* separator = ", ", const TextSet* text_archive = nullptr) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
/* 0000 */ be_uint32_t card_id;
|
||||
@@ -780,7 +780,7 @@ struct CardDefinition {
|
||||
CardClass card_class() const;
|
||||
|
||||
void decode_range();
|
||||
std::string str(bool single_line = true, const TextArchive* text_archive = nullptr) const;
|
||||
std::string str(bool single_line = true, const TextSet* text_archive = nullptr) const;
|
||||
} __attribute__((packed)); // 0x128 bytes in total
|
||||
|
||||
struct CardDefinitionsFooter {
|
||||
|
||||
+85
-82
@@ -3,6 +3,7 @@
|
||||
#include <phosg/Random.hh>
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "../Loggers.hh"
|
||||
#include "../SendCommands.hh"
|
||||
|
||||
using namespace std;
|
||||
@@ -26,6 +27,7 @@ void Server::PresenceEntry::clear() {
|
||||
|
||||
Server::Server(shared_ptr<Lobby> lobby, Options&& options)
|
||||
: lobby(lobby),
|
||||
has_lobby(lobby != nullptr),
|
||||
options(std::move(options)),
|
||||
last_chosen_map(this->options.tournament ? this->options.tournament->get_map() : nullptr),
|
||||
tournament_match_result_sent(false),
|
||||
@@ -64,7 +66,11 @@ Server::Server(shared_ptr<Lobby> lobby, Options&& options)
|
||||
has_done_pb(0),
|
||||
num_6xB4x06_commands_sent(0),
|
||||
prev_num_6xB4x06_commands_sent(0) {
|
||||
new StackLogger(this, lobby->log.prefix + "[Ep3::Server] ", lobby->log.min_level);
|
||||
if (this->has_lobby) {
|
||||
new StackLogger(this, lobby->log.prefix + "[Ep3::Server] ", lobby->log.min_level);
|
||||
} else {
|
||||
new StackLogger(this, "[Ep3::Server] ", lobby_log.min_level);
|
||||
}
|
||||
}
|
||||
|
||||
Server::~Server() noexcept(false) {
|
||||
@@ -187,52 +193,50 @@ int8_t Server::get_winner_team_id() const {
|
||||
return -1; // No team has won (yet)
|
||||
}
|
||||
|
||||
void Server::send(const void* data, size_t size) const {
|
||||
void Server::send(const void* data, size_t size, uint8_t command, bool enable_masking) const {
|
||||
// Note: This function is (obviously) not part of the original implementation.
|
||||
auto l = this->lobby.lock();
|
||||
if (!l) {
|
||||
throw runtime_error("lobby is deleted");
|
||||
}
|
||||
if (this->has_lobby) {
|
||||
auto l = this->lobby.lock();
|
||||
if (!l) {
|
||||
throw runtime_error("lobby is deleted");
|
||||
}
|
||||
|
||||
string masked_data;
|
||||
if (!(this->options.behavior_flags & BehaviorFlag::DISABLE_MASKING)) {
|
||||
if (size >= 8) {
|
||||
string masked_data;
|
||||
if (enable_masking &&
|
||||
!(this->options.behavior_flags & BehaviorFlag::DISABLE_MASKING) &&
|
||||
(size >= 8)) {
|
||||
masked_data.assign(reinterpret_cast<const char*>(data), size);
|
||||
uint8_t mask_key = (random_object<uint32_t>() % 0xFF) + 1;
|
||||
set_mask_for_ep3_game_command(masked_data.data(), masked_data.size(), mask_key);
|
||||
data = masked_data.data();
|
||||
size = masked_data.size();
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Sega's servers sent battle commands with the 60 command. The handlers
|
||||
// for 60, 62, and C9 on the client are identical, so we choose to use C9
|
||||
// instead because it's unique to Episode 3, and therefore seems more
|
||||
// appropriate to convey battle commands.
|
||||
send_command(l, 0xC9, 0x00, data, size);
|
||||
for (auto watcher_l : l->watcher_lobbies) {
|
||||
send_command_if_not_loading(watcher_l, 0xC9, 0x00, data, size);
|
||||
}
|
||||
if (l->battle_record && l->battle_record->writable()) {
|
||||
l->battle_record->add_command(
|
||||
BattleRecord::Event::Type::BATTLE_COMMAND, data, size);
|
||||
// Note: Sega's servers sent battle commands with the 60 command. The handlers
|
||||
// for 60, 62, and C9 on the client are identical, so we choose to use C9
|
||||
// instead because it's unique to Episode 3, and therefore seems more
|
||||
// appropriate to convey battle commands.
|
||||
send_command(l, command, 0x00, data, size);
|
||||
for (auto watcher_l : l->watcher_lobbies) {
|
||||
send_command_if_not_loading(watcher_l, command, 0x00, data, size);
|
||||
}
|
||||
if (l->battle_record && l->battle_record->writable()) {
|
||||
l->battle_record->add_command(BattleRecord::Event::Type::BATTLE_COMMAND, data, size);
|
||||
}
|
||||
|
||||
} else if (this->log().info("Generated command")) {
|
||||
print_data(stderr, data, size);
|
||||
}
|
||||
}
|
||||
|
||||
void Server::send_6xB4x46() const {
|
||||
// Note: This function is not part of the original implementation; it was
|
||||
// factored out from its callsites in this file and the strings were changed.
|
||||
auto l = this->lobby.lock();
|
||||
if (!l) {
|
||||
throw runtime_error("lobby is deleted");
|
||||
}
|
||||
|
||||
G_ServerVersionStrings_GC_Ep3_6xB4x46 cmd46;
|
||||
cmd46.version_signature.encode(VERSION_SIGNATURE, 1);
|
||||
cmd46.date_str1.encode(format_time(this->options.card_index->definitions_mtime() * 1000000), 1);
|
||||
string date_str2 = string_printf(
|
||||
"Lobby:%08" PRIX32 " Random:%08" PRIX32 "+%08" PRIX32,
|
||||
l->lobby_id,
|
||||
"Random:%08" PRIX32 "+%08" PRIX32,
|
||||
this->options.random_crypt->seed(),
|
||||
this->options.random_crypt->absolute_offset());
|
||||
if (this->last_chosen_map) {
|
||||
@@ -2110,17 +2114,15 @@ void Server::handle_CAx1D_start_battle(shared_ptr<Client>, const string& data) {
|
||||
this->battle_in_progress = false;
|
||||
} else {
|
||||
auto l = this->lobby.lock();
|
||||
if (!l) {
|
||||
throw runtime_error("lobby is deleted");
|
||||
}
|
||||
if (l->battle_record) {
|
||||
l->battle_record->set_battle_start_timestamp();
|
||||
}
|
||||
|
||||
// Note: Sega's implementation doesn't set EX results values here; they
|
||||
// did it at game join time instead. We do it here for code simplicity.
|
||||
if (l->ep3_ex_result_values) {
|
||||
this->send(*l->ep3_ex_result_values);
|
||||
if (l) {
|
||||
if (l->battle_record) {
|
||||
l->battle_record->set_battle_start_timestamp();
|
||||
}
|
||||
// Note: Sega's implementation doesn't set EX results values here; they
|
||||
// did it at game join time instead. We do it here for code simplicity.
|
||||
if (l->ep3_ex_result_values) {
|
||||
this->send(*l->ep3_ex_result_values);
|
||||
}
|
||||
}
|
||||
|
||||
this->setup_and_start_battle();
|
||||
@@ -2327,65 +2329,66 @@ void Server::handle_CAx40_map_list_request(shared_ptr<Client> sender_c, const st
|
||||
throw runtime_error("lobby is deleted");
|
||||
}
|
||||
|
||||
const auto& list_data = this->options.map_index->get_compressed_list(l->count_clients(), sender_c->language());
|
||||
size_t num_players = l ? l->count_clients() : 1;
|
||||
uint8_t language = sender_c ? sender_c->language() : 1;
|
||||
const auto& list_data = this->options.map_index->get_compressed_list(num_players, language);
|
||||
|
||||
StringWriter w;
|
||||
uint32_t subcommand_size = (list_data.size() + sizeof(G_MapList_GC_Ep3_6xB6x40) + 3) & (~3);
|
||||
w.put<G_MapList_GC_Ep3_6xB6x40>(
|
||||
G_MapList_GC_Ep3_6xB6x40{{{{0xB6, 0, 0}, subcommand_size}, 0x40, {}}, list_data.size(), 0});
|
||||
w.write(list_data);
|
||||
send_command(l, 0x6C, 0x00, w.str());
|
||||
for (auto watcher_l : l->watcher_lobbies) {
|
||||
send_command_if_not_loading(watcher_l, 0x6C, 0x00, w.str());
|
||||
while (w.size() & 3) {
|
||||
w.put_u8(0);
|
||||
}
|
||||
|
||||
if (l->battle_record && l->battle_record->writable()) {
|
||||
l->battle_record->add_command(
|
||||
BattleRecord::Event::Type::BATTLE_COMMAND, std::move(w.str()));
|
||||
}
|
||||
const auto& out_data = w.str();
|
||||
this->send(out_data.data(), out_data.size(), 0x6C, false);
|
||||
}
|
||||
|
||||
void Server::send_6xB6x41_to_all_clients() const {
|
||||
auto l = this->lobby.lock();
|
||||
if (!l) {
|
||||
throw runtime_error("lobby is deleted");
|
||||
}
|
||||
|
||||
vector<string> map_commands_by_language;
|
||||
auto send_to_client = [&](shared_ptr<Client> c) -> void {
|
||||
if (!c) {
|
||||
return;
|
||||
}
|
||||
if (map_commands_by_language.size() <= c->language()) {
|
||||
map_commands_by_language.resize(c->language() + 1);
|
||||
}
|
||||
if (map_commands_by_language[c->language()].empty()) {
|
||||
map_commands_by_language[c->language()] = this->prepare_6xB6x41_map_definition(
|
||||
this->last_chosen_map, c->language(), (l->base_version == Version::GC_EP3_NTE));
|
||||
}
|
||||
this->log().info("Sending %c version of map %08" PRIX32, char_for_language_code(c->language()), this->last_chosen_map->map_number);
|
||||
send_command(c, 0x6C, 0x00, map_commands_by_language[c->language()]);
|
||||
};
|
||||
for (const auto& c : l->clients) {
|
||||
send_to_client(c);
|
||||
}
|
||||
for (auto watcher_l : l->watcher_lobbies) {
|
||||
for (const auto& c : watcher_l->clients) {
|
||||
if (l) {
|
||||
vector<string> map_commands_by_language;
|
||||
auto send_to_client = [&](shared_ptr<Client> c) -> void {
|
||||
if (!c) {
|
||||
return;
|
||||
}
|
||||
if (map_commands_by_language.size() <= c->language()) {
|
||||
map_commands_by_language.resize(c->language() + 1);
|
||||
}
|
||||
if (map_commands_by_language[c->language()].empty()) {
|
||||
map_commands_by_language[c->language()] = this->prepare_6xB6x41_map_definition(
|
||||
this->last_chosen_map, c->language(), (l->base_version == Version::GC_EP3_NTE));
|
||||
}
|
||||
this->log().info("Sending %c version of map %08" PRIX32, char_for_language_code(c->language()), this->last_chosen_map->map_number);
|
||||
send_command(c, 0x6C, 0x00, map_commands_by_language[c->language()]);
|
||||
};
|
||||
for (const auto& c : l->clients) {
|
||||
send_to_client(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (l->battle_record && l->battle_record->writable()) {
|
||||
// TODO: It's not great that we just pick the first one; ideally we'd put
|
||||
// all of them in the recording and send the appropriate one to the client
|
||||
// in the playback lobby
|
||||
for (string& data : map_commands_by_language) {
|
||||
if (!data.empty()) {
|
||||
l->battle_record->add_command(
|
||||
BattleRecord::Event::Type::BATTLE_COMMAND, std::move(data));
|
||||
break;
|
||||
for (auto watcher_l : l->watcher_lobbies) {
|
||||
for (const auto& c : watcher_l->clients) {
|
||||
send_to_client(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (l->battle_record && l->battle_record->writable()) {
|
||||
// TODO: It's not great that we just pick the first one; ideally we'd put
|
||||
// all of them in the recording and send the appropriate one to the client
|
||||
// in the playback lobby
|
||||
for (string& data : map_commands_by_language) {
|
||||
if (!data.empty()) {
|
||||
l->battle_record->add_command(
|
||||
BattleRecord::Event::Type::BATTLE_COMMAND, std::move(data));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
auto out_data = this->prepare_6xB6x41_map_definition(this->last_chosen_map, 1, false);
|
||||
this->send(out_data.data(), out_data.size(), 0x6C, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ public:
|
||||
int8_t get_winner_team_id() const;
|
||||
|
||||
template <typename T>
|
||||
void send(const T& cmd) const {
|
||||
void send(const T& cmd, uint8_t command = 0xC9, bool enable_masking = true) const {
|
||||
if (cmd.header.size != sizeof(cmd) / 4) {
|
||||
throw std::logic_error("outbound command size field is incorrect");
|
||||
}
|
||||
@@ -109,9 +109,9 @@ public:
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->send(&cmd, cmd.header.size * 4);
|
||||
this->send(&cmd, cmd.header.size * 4, command, enable_masking);
|
||||
}
|
||||
void send(const void* data, size_t size) const;
|
||||
void send(const void* data, size_t size, uint8_t command = 0xC9, bool enable_masking = true) const;
|
||||
void send_commands_for_joining_spectator(Channel& ch) const;
|
||||
|
||||
void force_battle_result(uint8_t surrendered_client_id, bool set_winner);
|
||||
@@ -242,6 +242,7 @@ private:
|
||||
public:
|
||||
// These fields are not part of the original implementation
|
||||
std::weak_ptr<Lobby> lobby;
|
||||
bool has_lobby;
|
||||
Options options;
|
||||
std::shared_ptr<const MapIndex::Map> last_chosen_map;
|
||||
bool tournament_match_result_sent;
|
||||
|
||||
+63
-46
@@ -134,46 +134,49 @@ uint8_t ItemCreator::normalize_area_number(uint8_t area) const {
|
||||
}
|
||||
}
|
||||
|
||||
ItemData ItemCreator::on_box_item_drop(uint16_t entity_id, uint8_t area) {
|
||||
ItemCreator::DropResult ItemCreator::on_box_item_drop(uint16_t entity_id, uint8_t area) {
|
||||
return this->destroyed_boxes.count(entity_id)
|
||||
? ItemData()
|
||||
? DropResult()
|
||||
: this->on_box_item_drop_with_area_norm(this->normalize_area_number(area));
|
||||
}
|
||||
|
||||
ItemData ItemCreator::on_monster_item_drop(uint16_t entity_id, uint32_t enemy_type, uint8_t area) {
|
||||
ItemCreator::DropResult ItemCreator::on_monster_item_drop(uint16_t entity_id, uint32_t enemy_type, uint8_t area) {
|
||||
return this->destroyed_monsters.count(entity_id)
|
||||
? ItemData()
|
||||
? DropResult()
|
||||
: this->on_monster_item_drop_with_area_norm(enemy_type, this->normalize_area_number(area));
|
||||
}
|
||||
|
||||
ItemData ItemCreator::on_box_item_drop_with_area_norm(uint8_t area_norm) {
|
||||
ItemCreator::DropResult ItemCreator::on_box_item_drop_with_area_norm(uint8_t area_norm) {
|
||||
this->log.info("Box drop checks for area_norm %02hhX; random state: %08" PRIX32 " %08" PRIX32,
|
||||
area_norm, this->random_crypt.seed(), this->random_crypt.absolute_offset());
|
||||
ItemData item = this->check_rare_specs_and_create_rare_box_item(area_norm);
|
||||
if (item.empty()) {
|
||||
DropResult res;
|
||||
res.item = this->check_rare_specs_and_create_rare_box_item(area_norm);
|
||||
if (!res.item.empty()) {
|
||||
res.is_from_rare_table = true;
|
||||
} else {
|
||||
uint8_t item_class = this->get_rand_from_weighted_tables_2d_vertical(this->pt->box_item_class_prob_table, area_norm);
|
||||
this->log.info("Item class is %02hhX", item_class);
|
||||
switch (item_class) {
|
||||
case 0: // Weapon
|
||||
item.data1[0] = 0;
|
||||
res.item.data1[0] = 0;
|
||||
break;
|
||||
case 1: // Armor
|
||||
item.data1[0] = 1;
|
||||
item.data1[1] = 1;
|
||||
res.item.data1[0] = 1;
|
||||
res.item.data1[1] = 1;
|
||||
break;
|
||||
case 2: // Shield
|
||||
item.data1[0] = 1;
|
||||
item.data1[1] = 2;
|
||||
res.item.data1[0] = 1;
|
||||
res.item.data1[1] = 2;
|
||||
break;
|
||||
case 3: // Unit
|
||||
item.data1[0] = 1;
|
||||
item.data1[1] = 3;
|
||||
res.item.data1[0] = 1;
|
||||
res.item.data1[1] = 3;
|
||||
break;
|
||||
case 4: // Tool
|
||||
item.data1[0] = 3;
|
||||
res.item.data1[0] = 3;
|
||||
break;
|
||||
case 5: // Meseta
|
||||
item.data1[0] = 4;
|
||||
res.item.data1[0] = 4;
|
||||
break;
|
||||
case 6: // Nothing
|
||||
break;
|
||||
@@ -181,16 +184,16 @@ ItemData ItemCreator::on_box_item_drop_with_area_norm(uint8_t area_norm) {
|
||||
throw logic_error("this should be impossible");
|
||||
}
|
||||
if (item_class < 6) {
|
||||
this->generate_common_item_variances(area_norm, item);
|
||||
this->generate_common_item_variances(area_norm, res.item);
|
||||
}
|
||||
}
|
||||
return item;
|
||||
return res;
|
||||
}
|
||||
|
||||
ItemData ItemCreator::on_monster_item_drop_with_area_norm(uint32_t enemy_type, uint8_t area_norm) {
|
||||
ItemCreator::DropResult ItemCreator::on_monster_item_drop_with_area_norm(uint32_t enemy_type, uint8_t area_norm) {
|
||||
if (enemy_type > 0x58) {
|
||||
this->log.warning("Invalid enemy type: %" PRIX32, enemy_type);
|
||||
return ItemData();
|
||||
return DropResult();
|
||||
}
|
||||
this->log.info("Enemy type: %" PRIX32 "; random state: %08" PRIX32 " %08" PRIX32, enemy_type, this->random_crypt.seed(), this->random_crypt.absolute_offset());
|
||||
|
||||
@@ -198,13 +201,16 @@ ItemData ItemCreator::on_monster_item_drop_with_area_norm(uint32_t enemy_type, u
|
||||
uint8_t drop_sample = this->rand_int(100);
|
||||
if (drop_sample >= type_drop_prob) {
|
||||
this->log.info("Drop not chosen (%hhu >= %hhu)", drop_sample, type_drop_prob);
|
||||
return ItemData();
|
||||
return DropResult();
|
||||
} else {
|
||||
this->log.info("Drop chosen (%hhu < %hhu)", drop_sample, type_drop_prob);
|
||||
}
|
||||
|
||||
ItemData item = this->check_rare_spec_and_create_rare_enemy_item(enemy_type, area_norm);
|
||||
if (item.empty()) {
|
||||
DropResult res;
|
||||
res.item = this->check_rare_spec_and_create_rare_enemy_item(enemy_type, area_norm);
|
||||
if (!res.item.empty()) {
|
||||
res.is_from_rare_table = true;
|
||||
} else {
|
||||
uint32_t item_class_determinant =
|
||||
this->should_allow_meseta_drops()
|
||||
? this->rand_int(3)
|
||||
@@ -229,34 +235,34 @@ ItemData ItemCreator::on_monster_item_drop_with_area_norm(uint32_t enemy_type, u
|
||||
|
||||
switch (item_class) {
|
||||
case 0: // Weapon
|
||||
item.data1[0] = 0x00;
|
||||
res.item.data1[0] = 0x00;
|
||||
break;
|
||||
case 1: // Armor
|
||||
item.data1w[0] = 0x0101;
|
||||
res.item.data1w[0] = 0x0101;
|
||||
break;
|
||||
case 2: // Shield
|
||||
item.data1w[0] = 0x0201;
|
||||
res.item.data1w[0] = 0x0201;
|
||||
break;
|
||||
case 3: // Unit
|
||||
item.data1w[0] = 0x0301;
|
||||
res.item.data1w[0] = 0x0301;
|
||||
break;
|
||||
case 4: // Tool
|
||||
item.data1[0] = 0x03;
|
||||
res.item.data1[0] = 0x03;
|
||||
break;
|
||||
case 5: // Meseta
|
||||
item.data1[0] = 0x04;
|
||||
item.data2d = this->choose_meseta_amount(this->pt->enemy_meseta_ranges, enemy_type) & 0xFFFF;
|
||||
res.item.data1[0] = 0x04;
|
||||
res.item.data2d = this->choose_meseta_amount(this->pt->enemy_meseta_ranges, enemy_type) & 0xFFFF;
|
||||
break;
|
||||
default:
|
||||
return item;
|
||||
return res;
|
||||
}
|
||||
|
||||
if (item.data1[0] != 0x04) {
|
||||
this->generate_common_item_variances(area_norm, item);
|
||||
if (res.item.data1[0] != 0x04) {
|
||||
this->generate_common_item_variances(area_norm, res.item);
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
return res;
|
||||
}
|
||||
|
||||
ItemData ItemCreator::check_rare_specs_and_create_rare_box_item(uint8_t area_norm) {
|
||||
@@ -474,7 +480,7 @@ void ItemCreator::set_item_unidentified_flag_if_not_challenge(ItemData& item) co
|
||||
|
||||
void ItemCreator::set_tool_item_amount_to_1(ItemData& item) const {
|
||||
if (item.data1[0] == 0x03) {
|
||||
item.set_tool_item_amount(1);
|
||||
item.set_tool_item_amount(this->version, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -695,10 +701,20 @@ uint8_t ItemCreator::generate_tech_disk_level(uint32_t tech_num, uint32_t area_n
|
||||
return range.min;
|
||||
}
|
||||
|
||||
void ItemCreator::generate_common_mag_variances(ItemData& item) const {
|
||||
void ItemCreator::generate_common_mag_variances(ItemData& item) {
|
||||
if (item.data1[0] == 0x02) {
|
||||
item.data1[1] = 0x00;
|
||||
item.assign_mag_stats(ItemMagStats());
|
||||
|
||||
// The original code (on PSO GC) assigns the mag color as 0x0E. We assign
|
||||
// a random color instead.
|
||||
if (is_pre_v1(this->version)) {
|
||||
item.data2[3] = 0x00;
|
||||
} else if (is_v1_or_v2(this->version)) {
|
||||
item.data2[3] = this->random_crypt.next() % 0x0E;
|
||||
} else {
|
||||
item.data2[3] = this->random_crypt.next() % 0x12;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1663,23 +1679,24 @@ void ItemCreator::generate_weapon_shop_item_bonus2(ItemData& item, size_t player
|
||||
}
|
||||
}
|
||||
|
||||
ItemData ItemCreator::on_specialized_box_item_drop(
|
||||
ItemCreator::DropResult ItemCreator::on_specialized_box_item_drop(
|
||||
uint16_t entity_id, uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2) {
|
||||
if (this->destroyed_boxes.count(entity_id)) {
|
||||
return ItemData();
|
||||
return DropResult();
|
||||
}
|
||||
|
||||
ItemData item = this->base_item_for_specialized_box(def0, def1, def2);
|
||||
DropResult res;
|
||||
res.item = this->base_item_for_specialized_box(def0, def1, def2);
|
||||
if (def_z == 0.0f) {
|
||||
uint16_t type = item.data1w[0];
|
||||
item.clear();
|
||||
item.data1w[0] = type;
|
||||
this->generate_common_item_variances(this->normalize_area_number(area), item);
|
||||
uint16_t type = res.item.data1w[0];
|
||||
res.item.clear();
|
||||
res.item.data1w[0] = type;
|
||||
this->generate_common_item_variances(this->normalize_area_number(area), res.item);
|
||||
}
|
||||
return item;
|
||||
return res;
|
||||
}
|
||||
|
||||
ItemData ItemCreator::base_item_for_specialized_box(uint32_t def0, uint32_t def1, uint32_t def2) {
|
||||
ItemData ItemCreator::base_item_for_specialized_box(uint32_t def0, uint32_t def1, uint32_t def2) const {
|
||||
ItemData item;
|
||||
item.data1[0] = (def0 >> 0x18) & 0x0F;
|
||||
item.data1[1] = (def0 >> 0x10) + ((item.data1[0] == 0x00) || (item.data1[0] == 0x01));
|
||||
@@ -1708,7 +1725,7 @@ ItemData ItemCreator::base_item_for_specialized_box(uint32_t def0, uint32_t def1
|
||||
if (item.data1[1] == 0x02) {
|
||||
item.data1[4] = def0 & 0xFF;
|
||||
}
|
||||
item.set_tool_item_amount(1);
|
||||
item.set_tool_item_amount(this->version, 1);
|
||||
break;
|
||||
case 0x04:
|
||||
item.data2d = ((def1 >> 0x10) & 0xFFFF) * 10;
|
||||
|
||||
+12
-7
@@ -31,14 +31,19 @@ public:
|
||||
void set_random_state(uint32_t seed, uint32_t absolute_offset);
|
||||
void clear_destroyed_entities();
|
||||
|
||||
ItemData on_monster_item_drop(uint16_t entity_id, uint32_t enemy_type, uint8_t area);
|
||||
ItemData on_box_item_drop(uint16_t entity_id, uint8_t area);
|
||||
ItemData on_specialized_box_item_drop(uint16_t entity_id, uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2);
|
||||
struct DropResult {
|
||||
ItemData item;
|
||||
bool is_from_rare_table = false;
|
||||
};
|
||||
|
||||
DropResult on_monster_item_drop(uint16_t entity_id, uint32_t enemy_type, uint8_t area);
|
||||
DropResult on_box_item_drop(uint16_t entity_id, uint8_t area);
|
||||
DropResult on_specialized_box_item_drop(uint16_t entity_id, uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2);
|
||||
|
||||
void set_monster_destroyed(uint16_t entity_id);
|
||||
void set_box_destroyed(uint16_t entity_id);
|
||||
|
||||
static ItemData base_item_for_specialized_box(uint32_t def0, uint32_t def1, uint32_t def2);
|
||||
ItemData base_item_for_specialized_box(uint32_t def0, uint32_t def1, uint32_t def2) const;
|
||||
|
||||
std::vector<ItemData> generate_armor_shop_contents(size_t player_level);
|
||||
std::vector<ItemData> generate_tool_shop_contents(size_t player_level);
|
||||
@@ -87,8 +92,8 @@ private:
|
||||
bool are_rare_drops_allowed() const;
|
||||
uint8_t normalize_area_number(uint8_t area) const;
|
||||
|
||||
ItemData on_monster_item_drop_with_area_norm(uint32_t enemy_type, uint8_t area_norm);
|
||||
ItemData on_box_item_drop_with_area_norm(uint8_t area_norm);
|
||||
DropResult on_monster_item_drop_with_area_norm(uint32_t enemy_type, uint8_t area_norm);
|
||||
DropResult on_box_item_drop_with_area_norm(uint8_t area_norm);
|
||||
|
||||
uint32_t rand_int(uint64_t max);
|
||||
float rand_float_0_1_from_crypt();
|
||||
@@ -116,7 +121,7 @@ private:
|
||||
void generate_common_armor_or_shield_type_and_variances(char area_norm, ItemData& item);
|
||||
void generate_common_tool_variances(uint32_t area_norm, ItemData& item);
|
||||
uint8_t generate_tech_disk_level(uint32_t tech_num, uint32_t area_norm);
|
||||
void generate_common_mag_variances(ItemData& item) const;
|
||||
void generate_common_mag_variances(ItemData& item);
|
||||
void generate_common_weapon_variances(uint8_t area_norm, ItemData& item);
|
||||
void generate_common_weapon_grind(ItemData& item, uint8_t offset_within_subtype_range);
|
||||
void generate_common_weapon_bonuses(ItemData& item, uint8_t area_norm);
|
||||
|
||||
+18
-18
@@ -84,7 +84,7 @@ uint32_t ItemData::primary_identifier() const {
|
||||
}
|
||||
}
|
||||
|
||||
bool ItemData::is_wrapped() const {
|
||||
bool ItemData::is_wrapped(Version version) const {
|
||||
switch (this->data1[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
@@ -92,7 +92,7 @@ bool ItemData::is_wrapped() const {
|
||||
case 2:
|
||||
return this->data2[2] & 0x40;
|
||||
case 3:
|
||||
return !this->is_stackable() && (this->data1[3] & 0x40);
|
||||
return !this->is_stackable(version) && (this->data1[3] & 0x40);
|
||||
case 4:
|
||||
return false;
|
||||
default:
|
||||
@@ -100,7 +100,7 @@ bool ItemData::is_wrapped() const {
|
||||
}
|
||||
}
|
||||
|
||||
void ItemData::wrap() {
|
||||
void ItemData::wrap(Version version) {
|
||||
switch (this->data1[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
@@ -110,7 +110,7 @@ void ItemData::wrap() {
|
||||
this->data2[2] |= 0x40;
|
||||
break;
|
||||
case 3:
|
||||
if (!this->is_stackable()) {
|
||||
if (!this->is_stackable(version)) {
|
||||
this->data1[3] |= 0x40;
|
||||
}
|
||||
break;
|
||||
@@ -121,7 +121,7 @@ void ItemData::wrap() {
|
||||
}
|
||||
}
|
||||
|
||||
void ItemData::unwrap() {
|
||||
void ItemData::unwrap(Version version) {
|
||||
switch (this->data1[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
@@ -131,7 +131,7 @@ void ItemData::unwrap() {
|
||||
this->data2[2] &= 0xBF;
|
||||
break;
|
||||
case 3:
|
||||
if (!this->is_stackable()) {
|
||||
if (!this->is_stackable(version)) {
|
||||
this->data1[3] &= 0xBF;
|
||||
}
|
||||
break;
|
||||
@@ -142,23 +142,23 @@ void ItemData::unwrap() {
|
||||
}
|
||||
}
|
||||
|
||||
bool ItemData::is_stackable() const {
|
||||
return this->max_stack_size() > 1;
|
||||
bool ItemData::is_stackable(Version version) const {
|
||||
return this->max_stack_size(version) > 1;
|
||||
}
|
||||
|
||||
size_t ItemData::stack_size() const {
|
||||
if (max_stack_size_for_item(this->data1[0], this->data1[1]) > 1) {
|
||||
size_t ItemData::stack_size(Version version) const {
|
||||
if (max_stack_size_for_item(version, this->data1[0], this->data1[1]) > 1) {
|
||||
return this->data1[5];
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t ItemData::max_stack_size() const {
|
||||
return max_stack_size_for_item(this->data1[0], this->data1[1]);
|
||||
size_t ItemData::max_stack_size(Version version) const {
|
||||
return max_stack_size_for_item(version, this->data1[0], this->data1[1]);
|
||||
}
|
||||
|
||||
void ItemData::enforce_min_stack_size() {
|
||||
if (this->stack_size() == 0) {
|
||||
void ItemData::enforce_min_stack_size(Version version) {
|
||||
if (this->stack_size(version) == 0) {
|
||||
this->data1[5] = 1;
|
||||
}
|
||||
}
|
||||
@@ -502,12 +502,12 @@ void ItemData::set_sealed_item_kill_count(uint16_t v) {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t ItemData::get_tool_item_amount() const {
|
||||
return this->is_stackable() ? this->data1[5] : 1;
|
||||
uint8_t ItemData::get_tool_item_amount(Version version) const {
|
||||
return this->is_stackable(version) ? this->data1[5] : 1;
|
||||
}
|
||||
|
||||
void ItemData::set_tool_item_amount(uint8_t amount) {
|
||||
if (this->is_stackable()) {
|
||||
void ItemData::set_tool_item_amount(Version version, uint8_t amount) {
|
||||
if (this->is_stackable(version)) {
|
||||
this->data1[5] = amount;
|
||||
} else if (this->data1[0] == 0x03) {
|
||||
this->data1[5] = 0x00;
|
||||
|
||||
+18
-29
@@ -29,26 +29,15 @@ enum class EquipSlot {
|
||||
};
|
||||
|
||||
struct ItemMagStats {
|
||||
uint16_t iq;
|
||||
uint16_t synchro;
|
||||
uint16_t def;
|
||||
uint16_t pow;
|
||||
uint16_t dex;
|
||||
uint16_t mind;
|
||||
uint8_t flags;
|
||||
uint8_t photon_blasts;
|
||||
uint8_t color;
|
||||
|
||||
ItemMagStats()
|
||||
: iq(0),
|
||||
synchro(40),
|
||||
def(500),
|
||||
pow(0),
|
||||
dex(0),
|
||||
mind(0),
|
||||
flags(0),
|
||||
photon_blasts(0),
|
||||
color(14) {}
|
||||
uint16_t iq = 0;
|
||||
uint16_t synchro = 40;
|
||||
uint16_t def = 500;
|
||||
uint16_t pow = 0;
|
||||
uint16_t dex = 0;
|
||||
uint16_t mind = 0;
|
||||
uint8_t flags = 0;
|
||||
uint8_t photon_blasts = 0;
|
||||
uint8_t color = 14;
|
||||
|
||||
inline uint16_t def_level() const {
|
||||
return this->def / 100;
|
||||
@@ -140,14 +129,14 @@ struct ItemData { // 0x14 bytes
|
||||
std::string hex() const;
|
||||
uint32_t primary_identifier() const;
|
||||
|
||||
bool is_wrapped() const;
|
||||
void wrap();
|
||||
void unwrap();
|
||||
bool is_wrapped(Version version) const;
|
||||
void wrap(Version version);
|
||||
void unwrap(Version version);
|
||||
|
||||
bool is_stackable() const;
|
||||
size_t stack_size() const;
|
||||
size_t max_stack_size() const;
|
||||
void enforce_min_stack_size();
|
||||
bool is_stackable(Version version) const;
|
||||
size_t stack_size(Version version) const;
|
||||
size_t max_stack_size(Version version) const;
|
||||
void enforce_min_stack_size(Version version);
|
||||
|
||||
static bool is_common_consumable(uint32_t primary_identifier);
|
||||
bool is_common_consumable() const;
|
||||
@@ -166,8 +155,8 @@ struct ItemData { // 0x14 bytes
|
||||
|
||||
uint16_t get_sealed_item_kill_count() const;
|
||||
void set_sealed_item_kill_count(uint16_t v);
|
||||
uint8_t get_tool_item_amount() const;
|
||||
void set_tool_item_amount(uint8_t amount);
|
||||
uint8_t get_tool_item_amount(Version version) const;
|
||||
void set_tool_item_amount(Version version, uint8_t amount);
|
||||
int16_t get_armor_or_shield_defense_bonus() const;
|
||||
void set_armor_or_shield_defense_bonus(int16_t bonus);
|
||||
int16_t get_common_armor_evasion_bonus() const;
|
||||
|
||||
+81
-70
@@ -4,37 +4,69 @@
|
||||
|
||||
using namespace std;
|
||||
|
||||
ItemNameIndex::ItemNameIndex(JSON&& v2_names, JSON&& v3_names, JSON&& v4_names) {
|
||||
auto get_or_create_meta = [&](uint32_t primary_identifier) {
|
||||
shared_ptr<ItemMetadata> meta;
|
||||
try {
|
||||
return this->primary_identifier_index.at(primary_identifier);
|
||||
} catch (const out_of_range&) {
|
||||
auto meta = make_shared<ItemMetadata>();
|
||||
meta->primary_identifier = primary_identifier;
|
||||
this->primary_identifier_index.emplace(primary_identifier, meta);
|
||||
return meta;
|
||||
// class ItemNameIndex {
|
||||
// public:
|
||||
// ItemNameIndex(std::shared_ptr<const ItemParameterTable> pmt, const std::vector<std::string>& name_coll);
|
||||
// std::string describe_item(const ItemData& item, bool include_color_escapes = false) const;
|
||||
// ItemData parse_item_description(const std::string& description) const;
|
||||
// private:
|
||||
// ItemData parse_item_description_phase(const std::string& description, bool skip_special) const;
|
||||
// std::shared_ptr<const ItemParameterTable> item_parameter_table;
|
||||
// struct ItemMetadata {
|
||||
// uint32_t primary_identifier;
|
||||
// std::string name;
|
||||
// };
|
||||
// std::unordered_map<uint32_t, std::shared_ptr<ItemMetadata>> primary_identifier_indexes;
|
||||
// std::map<std::string, std::shared_ptr<ItemMetadata>> name_indexes;
|
||||
// };
|
||||
|
||||
ItemNameIndex::ItemNameIndex(
|
||||
Version version,
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table,
|
||||
const std::vector<std::string>& name_coll)
|
||||
: version(version),
|
||||
item_parameter_table(item_parameter_table) {
|
||||
|
||||
auto find_items_1d = [&](uint64_t data1, size_t position) -> size_t {
|
||||
ItemData item(data1, 0);
|
||||
for (size_t x = 0; x < 0x100; x++) {
|
||||
item.data1[position] = x;
|
||||
uint32_t id;
|
||||
try {
|
||||
id = this->item_parameter_table->get_item_id(item);
|
||||
} catch (const out_of_range&) {
|
||||
return x;
|
||||
}
|
||||
const string* name = nullptr;
|
||||
try {
|
||||
name = &name_coll.at(id);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
if (name) {
|
||||
auto meta = make_shared<ItemMetadata>();
|
||||
meta->primary_identifier = item.primary_identifier();
|
||||
meta->name = *name;
|
||||
this->primary_identifier_index.emplace(meta->primary_identifier, meta);
|
||||
this->name_index.emplace(tolower(meta->name), meta);
|
||||
}
|
||||
}
|
||||
return 0x100;
|
||||
};
|
||||
auto find_items_2d = [&](uint64_t data1) {
|
||||
for (size_t x = 0; x < 0x100; x++) {
|
||||
if (find_items_1d(data1 | (static_cast<uint64_t>(x) << 48), 2) == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const auto& it : v2_names.as_dict()) {
|
||||
uint32_t primary_identifier = stoul(it.first, nullptr, 16);
|
||||
auto meta = get_or_create_meta(primary_identifier);
|
||||
meta->v2_name = std::move(it.second->as_string());
|
||||
this->v2_name_index.emplace(tolower(meta->v2_name), meta);
|
||||
}
|
||||
for (const auto& it : v3_names.as_dict()) {
|
||||
uint32_t primary_identifier = stoul(it.first, nullptr, 16);
|
||||
auto meta = get_or_create_meta(primary_identifier);
|
||||
meta->v3_name = std::move(it.second->as_string());
|
||||
this->v3_name_index.emplace(tolower(meta->v3_name), meta);
|
||||
}
|
||||
for (const auto& it : v4_names.as_dict()) {
|
||||
uint32_t primary_identifier = stoul(it.first, nullptr, 16);
|
||||
auto meta = get_or_create_meta(primary_identifier);
|
||||
meta->v4_name = std::move(it.second->as_string());
|
||||
this->v4_name_index.emplace(tolower(meta->v4_name), meta);
|
||||
}
|
||||
find_items_2d(0x0000000000000000);
|
||||
find_items_1d(0x0101000000000000, 2);
|
||||
find_items_1d(0x0102000000000000, 2);
|
||||
find_items_1d(0x0103000000000000, 2);
|
||||
find_items_1d(0x0200000000000000, 1);
|
||||
find_items_2d(0x0300000000000000);
|
||||
}
|
||||
|
||||
static const char* s_rank_name_characters = "\0ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_";
|
||||
@@ -106,11 +138,10 @@ const array<const char*, 0x11> name_for_s_rank_special = {
|
||||
};
|
||||
|
||||
std::string ItemNameIndex::describe_item(
|
||||
Version version,
|
||||
const ItemData& item,
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table) const {
|
||||
bool include_color_escapes) const {
|
||||
if (item.data1[0] == 0x04) {
|
||||
return string_printf("%s%" PRIu32 " Meseta", item_parameter_table ? "$C7" : "", item.data2d.load());
|
||||
return string_printf("%s%" PRIu32 " Meseta", include_color_escapes ? "$C7" : "", item.data2d.load());
|
||||
}
|
||||
|
||||
vector<string> ret_tokens;
|
||||
@@ -147,7 +178,7 @@ std::string ItemNameIndex::describe_item(
|
||||
// flags in a different location.
|
||||
if (((item.data1[1] == 0x01) && (item.data1[4] & 0x40)) ||
|
||||
((item.data1[0] == 0x02) && (item.data2[2] & 0x40)) ||
|
||||
((item.data1[0] == 0x03) && !item.is_stackable() && (item.data1[3] & 0x40))) {
|
||||
((item.data1[0] == 0x03) && !item.is_stackable(this->version) && (item.data1[3] & 0x40))) {
|
||||
ret_tokens.emplace_back("Wrapped");
|
||||
}
|
||||
|
||||
@@ -172,18 +203,7 @@ std::string ItemNameIndex::describe_item(
|
||||
} else {
|
||||
try {
|
||||
auto meta = this->primary_identifier_index.at(primary_identifier);
|
||||
const string* name;
|
||||
if (is_v4(version)) {
|
||||
name = &meta->v4_name;
|
||||
} else if (is_v3(version)) {
|
||||
name = &meta->v3_name;
|
||||
} else {
|
||||
name = &meta->v2_name;
|
||||
}
|
||||
if (name->empty()) {
|
||||
throw out_of_range("item does not exist");
|
||||
}
|
||||
ret_tokens.emplace_back(*name);
|
||||
ret_tokens.emplace_back(meta->name);
|
||||
|
||||
} catch (const out_of_range&) {
|
||||
ret_tokens.emplace_back(string_printf("!ID:%06" PRIX32, primary_identifier));
|
||||
@@ -341,16 +361,16 @@ std::string ItemNameIndex::describe_item(
|
||||
|
||||
// For tools, add the amount (if applicable)
|
||||
} else if (item.data1[0] == 0x03) {
|
||||
if (item.max_stack_size() > 1) {
|
||||
if (item.max_stack_size(this->version) > 1) {
|
||||
ret_tokens.emplace_back(string_printf("x%hhu", item.data1[5]));
|
||||
}
|
||||
}
|
||||
|
||||
string ret = join(ret_tokens, " ");
|
||||
if (item_parameter_table) {
|
||||
if (include_color_escapes) {
|
||||
if (item.is_s_rank_weapon()) {
|
||||
return "$C4" + ret;
|
||||
} else if (item_parameter_table->is_item_rare(item)) {
|
||||
} else if (this->item_parameter_table->is_item_rare(item)) {
|
||||
return "$C6" + ret;
|
||||
} else if (item.has_bonuses()) {
|
||||
return "$C2" + ret;
|
||||
@@ -362,32 +382,32 @@ std::string ItemNameIndex::describe_item(
|
||||
}
|
||||
}
|
||||
|
||||
ItemData ItemNameIndex::parse_item_description(Version version, const std::string& desc) const {
|
||||
ItemData ItemNameIndex::parse_item_description(const std::string& desc) const {
|
||||
ItemData ret;
|
||||
try {
|
||||
ret = this->parse_item_description_phase(version, desc, false);
|
||||
ret = this->parse_item_description_phase(desc, false);
|
||||
} catch (const exception& e1) {
|
||||
try {
|
||||
ret = this->parse_item_description_phase(version, desc, true);
|
||||
ret = this->parse_item_description_phase(desc, true);
|
||||
} catch (const exception& e2) {
|
||||
try {
|
||||
ret = ItemData::from_data(parse_data_string(desc));
|
||||
} catch (const exception& ed) {
|
||||
if (strcmp(e1.what(), e2.what())) {
|
||||
throw runtime_error(string_printf("cannot parse item description \"%s\" in %s (as text 1: %s) (as text 2: %s) (as data: %s)",
|
||||
desc.c_str(), name_for_enum(version), e1.what(), e2.what(), ed.what()));
|
||||
throw runtime_error(string_printf("cannot parse item description \"%s\" (as text 1: %s) (as text 2: %s) (as data: %s)",
|
||||
desc.c_str(), e1.what(), e2.what(), ed.what()));
|
||||
} else {
|
||||
throw runtime_error(string_printf("cannot parse item description \"%s\" in %s (as text: %s) (as data: %s)",
|
||||
desc.c_str(), name_for_enum(version), e1.what(), ed.what()));
|
||||
throw runtime_error(string_printf("cannot parse item description \"%s\" (as text: %s) (as data: %s)",
|
||||
desc.c_str(), e1.what(), ed.what()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ret.enforce_min_stack_size();
|
||||
ret.enforce_min_stack_size(this->version);
|
||||
return ret;
|
||||
}
|
||||
|
||||
ItemData ItemNameIndex::parse_item_description_phase(Version version, const std::string& description, bool skip_special) const {
|
||||
ItemData ItemNameIndex::parse_item_description_phase(const std::string& description, bool skip_special) const {
|
||||
ItemData ret;
|
||||
ret.data1d.clear(0);
|
||||
ret.id = 0xFFFFFFFF;
|
||||
@@ -448,24 +468,15 @@ ItemData ItemNameIndex::parse_item_description_phase(Version version, const std:
|
||||
}
|
||||
}
|
||||
|
||||
const map<string, shared_ptr<ItemMetadata>>* name_index;
|
||||
if (is_v4(version)) {
|
||||
name_index = &this->v4_name_index;
|
||||
} else if (is_v3(version)) {
|
||||
name_index = &this->v3_name_index;
|
||||
} else {
|
||||
name_index = &this->v2_name_index;
|
||||
}
|
||||
|
||||
auto name_it = name_index->lower_bound(desc);
|
||||
auto name_it = this->name_index.lower_bound(desc);
|
||||
// Look up to 3 places before the lower bound. We have to do this to catch
|
||||
// cases like Sange vs. Sange & Yasha - if the input is like "Sange 0/...",
|
||||
// then we'll see Sange & Yasha first, which we should skip.
|
||||
size_t lookback = 0;
|
||||
while (lookback < 4) {
|
||||
if (name_it != name_index->end() && desc.starts_with(name_it->first)) {
|
||||
if (name_it != this->name_index.end() && desc.starts_with(name_it->first)) {
|
||||
break;
|
||||
} else if (name_it == name_index->begin()) {
|
||||
} else if (name_it == this->name_index.begin()) {
|
||||
throw runtime_error("no such item");
|
||||
} else {
|
||||
name_it--;
|
||||
@@ -626,7 +637,7 @@ ItemData ItemNameIndex::parse_item_description_phase(Version version, const std:
|
||||
ret.data2[2] |= 0x40;
|
||||
}
|
||||
} else if (ret.data1[0] == 0x03) {
|
||||
if (ret.max_stack_size() > 1) {
|
||||
if (ret.max_stack_size(this->version) > 1) {
|
||||
if (starts_with(desc, "x")) {
|
||||
ret.data1[5] = stoul(desc.substr(1), nullptr, 10);
|
||||
} else {
|
||||
@@ -637,7 +648,7 @@ ItemData ItemNameIndex::parse_item_description_phase(Version version, const std:
|
||||
}
|
||||
|
||||
if (is_wrapped) {
|
||||
if (ret.is_stackable()) {
|
||||
if (ret.is_stackable(this->version)) {
|
||||
throw runtime_error("stackable items cannot be wrapped");
|
||||
} else {
|
||||
ret.data1[3] |= 0x40;
|
||||
|
||||
+28
-18
@@ -14,26 +14,36 @@
|
||||
|
||||
class ItemNameIndex {
|
||||
public:
|
||||
ItemNameIndex(JSON&& v2_names, JSON&& v3_names, JSON&& v4_names);
|
||||
|
||||
std::string describe_item(
|
||||
Version version,
|
||||
const ItemData& item,
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table = nullptr) const;
|
||||
ItemData parse_item_description(Version version, const std::string& description) const;
|
||||
|
||||
private:
|
||||
ItemData parse_item_description_phase(Version version, const std::string& description, bool skip_special) const;
|
||||
|
||||
struct ItemMetadata {
|
||||
uint32_t primary_identifier;
|
||||
std::string v2_name;
|
||||
std::string v3_name;
|
||||
std::string v4_name;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
std::unordered_map<uint32_t, std::shared_ptr<ItemMetadata>> primary_identifier_index;
|
||||
std::map<std::string, std::shared_ptr<ItemMetadata>> v2_name_index;
|
||||
std::map<std::string, std::shared_ptr<ItemMetadata>> v3_name_index;
|
||||
std::map<std::string, std::shared_ptr<ItemMetadata>> v4_name_index;
|
||||
ItemNameIndex(
|
||||
Version version,
|
||||
std::shared_ptr<const ItemParameterTable> pmt,
|
||||
const std::vector<std::string>& name_coll);
|
||||
|
||||
inline size_t entry_count() const {
|
||||
return this->primary_identifier_index.size();
|
||||
}
|
||||
|
||||
inline const std::unordered_map<uint32_t, std::shared_ptr<const ItemMetadata>>& all_by_primary_identifier() const {
|
||||
return this->primary_identifier_index;
|
||||
}
|
||||
inline const std::map<std::string, std::shared_ptr<const ItemMetadata>>& all_by_name() const {
|
||||
return this->name_index;
|
||||
}
|
||||
|
||||
std::string describe_item(const ItemData& item, bool include_color_escapes = false) const;
|
||||
ItemData parse_item_description(const std::string& description) const;
|
||||
|
||||
private:
|
||||
ItemData parse_item_description_phase(const std::string& description, bool skip_special) const;
|
||||
|
||||
Version version;
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table;
|
||||
|
||||
std::unordered_map<uint32_t, std::shared_ptr<const ItemMetadata>> primary_identifier_index;
|
||||
std::map<std::string, std::shared_ptr<const ItemMetadata>> name_index;
|
||||
};
|
||||
|
||||
+46
-43
@@ -52,13 +52,13 @@ ItemParameterTable::ItemParameterTable(shared_ptr<const string> data, Version ve
|
||||
|
||||
const ItemParameterTable::WeaponV4& ItemParameterTable::get_weapon(uint8_t data1_1, uint8_t data1_2) const {
|
||||
if (data1_1 >= this->num_weapon_classes) {
|
||||
throw runtime_error("weapon ID out of range");
|
||||
throw out_of_range("weapon ID out of range");
|
||||
}
|
||||
|
||||
if (this->offsets_v4) {
|
||||
const auto& co = this->r.pget<ArrayRefLE>(this->offsets_v4->weapon_table + sizeof(ArrayRefLE) * data1_1);
|
||||
if (data1_2 >= co.count) {
|
||||
throw runtime_error("weapon ID out of range");
|
||||
throw out_of_range("weapon ID out of range");
|
||||
}
|
||||
return this->r.pget<WeaponV4>(co.offset + sizeof(WeaponV4) * data1_2);
|
||||
}
|
||||
@@ -67,12 +67,12 @@ const ItemParameterTable::WeaponV4& ItemParameterTable::get_weapon(uint8_t data1
|
||||
try {
|
||||
return this->parsed_weapons.at(key);
|
||||
} catch (const std::out_of_range&) {
|
||||
auto& def_v4 = this->parsed_weapons.emplace(key, WeaponV4{}).first->second;
|
||||
WeaponV4 def_v4;
|
||||
|
||||
if (this->offsets_v2) {
|
||||
const auto& co = this->r.pget<ArrayRefLE>(this->offsets_v2->weapon_table + sizeof(ArrayRefLE) * data1_1);
|
||||
if (data1_2 >= co.count) {
|
||||
throw runtime_error("weapon ID out of range");
|
||||
throw out_of_range("weapon ID out of range");
|
||||
}
|
||||
const auto& def_v2 = this->r.pget<WeaponV2>(co.offset + sizeof(WeaponV2) * data1_2);
|
||||
def_v4.base.id = def_v2.base.id;
|
||||
@@ -90,7 +90,7 @@ const ItemParameterTable::WeaponV4& ItemParameterTable::get_weapon(uint8_t data1
|
||||
} else if (this->offsets_v3) {
|
||||
const auto& co = this->r.pget<ArrayRefBE>(this->offsets_v3->weapon_table + sizeof(ArrayRefBE) * data1_1);
|
||||
if (data1_2 >= co.count) {
|
||||
throw runtime_error("weapon ID out of range");
|
||||
throw out_of_range("weapon ID out of range");
|
||||
}
|
||||
const auto& def_v3 = this->r.pget<WeaponV3<true>>(co.offset + sizeof(WeaponV3<true>) * data1_2);
|
||||
def_v4.base.id = def_v3.base.id.load();
|
||||
@@ -126,19 +126,19 @@ const ItemParameterTable::WeaponV4& ItemParameterTable::get_weapon(uint8_t data1
|
||||
throw logic_error("table is not v2, v3, or v4");
|
||||
}
|
||||
|
||||
return def_v4;
|
||||
return this->parsed_weapons.emplace(key, def_v4).first->second;
|
||||
}
|
||||
}
|
||||
|
||||
const ItemParameterTable::ArmorOrShieldV4& ItemParameterTable::get_armor_or_shield(uint8_t data1_1, uint8_t data1_2) const {
|
||||
if ((data1_1 < 1) || (data1_1 > 2)) {
|
||||
throw runtime_error("armor/shield class ID out of range");
|
||||
throw out_of_range("armor/shield class ID out of range");
|
||||
}
|
||||
|
||||
if (this->offsets_v4) {
|
||||
const auto& co = this->r.pget<ArrayRefLE>(this->offsets_v4->armor_table + sizeof(ArrayRefLE) * (data1_1 - 1));
|
||||
if (data1_2 >= co.count) {
|
||||
throw runtime_error("armor/shield ID out of range");
|
||||
throw out_of_range("armor/shield ID out of range");
|
||||
}
|
||||
return this->r.pget<ArmorOrShieldV4>(co.offset + sizeof(ArmorOrShieldV4) * data1_2);
|
||||
}
|
||||
@@ -151,15 +151,12 @@ const ItemParameterTable::ArmorOrShieldV4& ItemParameterTable::get_armor_or_shie
|
||||
}
|
||||
return ret;
|
||||
} catch (const std::out_of_range&) {
|
||||
if (data1_2 >= parsed_vec.size()) {
|
||||
parsed_vec.resize(data1_2 + 1);
|
||||
}
|
||||
auto& def_v4 = parsed_vec[data1_2];
|
||||
ArmorOrShieldV4 def_v4;
|
||||
|
||||
if (this->offsets_v2) {
|
||||
const auto& co = this->r.pget<ArrayRefLE>(this->offsets_v2->armor_table + sizeof(ArrayRefLE) * (data1_1 - 1));
|
||||
if (data1_2 >= co.count) {
|
||||
throw runtime_error("armor/shield ID out of range");
|
||||
throw out_of_range("armor/shield ID out of range");
|
||||
}
|
||||
const auto& def_v2 = this->r.pget<ArmorOrShieldV2>(co.offset + sizeof(ArmorOrShieldV2) * data1_2);
|
||||
def_v4.base.id = def_v2.base.id;
|
||||
@@ -183,7 +180,7 @@ const ItemParameterTable::ArmorOrShieldV4& ItemParameterTable::get_armor_or_shie
|
||||
} else if (this->offsets_v3) {
|
||||
const auto& co = this->r.pget<ArrayRefBE>(this->offsets_v3->armor_table + sizeof(ArrayRefBE) * (data1_1 - 1));
|
||||
if (data1_2 >= co.count) {
|
||||
throw runtime_error("armor/shield ID out of range");
|
||||
throw out_of_range("armor/shield ID out of range");
|
||||
}
|
||||
const auto& def_v3 = this->r.pget<ArmorOrShieldV3>(co.offset + sizeof(ArmorOrShieldV3) * data1_2);
|
||||
def_v4.base.id = def_v3.base.id.load();
|
||||
@@ -210,7 +207,11 @@ const ItemParameterTable::ArmorOrShieldV4& ItemParameterTable::get_armor_or_shie
|
||||
throw logic_error("table is not v2, v3, or v4");
|
||||
}
|
||||
|
||||
return def_v4;
|
||||
if (data1_2 >= parsed_vec.size()) {
|
||||
parsed_vec.resize(data1_2 + 1);
|
||||
}
|
||||
parsed_vec[data1_2] = def_v4;
|
||||
return parsed_vec[data1_2];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +219,7 @@ const ItemParameterTable::UnitV4& ItemParameterTable::get_unit(uint8_t data1_2)
|
||||
if (this->offsets_v4) {
|
||||
const auto& co = this->r.pget<ArrayRefLE>(this->offsets_v4->unit_table);
|
||||
if (data1_2 >= co.count) {
|
||||
throw runtime_error("unit ID out of range");
|
||||
throw out_of_range("unit ID out of range");
|
||||
}
|
||||
return this->r.pget<UnitV4>(co.offset + sizeof(UnitV4) * data1_2);
|
||||
}
|
||||
@@ -230,15 +231,12 @@ const ItemParameterTable::UnitV4& ItemParameterTable::get_unit(uint8_t data1_2)
|
||||
}
|
||||
return ret;
|
||||
} catch (const std::out_of_range&) {
|
||||
if (data1_2 >= this->parsed_units.size()) {
|
||||
this->parsed_units.resize(data1_2 + 1);
|
||||
}
|
||||
auto& def_v4 = this->parsed_units[data1_2];
|
||||
UnitV4 def_v4;
|
||||
|
||||
if (this->offsets_v2) {
|
||||
const auto& co = this->r.pget<ArrayRefLE>(this->offsets_v2->unit_table);
|
||||
if (data1_2 >= co.count) {
|
||||
throw runtime_error("unit ID out of range");
|
||||
throw out_of_range("unit ID out of range");
|
||||
}
|
||||
const auto& def_v2 = this->r.pget<UnitV2>(co.offset + sizeof(UnitV2) * data1_2);
|
||||
def_v4.base.id = def_v2.base.id;
|
||||
@@ -249,7 +247,7 @@ const ItemParameterTable::UnitV4& ItemParameterTable::get_unit(uint8_t data1_2)
|
||||
} else if (this->offsets_v3) {
|
||||
const auto& co = this->r.pget<ArrayRefBE>(this->offsets_v3->unit_table);
|
||||
if (data1_2 >= co.count) {
|
||||
throw runtime_error("unit ID out of range");
|
||||
throw out_of_range("unit ID out of range");
|
||||
}
|
||||
const auto& def_v3 = this->r.pget<UnitV3>(co.offset + sizeof(UnitV3) * data1_2);
|
||||
def_v4.base.id = def_v3.base.id.load();
|
||||
@@ -263,7 +261,11 @@ const ItemParameterTable::UnitV4& ItemParameterTable::get_unit(uint8_t data1_2)
|
||||
throw logic_error("table is not v2, v3, or v4");
|
||||
}
|
||||
|
||||
return def_v4;
|
||||
if (data1_2 >= this->parsed_units.size()) {
|
||||
this->parsed_units.resize(data1_2 + 1);
|
||||
}
|
||||
this->parsed_units[data1_2] = def_v4;
|
||||
return this->parsed_units[data1_2];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +273,7 @@ const ItemParameterTable::MagV4& ItemParameterTable::get_mag(uint8_t data1_1) co
|
||||
if (this->offsets_v4) {
|
||||
const auto& co = this->r.pget<ArrayRefLE>(this->offsets_v4->mag_table);
|
||||
if (data1_1 >= co.count) {
|
||||
throw runtime_error("mag ID out of range");
|
||||
throw out_of_range("mag ID out of range");
|
||||
}
|
||||
return this->r.pget<MagV4>(co.offset + sizeof(MagV4) * data1_1);
|
||||
}
|
||||
@@ -283,15 +285,12 @@ const ItemParameterTable::MagV4& ItemParameterTable::get_mag(uint8_t data1_1) co
|
||||
}
|
||||
return ret;
|
||||
} catch (const std::out_of_range&) {
|
||||
if (data1_1 >= this->parsed_mags.size()) {
|
||||
this->parsed_mags.resize(data1_1 + 1);
|
||||
}
|
||||
auto& def_v4 = this->parsed_mags[data1_1];
|
||||
MagV4 def_v4;
|
||||
|
||||
if (this->offsets_v2) {
|
||||
const auto& co = this->r.pget<ArrayRefLE>(this->offsets_v2->mag_table);
|
||||
if (data1_1 >= co.count) {
|
||||
throw runtime_error("mag ID out of range");
|
||||
throw out_of_range("mag ID out of range");
|
||||
}
|
||||
const auto& def_v2 = this->r.pget<MagV2>(co.offset + sizeof(MagV2) * data1_1);
|
||||
def_v4.base.id = def_v2.base.id;
|
||||
@@ -311,7 +310,7 @@ const ItemParameterTable::MagV4& ItemParameterTable::get_mag(uint8_t data1_1) co
|
||||
} else if (this->offsets_v3) {
|
||||
const auto& co = this->r.pget<ArrayRefBE>(this->offsets_v3->mag_table);
|
||||
if (data1_1 >= co.count) {
|
||||
throw runtime_error("mag ID out of range");
|
||||
throw out_of_range("mag ID out of range");
|
||||
}
|
||||
const auto& def_v3 = this->r.pget<MagV3>(co.offset + sizeof(MagV3) * data1_1);
|
||||
def_v4.base.id = def_v3.base.id.load();
|
||||
@@ -334,19 +333,23 @@ const ItemParameterTable::MagV4& ItemParameterTable::get_mag(uint8_t data1_1) co
|
||||
throw logic_error("table is not v2, v3, or v4");
|
||||
}
|
||||
|
||||
return def_v4;
|
||||
if (data1_1 >= this->parsed_mags.size()) {
|
||||
this->parsed_mags.resize(data1_1 + 1);
|
||||
}
|
||||
this->parsed_mags[data1_1] = def_v4;
|
||||
return this->parsed_mags[data1_1];
|
||||
}
|
||||
}
|
||||
|
||||
const ItemParameterTable::ToolV4& ItemParameterTable::get_tool(uint8_t data1_1, uint8_t data1_2) const {
|
||||
if (data1_1 >= this->num_tool_classes) {
|
||||
throw runtime_error("tool class ID out of range");
|
||||
throw out_of_range("tool class ID out of range");
|
||||
}
|
||||
|
||||
if (this->offsets_v4) {
|
||||
const auto& co = this->r.pget<ArrayRefLE>(this->offsets_v4->tool_table + sizeof(ArrayRefLE) * data1_1);
|
||||
if (data1_2 >= co.count) {
|
||||
throw runtime_error("tool ID out of range");
|
||||
throw out_of_range("tool ID out of range");
|
||||
}
|
||||
return this->r.pget<ToolV4>(co.offset + sizeof(ToolV4) * data1_2);
|
||||
}
|
||||
@@ -355,12 +358,12 @@ const ItemParameterTable::ToolV4& ItemParameterTable::get_tool(uint8_t data1_1,
|
||||
try {
|
||||
return this->parsed_tools.at(key);
|
||||
} catch (const std::out_of_range&) {
|
||||
auto& def_v4 = this->parsed_tools.emplace(key, ToolV4{}).first->second;
|
||||
ToolV4 def_v4;
|
||||
|
||||
if (this->offsets_v2) {
|
||||
const auto& co = this->r.pget<ArrayRefLE>(this->offsets_v2->tool_table + sizeof(ArrayRefLE) * data1_1);
|
||||
if (data1_2 >= co.count) {
|
||||
throw runtime_error("tool ID out of range");
|
||||
throw out_of_range("tool ID out of range");
|
||||
}
|
||||
const auto& def_v2 = this->r.pget<ToolV2>(co.offset + sizeof(ToolV2) * data1_2);
|
||||
def_v4.base.id = def_v2.base.id;
|
||||
@@ -372,7 +375,7 @@ const ItemParameterTable::ToolV4& ItemParameterTable::get_tool(uint8_t data1_1,
|
||||
} else if (this->offsets_v3) {
|
||||
const auto& co = this->r.pget<ArrayRefBE>(this->offsets_v3->tool_table + sizeof(ArrayRefBE) * data1_1);
|
||||
if (data1_2 >= co.count) {
|
||||
throw runtime_error("tool ID out of range");
|
||||
throw out_of_range("tool ID out of range");
|
||||
}
|
||||
const auto& def_v3 = this->r.pget<ToolV3>(co.offset + sizeof(ToolV3) * data1_2);
|
||||
def_v4.base.id = def_v3.base.id.load();
|
||||
@@ -387,7 +390,7 @@ const ItemParameterTable::ToolV4& ItemParameterTable::get_tool(uint8_t data1_1,
|
||||
throw logic_error("table is not v2, v3, or v4");
|
||||
}
|
||||
|
||||
return def_v4;
|
||||
return this->parsed_tools.emplace(key, def_v4).first->second;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,10 +475,10 @@ float ItemParameterTable::get_sale_divisor(uint8_t data1_0, uint8_t data1_1) con
|
||||
const ItemParameterTable::MagFeedResult& ItemParameterTable::get_mag_feed_result(
|
||||
uint8_t table_index, uint8_t item_index) const {
|
||||
if (table_index >= 8) {
|
||||
throw runtime_error("invalid mag feed table index");
|
||||
throw out_of_range("invalid mag feed table index");
|
||||
}
|
||||
if (item_index >= 11) {
|
||||
throw runtime_error("invalid mag feed item index");
|
||||
throw out_of_range("invalid mag feed item index");
|
||||
}
|
||||
|
||||
uint32_t offset;
|
||||
@@ -521,7 +524,7 @@ uint8_t ItemParameterTable::get_special_stars(uint8_t det) const {
|
||||
const ItemParameterTable::Special<false>& ItemParameterTable::get_special(uint8_t special) const {
|
||||
special &= 0x3F;
|
||||
if (special >= this->num_specials) {
|
||||
throw runtime_error("invalid special index");
|
||||
throw out_of_range("invalid special index");
|
||||
}
|
||||
|
||||
if (this->offsets_v2) {
|
||||
@@ -545,10 +548,10 @@ const ItemParameterTable::Special<false>& ItemParameterTable::get_special(uint8_
|
||||
|
||||
uint8_t ItemParameterTable::get_max_tech_level(uint8_t char_class, uint8_t tech_num) const {
|
||||
if (char_class >= 12) {
|
||||
throw runtime_error("invalid character class");
|
||||
throw out_of_range("invalid character class");
|
||||
}
|
||||
if (tech_num >= 19) {
|
||||
throw runtime_error("invalid technique number");
|
||||
throw out_of_range("invalid technique number");
|
||||
}
|
||||
|
||||
if (this->offsets_v2) {
|
||||
@@ -762,7 +765,7 @@ std::pair<const ItemParameterTable::EventItem*, size_t> ItemParameterTable::get_
|
||||
uint32_t base_offset, uint8_t event_number) const {
|
||||
const auto& co = this->r.pget<ArrayRef<IsBigEndian>>(base_offset);
|
||||
if (event_number >= co.count) {
|
||||
throw runtime_error("invalid event number");
|
||||
throw out_of_range("invalid event number");
|
||||
}
|
||||
const auto& event_co = this->r.pget<ArrayRef<IsBigEndian>>(co.offset + sizeof(ArrayRef<IsBigEndian>) * event_number);
|
||||
const auto* defs = &this->r.pget<EventItem>(event_co.offset, event_co.count * sizeof(EventItem));
|
||||
|
||||
@@ -55,7 +55,8 @@ public:
|
||||
uint8_t photon = 0;
|
||||
uint8_t special = 0;
|
||||
uint8_t ata = 0;
|
||||
parray<uint8_t, 4> unknown_a9;
|
||||
uint8_t stat_boost = 0; // TODO: This could be larger (16 or 32 bits)
|
||||
parray<uint8_t, 3> unknown_a9;
|
||||
} __attribute__((packed));
|
||||
|
||||
template <bool IsBigEndian>
|
||||
@@ -368,6 +369,9 @@ private:
|
||||
/* 40 / 5704 */ le_uint32_t shield_effect_table; // -> [8-byte structs]
|
||||
} __attribute__((packed));
|
||||
|
||||
// TODO: The GC NTE ItemPMT format is intermediate between V2 and V3 - the
|
||||
// Offsets struct is 0x50 bytes. Figure it out and add support here.
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct TableOffsetsV3V4 {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
|
||||
+8
-8
@@ -23,7 +23,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<PSOLFGE
|
||||
// Nothing to do (it should be deleted)
|
||||
|
||||
} else if (item_identifier == 0x030200) { // Technique disk
|
||||
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
|
||||
auto item_parameter_table = s->item_parameter_table(c->version());
|
||||
uint8_t max_level = item_parameter_table->get_max_tech_level(player->disp.visual.char_class, item.data.data1[4]);
|
||||
if (item.data.data1[2] > max_level) {
|
||||
throw runtime_error("technique level too high");
|
||||
@@ -43,7 +43,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<PSOLFGE
|
||||
// get an accurate picture of what's actually in the player's inventory, so
|
||||
// there's no way to know if we would be enforcing the correct grind limit.
|
||||
if (is_v3_or_later) {
|
||||
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
|
||||
auto item_parameter_table = s->item_parameter_table(c->version());
|
||||
auto weapon_def = item_parameter_table->get_weapon(weapon.data.data1[1], weapon.data.data1[2]);
|
||||
if (weapon.data.data1[3] >= weapon_def.max_grind) {
|
||||
throw runtime_error("weapon already at maximum grind");
|
||||
@@ -111,9 +111,9 @@ void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<PSOLFGE
|
||||
}
|
||||
armor.data.data1[5]++;
|
||||
|
||||
} else if (item.data.is_wrapped()) {
|
||||
} else if (item.data.is_wrapped(c->version())) {
|
||||
// Unwrap present
|
||||
item.data.unwrap();
|
||||
item.data.unwrap(c->version());
|
||||
should_delete_item = false;
|
||||
|
||||
} else if (item_identifier == 0x003300) {
|
||||
@@ -168,7 +168,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<PSOLFGE
|
||||
|
||||
} else if ((item_identifier & 0xFFFF00) == 0x031500) {
|
||||
// Christmas Present, etc. - use unwrap_table + probabilities therein
|
||||
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
|
||||
auto item_parameter_table = s->item_parameter_table(c->version());
|
||||
auto table = item_parameter_table->get_event_items(item.data.data1[2]);
|
||||
size_t sum = 0;
|
||||
for (size_t z = 0; z < table.second; z++) {
|
||||
@@ -207,7 +207,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<PSOLFGE
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
|
||||
auto item_parameter_table = s->item_parameter_table(c->version());
|
||||
const auto& combo = item_parameter_table->get_item_combination(item.data, inv_item.data);
|
||||
if (combo.char_class != 0xFF && combo.char_class != player->disp.visual.char_class) {
|
||||
throw runtime_error("item combination requires specific char_class");
|
||||
@@ -250,7 +250,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<PSOLFGE
|
||||
if (should_delete_item) {
|
||||
// Allow overdrafting meseta if the client is not BB, since the server isn't
|
||||
// informed when meseta is added or removed from the bank.
|
||||
player->remove_item(item.data.id, 1, !is_v4(c->version()));
|
||||
player->remove_item(item.data.id, 1, c->version());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,7 +497,7 @@ void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fe
|
||||
apply_mag_feed_result(
|
||||
player->inventory.items[mag_item_index].data,
|
||||
player->inventory.items[fed_item_index].data,
|
||||
s->item_parameter_table_for_version(c->version()),
|
||||
s->item_parameter_table(c->version()),
|
||||
s->mag_evolution_table,
|
||||
player->disp.visual.char_class,
|
||||
player->disp.visual.section_id,
|
||||
|
||||
+21
-7
@@ -251,7 +251,7 @@ void Lobby::create_item_creator() {
|
||||
s->tool_random_set,
|
||||
s->weapon_random_sets.at(this->difficulty),
|
||||
s->tekker_adjustment_set,
|
||||
s->item_parameter_table_for_version(this->base_version),
|
||||
s->item_parameter_table(this->base_version),
|
||||
this->base_version,
|
||||
this->episode,
|
||||
(this->mode == GameMode::SOLO) ? GameMode::NORMAL : this->mode,
|
||||
@@ -351,7 +351,7 @@ void Lobby::load_maps() {
|
||||
|
||||
this->log.info("Generated objects list (%zu entries):", this->map->objects.size());
|
||||
for (size_t z = 0; z < this->map->objects.size(); z++) {
|
||||
string o_str = this->map->objects[z].str(s->item_name_index);
|
||||
string o_str = this->map->objects[z].str();
|
||||
this->log.info("(K-%zX) %s", z, o_str.c_str());
|
||||
}
|
||||
this->log.info("Generated enemies list (%zu entries):", this->map->enemies.size());
|
||||
@@ -483,7 +483,16 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
|
||||
}
|
||||
}
|
||||
|
||||
this->assign_inventory_and_bank_item_ids(c);
|
||||
// If this is not a game or the joining client is the leader, they will assign
|
||||
// their item IDs BEFORE they process any inbound commands (therefore a 6x6D
|
||||
// command, which we will send during loading, should reflect the item state
|
||||
// AFTER their IDs are assigned). If the joining client is not the leader,
|
||||
// they will not assign their item IDs until they receive a 6x71 command,
|
||||
// which is sent AFTER the 6x6D command, so the 6x6D should reflect the item
|
||||
// state BEFORE their IDs are assigned. (In the latter case, we'll assign the
|
||||
// IDs for real when they send a 6F command, or 6x1F equivalent in the case of
|
||||
// DC NTE and 11/2000.)
|
||||
this->assign_inventory_and_bank_item_ids(c, (!this->is_game() || (c->lobby_client_id == this->leader_id)));
|
||||
|
||||
// On BB, we send artificial flag state to fix an Episode 2 bug where the
|
||||
// CCA door lock state is overwritten by quests.
|
||||
@@ -710,17 +719,22 @@ void Lobby::on_item_id_generated_externally(uint32_t item_id) {
|
||||
}
|
||||
}
|
||||
|
||||
void Lobby::assign_inventory_and_bank_item_ids(shared_ptr<Client> c) {
|
||||
void Lobby::assign_inventory_and_bank_item_ids(shared_ptr<Client> c, bool consume_ids) {
|
||||
auto p = c->character();
|
||||
uint32_t orig_next_item_id = this->next_item_id_for_client.at(c->lobby_client_id);
|
||||
for (size_t z = 0; z < p->inventory.num_items; z++) {
|
||||
p->inventory.items[z].data.id = this->generate_item_id(c->lobby_client_id);
|
||||
}
|
||||
if (c->log.info("Assigned inventory item IDs")) {
|
||||
p->print_inventory(stderr, c->version(), c->require_server_state()->item_name_index);
|
||||
if (!consume_ids) {
|
||||
this->next_item_id_for_client[c->lobby_client_id] = orig_next_item_id;
|
||||
}
|
||||
|
||||
if (c->log.info("Assigned inventory item IDs%s", consume_ids ? "" : " but did not mark IDs as used")) {
|
||||
c->print_inventory(stderr);
|
||||
if (p->bank.num_items) {
|
||||
p->bank.assign_ids(0x99000000 + (c->lobby_client_id << 20));
|
||||
c->log.info("Assigned bank item IDs");
|
||||
p->print_bank(stderr, c->version(), c->require_server_state()->item_name_index);
|
||||
c->print_bank(stderr);
|
||||
} else {
|
||||
c->log.info("Bank is empty");
|
||||
}
|
||||
|
||||
+1
-1
@@ -237,7 +237,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
|
||||
uint32_t generate_item_id(uint8_t client_id);
|
||||
void on_item_id_generated_externally(uint32_t item_id);
|
||||
void assign_inventory_and_bank_item_ids(std::shared_ptr<Client> c);
|
||||
void assign_inventory_and_bank_item_ids(std::shared_ptr<Client> c, bool consume_ids);
|
||||
|
||||
QuestIndex::IncludeCondition quest_include_condition() const;
|
||||
|
||||
|
||||
+159
-121
@@ -43,8 +43,7 @@
|
||||
#include "ServerState.hh"
|
||||
#include "StaticGameData.hh"
|
||||
#include "Text.hh"
|
||||
#include "TextArchive.hh"
|
||||
#include "UnicodeTextSet.hh"
|
||||
#include "TextIndex.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -955,7 +954,7 @@ Action a_decode_gci(
|
||||
auto decoded = decode_gci_data(read_input_data(args), num_threads, dec_seed, skip_checksum);
|
||||
save_file(input_filename + ".dec", decoded);
|
||||
});
|
||||
Action a_decode_vmg(
|
||||
Action a_decode_vms(
|
||||
"decode-vms", nullptr, +[](Arguments& args) {
|
||||
string input_filename = args.get<string>(1, false);
|
||||
if (input_filename.empty() || (input_filename == "-")) {
|
||||
@@ -1171,25 +1170,35 @@ Action a_decode_sjis(
|
||||
});
|
||||
|
||||
Action a_decode_text_archive(
|
||||
"decode-text-archive", nullptr, +[](Arguments& args) {
|
||||
"decode-text-archive", "\
|
||||
decode-text-archive [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
|
||||
Decode a text archive to JSON. --collections=NUM_COLLECTIONS is given,\n\
|
||||
expects a fixed number of collections in the input. If --has-pr3 is given,\n\
|
||||
expects the input not to have a REL footer.\n",
|
||||
+[](Arguments& args) {
|
||||
string data = read_input_data(args);
|
||||
TextArchive a(data, args.get<bool>("big-endian"));
|
||||
JSON j = a.json();
|
||||
|
||||
unique_ptr<TextSet> ts;
|
||||
size_t collection_count = args.get<size_t>("collections", 0);
|
||||
if (collection_count) {
|
||||
ts = make_unique<BinaryTextSet>(data, collection_count, !args.get<bool>("has-pr3"));
|
||||
} else {
|
||||
ts = make_unique<BinaryTextAndKeyboardsSet>(data, args.get<bool>("big-endian"));
|
||||
}
|
||||
JSON j = ts->json();
|
||||
string out_data = j.serialize(JSON::SerializeOption::FORMAT);
|
||||
write_output_data(args, out_data.data(), out_data.size(), "json");
|
||||
});
|
||||
Action a_encode_text_archive(
|
||||
"encode-text-archive", "\
|
||||
decode-text-archive [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
|
||||
encode-text-archive [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
|
||||
Decode a text archive (e.g. TextEnglish.pr2) to JSON for easy editing, or\n\
|
||||
encode a JSON file to a text archive.\n",
|
||||
Encode a text archive. Currently only supports GC and Xbox format.\n",
|
||||
+[](Arguments& args) {
|
||||
const string& input_filename = args.get<string>(1, false);
|
||||
const string& output_filename = args.get<string>(2, false);
|
||||
|
||||
auto json = JSON::parse(read_input_data(args));
|
||||
TextArchive a(json);
|
||||
BinaryTextAndKeyboardsSet a(json);
|
||||
auto result = a.serialize(args.get<bool>("big-endian"));
|
||||
if (output_filename.empty()) {
|
||||
if (input_filename.empty() || (input_filename == "-")) {
|
||||
@@ -1214,14 +1223,8 @@ Action a_encode_text_archive(
|
||||
|
||||
Action a_decode_unicode_text_set(
|
||||
"decode-unicode-text-set", nullptr, +[](Arguments& args) {
|
||||
auto collections = parse_unicode_text_set(read_input_data(args));
|
||||
JSON j = JSON::list();
|
||||
for (const auto& collection : collections) {
|
||||
JSON& coll_j = j.emplace_back(JSON::list());
|
||||
for (const auto& s : collection) {
|
||||
coll_j.emplace_back(s);
|
||||
}
|
||||
}
|
||||
UnicodeTextSet uts(read_input_data(args));
|
||||
JSON j = uts.json();
|
||||
string out_data = j.serialize(JSON::SerializeOption::FORMAT);
|
||||
write_output_data(args, out_data.data(), out_data.size(), "json");
|
||||
});
|
||||
@@ -1232,15 +1235,8 @@ Action a_encode_unicode_text_set(
|
||||
Decode a Unicode text set (e.g. unitxt_e.prs) to JSON for easy editing, or\n\
|
||||
encode a JSON file to a Unicode text set.\n",
|
||||
+[](Arguments& args) {
|
||||
auto json = JSON::parse(read_input_data(args));
|
||||
vector<vector<string>> collections;
|
||||
for (const auto& coll_json : json.as_list()) {
|
||||
auto& collection = collections.emplace_back();
|
||||
for (const auto& s_json : coll_json->as_list()) {
|
||||
collection.emplace_back(std::move(s_json->as_string()));
|
||||
}
|
||||
}
|
||||
string encoded = serialize_unicode_text_set(collections);
|
||||
UnicodeTextSet uts(JSON::parse(read_input_data(args)));
|
||||
string encoded = uts.serialize();
|
||||
write_output_data(args, encoded.data(), encoded.size(), "prs");
|
||||
});
|
||||
|
||||
@@ -1256,30 +1252,22 @@ Action a_decode_word_select_set(
|
||||
auto version = get_cli_version(args);
|
||||
|
||||
string unitxt_filename = args.get<string>("unitxt");
|
||||
vector<string> unitxt_collection;
|
||||
const vector<string>* unitxt_collection;
|
||||
if (!unitxt_filename.empty()) {
|
||||
vector<vector<string>> unitxt_data;
|
||||
unique_ptr<UnicodeTextSet> uts;
|
||||
if (ends_with(unitxt_filename, ".prs")) {
|
||||
unitxt_data = parse_unicode_text_set(load_file(unitxt_filename));
|
||||
uts = make_unique<UnicodeTextSet>(load_file(unitxt_filename));
|
||||
} else if (ends_with(unitxt_filename, ".json")) {
|
||||
auto json = JSON::parse(load_file(unitxt_filename));
|
||||
for (const auto& coll_it : json.as_list()) {
|
||||
auto& coll = unitxt_data.emplace_back();
|
||||
for (const auto& str_it : coll_it->as_list()) {
|
||||
coll.emplace_back(str_it->as_string());
|
||||
}
|
||||
}
|
||||
uts = make_unique<UnicodeTextSet>(JSON::parse(load_file(unitxt_filename)));
|
||||
} else {
|
||||
throw runtime_error("unitxt filename must end in .prs or .json");
|
||||
}
|
||||
if (version == Version::BB_V4) {
|
||||
unitxt_collection = std::move(unitxt_data.at(0));
|
||||
} else {
|
||||
unitxt_collection = std::move(unitxt_data.at(35));
|
||||
}
|
||||
unitxt_collection = &uts->get((version == Version::BB_V4) ? 0 : 35);
|
||||
} else {
|
||||
unitxt_collection = nullptr;
|
||||
}
|
||||
|
||||
WordSelectSet ws(read_input_data(args), version, &unitxt_collection, args.get<bool>("japanese"));
|
||||
WordSelectSet ws(read_input_data(args), version, unitxt_collection, args.get<bool>("japanese"));
|
||||
ws.print(stdout);
|
||||
});
|
||||
Action a_print_word_select_table(
|
||||
@@ -1289,17 +1277,18 @@ Action a_print_word_select_table(
|
||||
given, prints the table sorted by token ID for that version. If no version\n\
|
||||
option is given, prints the token table sorted by canonical name.\n",
|
||||
+[](Arguments& args) {
|
||||
auto table = ServerState::load_word_select_table_from_system();
|
||||
Version v = Version::UNKNOWN;
|
||||
ServerState s;
|
||||
s.load_objects_and_upstream_dependents("word_select_table");
|
||||
Version v;
|
||||
try {
|
||||
v = get_cli_version(args);
|
||||
} catch (const runtime_error&) {
|
||||
v = Version::UNKNOWN;
|
||||
}
|
||||
|
||||
if (v != Version::UNKNOWN) {
|
||||
table->print_index(stdout, v);
|
||||
s.word_select_table->print_index(stdout, v);
|
||||
} else {
|
||||
table->print(stdout);
|
||||
s.word_select_table->print(stdout);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1343,21 +1332,20 @@ Action a_convert_rare_item_set(
|
||||
.afs (PSO V2 little-endian AFS archive)\n\
|
||||
.rel (Schtserv rare table; cannot be used in output filename)\n",
|
||||
+[](Arguments& args) {
|
||||
auto name_index = make_shared<ItemNameIndex>(
|
||||
JSON::parse(load_file("system/item-tables/names-v2.json")),
|
||||
JSON::parse(load_file("system/item-tables/names-v3.json")),
|
||||
JSON::parse(load_file("system/item-tables/names-v4.json")));
|
||||
auto version = get_cli_version(args);
|
||||
|
||||
ServerState s;
|
||||
s.load_objects_and_upstream_dependents("item_name_indexes");
|
||||
|
||||
string input_filename = args.get<string>(1, false);
|
||||
if (input_filename.empty() || (input_filename == "-")) {
|
||||
throw runtime_error("input filename must be given");
|
||||
}
|
||||
auto version = get_cli_version(args);
|
||||
|
||||
auto data = make_shared<string>(read_input_data(args));
|
||||
shared_ptr<RareItemSet> rs;
|
||||
if (ends_with(input_filename, ".json")) {
|
||||
rs = make_shared<RareItemSet>(JSON::parse(*data), version, name_index);
|
||||
rs = make_shared<RareItemSet>(JSON::parse(*data), s.item_name_index(version));
|
||||
} else if (ends_with(input_filename, ".gsl")) {
|
||||
rs = make_shared<RareItemSet>(GSLArchive(data, false), false);
|
||||
} else if (ends_with(input_filename, ".gslb")) {
|
||||
@@ -1372,9 +1360,9 @@ Action a_convert_rare_item_set(
|
||||
|
||||
string output_filename = args.get<string>(2, false);
|
||||
if (output_filename.empty() || (output_filename == "-")) {
|
||||
rs->print_all_collections(stdout, version, name_index);
|
||||
rs->print_all_collections(stdout, s.item_name_index(version));
|
||||
} else if (ends_with(output_filename, ".json")) {
|
||||
string data = rs->serialize_json(version, name_index);
|
||||
string data = rs->serialize_json(s.item_name_index(version));
|
||||
write_output_data(args, data.data(), data.size(), nullptr);
|
||||
} else if (ends_with(output_filename, ".gsl")) {
|
||||
string data = rs->serialize_gsl(args.get<bool>("big-endian"));
|
||||
@@ -1401,20 +1389,13 @@ Action a_describe_item(
|
||||
string description = args.get<string>(1);
|
||||
auto version = get_cli_version(args);
|
||||
|
||||
auto name_index = make_shared<ItemNameIndex>(
|
||||
JSON::parse(load_file("system/item-tables/names-v2.json")),
|
||||
JSON::parse(load_file("system/item-tables/names-v3.json")),
|
||||
JSON::parse(load_file("system/item-tables/names-v4.json")));
|
||||
auto pmt_data_v2 = make_shared<string>(prs_decompress(load_file("system/item-tables/ItemPMT-v2.prs")));
|
||||
auto pmt_v2 = make_shared<ItemParameterTable>(pmt_data_v2, ItemParameterTable::Version::V2);
|
||||
auto pmt_data_v3 = make_shared<string>(prs_decompress(load_file("system/item-tables/ItemPMT-gc.prs")));
|
||||
auto pmt_v3 = make_shared<ItemParameterTable>(pmt_data_v3, ItemParameterTable::Version::V3);
|
||||
auto pmt_data_v4 = make_shared<string>(prs_decompress(load_file("system/item-tables/ItemPMT-bb.prs")));
|
||||
auto pmt_v4 = make_shared<ItemParameterTable>(pmt_data_v4, ItemParameterTable::Version::V4);
|
||||
ServerState s;
|
||||
s.load_objects_and_upstream_dependents("item_name_indexes");
|
||||
auto name_index = s.item_name_index(version);
|
||||
|
||||
ItemData item = name_index->parse_item_description(version, description);
|
||||
ItemData item = name_index->parse_item_description(description);
|
||||
|
||||
string desc = name_index->describe_item(version, item);
|
||||
string desc = name_index->describe_item(item);
|
||||
log_info("Data (decoded): %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX -------- %02hhX%02hhX%02hhX%02hhX",
|
||||
item.data1[0], item.data1[1], item.data1[2], item.data1[3],
|
||||
item.data1[4], item.data1[5], item.data1[6], item.data1[7],
|
||||
@@ -1422,7 +1403,7 @@ Action a_describe_item(
|
||||
item.data2[0], item.data2[1], item.data2[2], item.data2[3]);
|
||||
|
||||
ItemData item_v1 = item;
|
||||
item_v1.encode_for_version(Version::PC_V2, pmt_v2);
|
||||
item_v1.encode_for_version(Version::PC_V2, s.item_parameter_table(Version::PC_V2));
|
||||
ItemData item_v1_decoded = item_v1;
|
||||
item_v1_decoded.decode_for_version(Version::PC_V2);
|
||||
|
||||
@@ -1441,7 +1422,7 @@ Action a_describe_item(
|
||||
}
|
||||
|
||||
ItemData item_gc = item;
|
||||
item_gc.encode_for_version(Version::GC_V3, pmt_v3);
|
||||
item_gc.encode_for_version(Version::GC_V3, s.item_parameter_table(Version::GC_V3));
|
||||
ItemData item_gc_decoded = item_gc;
|
||||
item_gc_decoded.decode_for_version(Version::GC_V3);
|
||||
|
||||
@@ -1461,11 +1442,49 @@ Action a_describe_item(
|
||||
|
||||
log_info("Description: %s", desc.c_str());
|
||||
|
||||
size_t purchase_price = pmt_v4->price_for_item(item);
|
||||
size_t purchase_price = s.item_parameter_table(Version::BB_V4)->price_for_item(item);
|
||||
size_t sale_price = purchase_price >> 3;
|
||||
log_info("Purchase price: %zu; sale price: %zu", purchase_price, sale_price);
|
||||
});
|
||||
|
||||
Action a_name_all_items(
|
||||
"name-all-items", nullptr, +[](Arguments&) {
|
||||
ServerState s;
|
||||
s.load_objects_and_upstream_dependents("item_name_indexes");
|
||||
|
||||
set<uint32_t> all_primary_identifiers;
|
||||
for (const auto& index : s.item_name_indexes) {
|
||||
if (index) {
|
||||
for (const auto& it : index->all_by_primary_identifier()) {
|
||||
all_primary_identifiers.emplace(it.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "IDENT :");
|
||||
for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) {
|
||||
Version version = static_cast<Version>(v_s);
|
||||
const auto& index = s.item_name_indexes.at(v_s);
|
||||
if (index) {
|
||||
fprintf(stderr, " %30s", name_for_enum(version));
|
||||
}
|
||||
}
|
||||
fputc('\n', stderr);
|
||||
|
||||
for (uint32_t primary_identifier : all_primary_identifiers) {
|
||||
fprintf(stderr, "%06" PRIX32 ":", primary_identifier);
|
||||
for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) {
|
||||
const auto& index = s.item_name_indexes.at(v_s);
|
||||
if (index) {
|
||||
ItemData item(static_cast<uint64_t>(primary_identifier) << 40);
|
||||
string name = index->describe_item(item);
|
||||
fprintf(stderr, " %30s", name.c_str());
|
||||
}
|
||||
}
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
});
|
||||
|
||||
Action a_show_ep3_cards(
|
||||
"show-ep3-cards", "\
|
||||
show-ep3-cards\n\
|
||||
@@ -1474,24 +1493,20 @@ Action a_show_ep3_cards(
|
||||
+[](Arguments& args) {
|
||||
bool one_line = args.get<bool>("one-line");
|
||||
|
||||
Episode3::CardIndex card_index(
|
||||
"system/ep3/card-definitions.mnr",
|
||||
"system/ep3/card-definitions.mnrd",
|
||||
"system/ep3/card-text.mnr",
|
||||
"system/ep3/card-text.mnrd",
|
||||
"system/ep3/card-dice-text.mnr",
|
||||
"system/ep3/card-dice-text.mnrd");
|
||||
unique_ptr<TextArchive> text_english;
|
||||
ServerState s;
|
||||
s.load_objects_and_upstream_dependents("ep3_data");
|
||||
|
||||
unique_ptr<BinaryTextSet> text_english;
|
||||
try {
|
||||
JSON json = JSON::parse(load_file("system/ep3/text-english.json"));
|
||||
text_english = make_unique<TextArchive>(json);
|
||||
text_english = make_unique<BinaryTextSet>(json);
|
||||
} catch (const exception& e) {
|
||||
}
|
||||
|
||||
auto card_ids = card_index.all_ids();
|
||||
auto card_ids = s.ep3_card_index->all_ids();
|
||||
log_info("%zu card definitions", card_ids.size());
|
||||
for (uint32_t card_id : card_ids) {
|
||||
auto entry = card_index.definition_for_id(card_id);
|
||||
auto entry = s.ep3_card_index->definition_for_id(card_id);
|
||||
string s = entry->def.str(one_line, text_english.get());
|
||||
if (one_line) {
|
||||
fprintf(stdout, "%s\n", s.c_str());
|
||||
@@ -1525,18 +1540,14 @@ Action a_generate_ep3_cards_html(
|
||||
+[](Arguments& args) {
|
||||
size_t num_threads = args.get<size_t>("threads", 0);
|
||||
|
||||
Episode3::CardIndex card_index(
|
||||
"system/ep3/card-definitions.mnr",
|
||||
"system/ep3/card-definitions.mnrd",
|
||||
"system/ep3/card-text.mnr",
|
||||
"system/ep3/card-text.mnrd",
|
||||
"system/ep3/card-dice-text.mnr",
|
||||
"system/ep3/card-dice-text.mnrd");
|
||||
unique_ptr<TextArchive> text_english;
|
||||
ServerState s;
|
||||
s.load_objects_and_upstream_dependents("ep3_data");
|
||||
s.load_objects_and_upstream_dependents("text_index");
|
||||
|
||||
shared_ptr<const TextSet> text_english;
|
||||
try {
|
||||
JSON json = JSON::parse(load_file("system/ep3/text-english.json"));
|
||||
text_english = make_unique<TextArchive>(json);
|
||||
} catch (const exception& e) {
|
||||
text_english = s.text_index->get(Version::GC_EP3, 1);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
struct CardInfo {
|
||||
@@ -1553,11 +1564,11 @@ Action a_generate_ep3_cards_html(
|
||||
}
|
||||
};
|
||||
vector<CardInfo> infos;
|
||||
for (uint32_t card_id : card_index.all_ids()) {
|
||||
for (uint32_t card_id : s.ep3_card_index->all_ids()) {
|
||||
if (infos.size() <= card_id) {
|
||||
infos.resize(card_id + 1);
|
||||
}
|
||||
infos[card_id].ce = card_index.definition_for_id(card_id);
|
||||
infos[card_id].ce = s.ep3_card_index->definition_for_id(card_id);
|
||||
}
|
||||
for (const auto& filename : list_directory_sorted("system/ep3/cardtex")) {
|
||||
if ((filename[0] == 'C' || filename[0] == 'M' || filename[0] == 'L') && (filename[1] == '_')) {
|
||||
@@ -1665,20 +1676,21 @@ Action a_show_ep3_maps(
|
||||
human-readable format.\n",
|
||||
+[](Arguments&) {
|
||||
config_log.info("Collecting Episode 3 data");
|
||||
Episode3::MapIndex map_index("system/ep3/maps");
|
||||
Episode3::CardIndex card_index("system/ep3/card-definitions.mnr", "system/ep3/card-definitions.mnrd");
|
||||
|
||||
auto map_ids = map_index.all_numbers();
|
||||
ServerState s;
|
||||
s.load_objects_and_upstream_dependents("ep3_data");
|
||||
|
||||
auto map_ids = s.ep3_map_index->all_numbers();
|
||||
log_info("%zu maps", map_ids.size());
|
||||
for (uint32_t map_id : map_ids) {
|
||||
auto map = map_index.for_number(map_id);
|
||||
auto map = s.ep3_map_index->for_number(map_id);
|
||||
const auto& vms = map->all_versions();
|
||||
for (size_t language = 0; language < vms.size(); language++) {
|
||||
if (!vms[language]) {
|
||||
continue;
|
||||
}
|
||||
string s = vms[language]->map->str(&card_index, language);
|
||||
fprintf(stdout, "(%c) %s\n", char_for_language_code(language), s.c_str());
|
||||
string map_s = vms[language]->map->str(s.ep3_card_index.get(), language);
|
||||
fprintf(stdout, "(%c) %s\n", char_for_language_code(language), map_s.c_str());
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1689,26 +1701,21 @@ Action a_show_battle_params(
|
||||
Print the Blue Burst battle parameters from the system/blueburst directory\n\
|
||||
in a human-readable format.\n",
|
||||
+[](Arguments&) {
|
||||
BattleParamsIndex index(
|
||||
make_shared<string>(load_file("system/blueburst/BattleParamEntry_on.dat")),
|
||||
make_shared<string>(load_file("system/blueburst/BattleParamEntry_lab_on.dat")),
|
||||
make_shared<string>(load_file("system/blueburst/BattleParamEntry_ep4_on.dat")),
|
||||
make_shared<string>(load_file("system/blueburst/BattleParamEntry.dat")),
|
||||
make_shared<string>(load_file("system/blueburst/BattleParamEntry_lab.dat")),
|
||||
make_shared<string>(load_file("system/blueburst/BattleParamEntry_ep4.dat")));
|
||||
ServerState s;
|
||||
s.load_objects_and_upstream_dependents("battle_params");
|
||||
|
||||
fprintf(stdout, "Episode 1 multi\n");
|
||||
index.get_table(false, Episode::EP1).print(stdout);
|
||||
s.battle_params->get_table(false, Episode::EP1).print(stdout);
|
||||
fprintf(stdout, "Episode 1 solo\n");
|
||||
index.get_table(true, Episode::EP1).print(stdout);
|
||||
s.battle_params->get_table(true, Episode::EP1).print(stdout);
|
||||
fprintf(stdout, "Episode 2 multi\n");
|
||||
index.get_table(false, Episode::EP2).print(stdout);
|
||||
s.battle_params->get_table(false, Episode::EP2).print(stdout);
|
||||
fprintf(stdout, "Episode 2 solo\n");
|
||||
index.get_table(true, Episode::EP2).print(stdout);
|
||||
s.battle_params->get_table(true, Episode::EP2).print(stdout);
|
||||
fprintf(stdout, "Episode 4 multi\n");
|
||||
index.get_table(false, Episode::EP4).print(stdout);
|
||||
s.battle_params->get_table(false, Episode::EP4).print(stdout);
|
||||
fprintf(stdout, "Episode 4 solo\n");
|
||||
index.get_table(true, Episode::EP4).print(stdout);
|
||||
s.battle_params->get_table(true, Episode::EP4).print(stdout);
|
||||
});
|
||||
|
||||
Action a_parse_object_graph(
|
||||
@@ -1827,6 +1834,33 @@ Action a_diff_dol_files(
|
||||
}
|
||||
});
|
||||
|
||||
Action a_replay_ep3_battle_commands(
|
||||
"replay-ep3-battle-commands", nullptr, +[](Arguments& args) {
|
||||
ServerState s;
|
||||
s.load_objects_and_upstream_dependents("ep3_data");
|
||||
|
||||
auto random_crypt = make_shared<PSOV2Encryption>(args.get<uint32_t>("seed", 0, Arguments::IntFormat::HEX));
|
||||
Episode3::Server::Options options = {
|
||||
.card_index = s.ep3_card_index,
|
||||
.map_index = s.ep3_map_index,
|
||||
.behavior_flags = 0x0092,
|
||||
.random_crypt = random_crypt,
|
||||
.tournament = nullptr,
|
||||
.trap_card_ids = {},
|
||||
};
|
||||
auto server = make_shared<Episode3::Server>(nullptr, std::move(options));
|
||||
server->init();
|
||||
|
||||
auto input = read_input_data(args);
|
||||
auto lines = split(input, '\n');
|
||||
for (const auto& line : lines) {
|
||||
string data = parse_data_string(line);
|
||||
if (!data.empty()) {
|
||||
server->on_server_data_input(nullptr, data);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Action a_run_server_replay_log(
|
||||
"", nullptr, +[](Arguments& args) {
|
||||
if (!isdir("system/players")) {
|
||||
@@ -1852,13 +1886,17 @@ Action a_run_server_replay_log(
|
||||
|
||||
shared_ptr<struct event_base> base(event_base_new(), event_base_free);
|
||||
auto state = make_shared<ServerState>(base, config_filename, is_replay);
|
||||
state->init();
|
||||
state->load_objects_and_downstream_dependents("all");
|
||||
|
||||
shared_ptr<DNSServer> dns_server;
|
||||
if (state->dns_server_port && !is_replay) {
|
||||
config_log.info("Starting DNS server on port %hu", state->dns_server_port);
|
||||
if (!state->dns_server_addr.empty()) {
|
||||
config_log.info("Starting DNS server on %s:%hu", state->dns_server_addr.c_str(), state->dns_server_port);
|
||||
} else {
|
||||
config_log.info("Starting DNS server on port %hu", state->dns_server_port);
|
||||
}
|
||||
dns_server = make_shared<DNSServer>(base, state->local_address, state->external_address);
|
||||
dns_server->listen("", state->dns_server_port);
|
||||
dns_server->listen(state->dns_server_addr, state->dns_server_port);
|
||||
} else {
|
||||
config_log.info("DNS server is disabled");
|
||||
}
|
||||
@@ -1900,14 +1938,14 @@ Action a_run_server_replay_log(
|
||||
auto [ss, size] = make_sockaddr_storage(
|
||||
state->proxy_destination_patch.first,
|
||||
state->proxy_destination_patch.second);
|
||||
state->proxy_server->listen(pc->port, pc->version, &ss);
|
||||
state->proxy_server->listen(pc->addr, pc->port, pc->version, &ss);
|
||||
} else if (is_v4(pc->version)) {
|
||||
auto [ss, size] = make_sockaddr_storage(
|
||||
state->proxy_destination_bb.first,
|
||||
state->proxy_destination_bb.second);
|
||||
state->proxy_server->listen(pc->port, pc->version, &ss);
|
||||
state->proxy_server->listen(pc->addr, pc->port, pc->version, &ss);
|
||||
} else {
|
||||
state->proxy_server->listen(pc->port, pc->version);
|
||||
state->proxy_server->listen(pc->addr, pc->port, pc->version);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1916,7 +1954,7 @@ Action a_run_server_replay_log(
|
||||
state->game_server = make_shared<Server>(base, state);
|
||||
}
|
||||
string spec = string_printf("T-%hu-%s-%s-%s", pc->port, name_for_enum(pc->version), pc->name.c_str(), name_for_enum(pc->behavior));
|
||||
state->game_server->listen(spec, "", pc->port, pc->version, pc->behavior);
|
||||
state->game_server->listen(spec, pc->addr, pc->port, pc->version, pc->behavior);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-10
@@ -113,17 +113,10 @@ string Map::Enemy::str() const {
|
||||
this->source_index, name_for_enum(this->type), this->floor, this->state_flags, this->last_hit_by_client_id);
|
||||
}
|
||||
|
||||
string Map::Object::str(shared_ptr<const ItemNameIndex> name_index) const {
|
||||
string Map::Object::str() const {
|
||||
if (this->param1 <= 0.0f) {
|
||||
string item_name;
|
||||
try {
|
||||
auto item = ItemCreator::base_item_for_specialized_box(this->param4, this->param5, this->param6);
|
||||
item_name = name_index ? name_index->describe_item(Version::BB_V4, item) : item.hex();
|
||||
} catch (const exception& e) {
|
||||
item_name = string_printf("(failed: %s)", e.what());
|
||||
}
|
||||
return string_printf("[Map::Object source %zX %04hX @%04hX p1=%g (specialized: %s) floor=%02hhX item_drop_checked=%s]",
|
||||
this->source_index, this->base_type, this->section, this->param1, item_name.c_str(), this->floor, this->item_drop_checked ? "true" : "false");
|
||||
return string_printf("[Map::Object source %zX %04hX @%04hX p1=%g (specialized: %08" PRIX32 " %08" PRIX32 " %08" PRIX32 ") floor=%02hhX item_drop_checked=%s]",
|
||||
this->source_index, this->base_type, this->section, this->param1, this->param4, this->param5, this->param6, this->floor, this->item_drop_checked ? "true" : "false");
|
||||
} else {
|
||||
return string_printf("[Map::Object source %zX %04hX @%04hX p1=%g (generic) p456=[%08" PRIX32 " %08" PRIX32 " %08" PRIX32 "] floor=%02hhX item_drop_checked=%s]",
|
||||
this->source_index, this->base_type, this->section, this->param1, this->param4, this->param5, this->param6,
|
||||
|
||||
+1
-2
@@ -10,7 +10,6 @@
|
||||
#include <vector>
|
||||
|
||||
#include "BattleParamsIndex.hh"
|
||||
#include "ItemNameIndex.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
#include "StaticGameData.hh"
|
||||
#include "Text.hh"
|
||||
@@ -217,7 +216,7 @@ struct Map {
|
||||
uint8_t floor;
|
||||
bool item_drop_checked;
|
||||
|
||||
std::string str(std::shared_ptr<const ItemNameIndex> name_index) const;
|
||||
std::string str() const;
|
||||
};
|
||||
|
||||
struct Enemy {
|
||||
|
||||
@@ -450,7 +450,7 @@ PlayerRecordsBB_Challenge::operator PlayerRecordsV3_Challenge<false>() const {
|
||||
return ret;
|
||||
}
|
||||
|
||||
void PlayerBank::add_item(const ItemData& item) {
|
||||
void PlayerBank::add_item(const ItemData& item, Version version) {
|
||||
uint32_t pid = item.primary_identifier();
|
||||
|
||||
if (pid == MESETA_IDENTIFIER) {
|
||||
@@ -461,7 +461,7 @@ void PlayerBank::add_item(const ItemData& item) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t combine_max = item.max_stack_size();
|
||||
size_t combine_max = item.max_stack_size(version);
|
||||
if (combine_max > 1) {
|
||||
size_t y;
|
||||
for (y = 0; y < this->num_items; y++) {
|
||||
@@ -486,17 +486,17 @@ void PlayerBank::add_item(const ItemData& item) {
|
||||
}
|
||||
auto& last_item = this->items[this->num_items];
|
||||
last_item.data = item;
|
||||
last_item.amount = (item.max_stack_size() > 1) ? item.data1[5] : 1;
|
||||
last_item.amount = (item.max_stack_size(version) > 1) ? item.data1[5] : 1;
|
||||
last_item.present = 1;
|
||||
this->num_items++;
|
||||
}
|
||||
|
||||
ItemData PlayerBank::remove_item(uint32_t item_id, uint32_t amount) {
|
||||
ItemData PlayerBank::remove_item(uint32_t item_id, uint32_t amount, Version version) {
|
||||
size_t index = this->find_item(item_id);
|
||||
auto& bank_item = this->items[index];
|
||||
|
||||
ItemData ret;
|
||||
if (amount && (bank_item.data.stack_size() > 1) && (amount < bank_item.data.data1[5])) {
|
||||
if (amount && (bank_item.data.stack_size(version) > 1) && (amount < bank_item.data.data1[5])) {
|
||||
ret = bank_item.data;
|
||||
ret.data1[5] = amount;
|
||||
bank_item.data.data1[5] -= amount;
|
||||
@@ -678,7 +678,7 @@ void PlayerInventory::encode_for_client(shared_ptr<Client> c) {
|
||||
}
|
||||
}
|
||||
|
||||
auto item_parameter_table = c->require_server_state()->item_parameter_table_for_version(c->version());
|
||||
auto item_parameter_table = c->require_server_state()->item_parameter_table(c->version());
|
||||
for (size_t z = 0; z < this->items.size(); z++) {
|
||||
this->items[z].data.encode_for_version(c->version(), item_parameter_table);
|
||||
}
|
||||
|
||||
@@ -100,8 +100,8 @@ struct PlayerBank {
|
||||
/* 0008 */ parray<PlayerBankItem, 200> items;
|
||||
/* 12C8 */
|
||||
|
||||
void add_item(const ItemData& item);
|
||||
ItemData remove_item(uint32_t item_id, uint32_t amount);
|
||||
void add_item(const ItemData& item, Version version);
|
||||
ItemData remove_item(uint32_t item_id, uint32_t amount, Version version);
|
||||
size_t find_item(uint32_t item_id);
|
||||
|
||||
void sort();
|
||||
|
||||
@@ -1783,7 +1783,8 @@ static HandlerResult C_V123_A0_A1(shared_ptr<ProxyServer::LinkedSession> ses, ui
|
||||
}
|
||||
|
||||
// Indexed as [command][version][is_from_client]
|
||||
static on_command_t handlers[0x100][14][2] = {
|
||||
static_assert(NUM_VERSIONS == 14, "Don\'t forget to update the ProxyCommands handlers table");
|
||||
static on_command_t handlers[0x100][NUM_VERSIONS][2] = {
|
||||
// clang-format off
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* 00 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
|
||||
|
||||
+7
-4
@@ -43,20 +43,23 @@ ProxyServer::ProxyServer(
|
||||
state(state),
|
||||
next_unlicensed_session_id(0xFF00000000000001) {}
|
||||
|
||||
void ProxyServer::listen(uint16_t port, Version version, const struct sockaddr_storage* default_destination) {
|
||||
auto socket_obj = make_shared<ListeningSocket>(this, port, version, default_destination);
|
||||
auto l = this->listeners.emplace(port, socket_obj).first->second;
|
||||
void ProxyServer::listen(const std::string& addr, uint16_t port, Version version, const struct sockaddr_storage* default_destination) {
|
||||
auto socket_obj = make_shared<ListeningSocket>(this, addr, port, version, default_destination);
|
||||
if (!this->listeners.emplace(port, socket_obj).second) {
|
||||
throw runtime_error("duplicate port in proxy server configuration");
|
||||
}
|
||||
}
|
||||
|
||||
ProxyServer::ListeningSocket::ListeningSocket(
|
||||
ProxyServer* server,
|
||||
const std::string& addr,
|
||||
uint16_t port,
|
||||
Version version,
|
||||
const struct sockaddr_storage* default_destination)
|
||||
: server(server),
|
||||
log(string_printf("[ProxyServer:ListeningSocket:%hu] ", port), proxy_server_log.min_level),
|
||||
port(port),
|
||||
fd(::listen("", port, SOMAXCONN)),
|
||||
fd(::listen(addr, port, SOMAXCONN)),
|
||||
listener(nullptr, evconnlistener_free),
|
||||
version(version) {
|
||||
if (!this->fd.is_open()) {
|
||||
|
||||
+2
-1
@@ -26,7 +26,7 @@ public:
|
||||
std::shared_ptr<ServerState> state);
|
||||
virtual ~ProxyServer() = default;
|
||||
|
||||
void listen(uint16_t port, Version version, const struct sockaddr_storage* default_destination = nullptr);
|
||||
void listen(const std::string& addr, uint16_t port, Version version, const struct sockaddr_storage* default_destination = nullptr);
|
||||
|
||||
void connect_client(struct bufferevent* bev, uint16_t server_port);
|
||||
|
||||
@@ -207,6 +207,7 @@ private:
|
||||
|
||||
ListeningSocket(
|
||||
ProxyServer* server,
|
||||
const std::string& addr,
|
||||
uint16_t port,
|
||||
Version version,
|
||||
const struct sockaddr_storage* default_destination);
|
||||
|
||||
@@ -494,6 +494,10 @@ QuestIndex::QuestIndex(
|
||||
continue;
|
||||
}
|
||||
for (string filename : list_directory_sorted(cat_path)) {
|
||||
if (filename == ".DS_Store") {
|
||||
continue;
|
||||
}
|
||||
|
||||
string file_path = cat_path + "/" + filename;
|
||||
try {
|
||||
string orig_filename = filename;
|
||||
|
||||
@@ -189,6 +189,8 @@ constexpr uint16_t v_flag(Version v) {
|
||||
|
||||
using Arg = QuestScriptOpcodeDefinition::Argument;
|
||||
|
||||
static_assert(NUM_VERSIONS == 14, "Don\'t forget to update the QuestScript flags and opcode definitions table");
|
||||
|
||||
static constexpr uint16_t F_PASS = 0x0001; // Version::PC_PATCH (unused for quests)
|
||||
static constexpr uint16_t F_ARGS = 0x0002; // Version::BB_PATCH (unused for quests)
|
||||
static constexpr uint16_t F_DC_NTE = 0x0004; // Version::DC_NTE
|
||||
|
||||
+11
-13
@@ -17,7 +17,7 @@ string RareItemSet::ExpandedDrop::str() const {
|
||||
this->probability, frac.first, frac.second, this->item_code[0], this->item_code[1], this->item_code[2]);
|
||||
}
|
||||
|
||||
string RareItemSet::ExpandedDrop::str(Version version, shared_ptr<const ItemNameIndex> name_index) const {
|
||||
string RareItemSet::ExpandedDrop::str(shared_ptr<const ItemNameIndex> name_index) const {
|
||||
ItemData item;
|
||||
item.data1[0] = this->item_code[0];
|
||||
item.data1[1] = this->item_code[1];
|
||||
@@ -25,7 +25,7 @@ string RareItemSet::ExpandedDrop::str(Version version, shared_ptr<const ItemName
|
||||
|
||||
string ret = this->str();
|
||||
ret += " (";
|
||||
ret += name_index->describe_item(version, item);
|
||||
ret += name_index->describe_item(item);
|
||||
ret += ")";
|
||||
return ret;
|
||||
}
|
||||
@@ -303,7 +303,7 @@ RareItemSet::RareItemSet(const string& rel_data, bool is_big_endian) {
|
||||
}
|
||||
}
|
||||
|
||||
RareItemSet::RareItemSet(const JSON& json, Version version, shared_ptr<const ItemNameIndex> name_index) {
|
||||
RareItemSet::RareItemSet(const JSON& json, shared_ptr<const ItemNameIndex> name_index) {
|
||||
for (const auto& mode_it : json.as_dict()) {
|
||||
static const unordered_map<string, GameMode> mode_keys(
|
||||
{{"Normal", GameMode::NORMAL}, {"Battle", GameMode::BATTLE}, {"Challenge", GameMode::CHALLENGE}, {"Solo", GameMode::SOLO}});
|
||||
@@ -369,7 +369,7 @@ RareItemSet::RareItemSet(const JSON& json, Version version, shared_ptr<const Ite
|
||||
if (!name_index) {
|
||||
throw runtime_error("item name index is not available");
|
||||
}
|
||||
ItemData data = name_index->parse_item_description(version, item_desc.as_string());
|
||||
ItemData data = name_index->parse_item_description(item_desc.as_string());
|
||||
d.item_code[0] = data.data1[0];
|
||||
d.item_code[1] = data.data1[1];
|
||||
d.item_code[2] = data.data1[2];
|
||||
@@ -427,7 +427,7 @@ std::string RareItemSet::serialize_gsl(bool big_endian) const {
|
||||
return GSLArchive::generate(files, big_endian);
|
||||
}
|
||||
|
||||
std::string RareItemSet::serialize_json(Version version, shared_ptr<const ItemNameIndex> name_index) const {
|
||||
std::string RareItemSet::serialize_json(shared_ptr<const ItemNameIndex> name_index) const {
|
||||
auto modes_dict = JSON::dict();
|
||||
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
||||
for (const auto& mode : modes) {
|
||||
@@ -458,7 +458,7 @@ std::string RareItemSet::serialize_json(Version version, shared_ptr<const ItemNa
|
||||
data.data1[0] = spec.item_code[0];
|
||||
data.data1[1] = spec.item_code[1];
|
||||
data.data1[2] = spec.item_code[2];
|
||||
spec_json.emplace_back(name_index->describe_item(version, data));
|
||||
spec_json.emplace_back(name_index->describe_item(data));
|
||||
}
|
||||
for (const auto& enemy_type : enemy_types) {
|
||||
if (enemy_type_valid_for_episode(episode, enemy_type)) {
|
||||
@@ -485,7 +485,7 @@ std::string RareItemSet::serialize_json(Version version, shared_ptr<const ItemNa
|
||||
data.data1[0] = spec.item_code[0];
|
||||
data.data1[1] = spec.item_code[1];
|
||||
data.data1[2] = spec.item_code[2];
|
||||
area_list.back().emplace_back(name_index->describe_item(version, data));
|
||||
area_list.back().emplace_back(name_index->describe_item(data));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,7 +512,6 @@ std::string RareItemSet::serialize_json(Version version, shared_ptr<const ItemNa
|
||||
|
||||
void RareItemSet::print_collection(
|
||||
FILE* stream,
|
||||
Version version,
|
||||
GameMode mode,
|
||||
Episode episode,
|
||||
uint8_t difficulty,
|
||||
@@ -544,7 +543,7 @@ void RareItemSet::print_collection(
|
||||
}
|
||||
|
||||
for (const auto& spec : collection->rt_index_to_specs[z]) {
|
||||
string s = name_index ? spec.str(version, name_index) : spec.str();
|
||||
string s = name_index ? spec.str(name_index) : spec.str();
|
||||
fprintf(stream, " %02zX: %s (%s)\n", z, s.c_str(), enemy_types_str.c_str());
|
||||
}
|
||||
}
|
||||
@@ -552,14 +551,13 @@ void RareItemSet::print_collection(
|
||||
fprintf(stream, " Box rares:\n");
|
||||
for (size_t area = 0; area < collection->box_area_to_specs.size(); area++) {
|
||||
for (const auto& spec : collection->box_area_to_specs[area]) {
|
||||
string s = name_index ? spec.str(version, name_index) : spec.str();
|
||||
string s = name_index ? spec.str(name_index) : spec.str();
|
||||
fprintf(stream, " (area %02zX) %s\n", area, s.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RareItemSet::print_all_collections(
|
||||
FILE* stream, Version version, std::shared_ptr<const ItemNameIndex> name_index) const {
|
||||
void RareItemSet::print_all_collections(FILE* stream, std::shared_ptr<const ItemNameIndex> name_index) const {
|
||||
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
||||
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
||||
for (GameMode mode : modes) {
|
||||
@@ -567,7 +565,7 @@ void RareItemSet::print_all_collections(
|
||||
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
|
||||
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
||||
try {
|
||||
this->print_collection(stream, version, mode, episode, difficulty, section_id, name_index);
|
||||
this->print_collection(stream, mode, episode, difficulty, section_id, name_index);
|
||||
} catch (const out_of_range& e) {
|
||||
}
|
||||
}
|
||||
|
||||
+4
-5
@@ -22,14 +22,14 @@ public:
|
||||
parray<uint8_t, 3> item_code;
|
||||
|
||||
std::string str() const;
|
||||
std::string str(Version version, std::shared_ptr<const ItemNameIndex> name_index) const;
|
||||
std::string str(std::shared_ptr<const ItemNameIndex> name_index) const;
|
||||
};
|
||||
|
||||
RareItemSet();
|
||||
RareItemSet(const AFSArchive& afs, bool is_v1);
|
||||
RareItemSet(const GSLArchive& gsl, bool is_big_endian);
|
||||
RareItemSet(const std::string& rel, bool is_big_endian);
|
||||
RareItemSet(const JSON& json, Version version, std::shared_ptr<const ItemNameIndex> name_index = nullptr);
|
||||
RareItemSet(const JSON& json, std::shared_ptr<const ItemNameIndex> name_index = nullptr);
|
||||
~RareItemSet() = default;
|
||||
|
||||
std::vector<ExpandedDrop> get_enemy_specs(GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t rt_index) const;
|
||||
@@ -37,17 +37,16 @@ public:
|
||||
|
||||
std::string serialize_afs(bool is_v1) const;
|
||||
std::string serialize_gsl(bool big_endian) const;
|
||||
std::string serialize_json(Version version, std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
||||
std::string serialize_json(std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
||||
|
||||
void print_collection(
|
||||
FILE* stream,
|
||||
Version version,
|
||||
GameMode mode,
|
||||
Episode episode,
|
||||
uint8_t difficulty,
|
||||
uint8_t section_id,
|
||||
std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
||||
void print_all_collections(FILE* stream, Version version, std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
||||
void print_all_collections(FILE* stream, std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
||||
|
||||
protected:
|
||||
struct SpecCollection {
|
||||
|
||||
+40
-29
@@ -132,7 +132,7 @@ void send_client_to_proxy_server(shared_ptr<Client> c) {
|
||||
|
||||
static void send_proxy_destinations_menu(shared_ptr<Client> c) {
|
||||
auto s = c->require_server_state();
|
||||
send_menu(c, s->proxy_destinations_menu_for_version(c->version()));
|
||||
send_menu(c, s->proxy_destinations_menu(c->version()));
|
||||
}
|
||||
|
||||
static bool send_enable_send_function_call_if_applicable(shared_ptr<Client> c) {
|
||||
@@ -1762,7 +1762,7 @@ static void on_D6_V3(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
check_size_v(data.size(), 0);
|
||||
if (c->config.check_flag(Client::Flag::IN_INFORMATION_MENU)) {
|
||||
auto s = c->require_server_state();
|
||||
send_menu(c, s->information_menu_for_version(c->version()));
|
||||
send_menu(c, s->information_menu(c->version()));
|
||||
} else if (c->config.check_flag(Client::Flag::AT_WELCOME_MESSAGE)) {
|
||||
c->config.clear_flag(Client::Flag::AT_WELCOME_MESSAGE);
|
||||
send_enable_send_function_call_if_applicable(c);
|
||||
@@ -1784,7 +1784,7 @@ static void on_09(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
case MenuID::QUEST_EP1:
|
||||
case MenuID::QUEST_EP2: {
|
||||
bool is_download_quest = !c->lobby.lock();
|
||||
auto quest_index = s->quest_index_for_version(c->version());
|
||||
auto quest_index = s->quest_index(c->version());
|
||||
if (!quest_index) {
|
||||
send_quest_info(c, "$C6Quests are not available.", is_download_quest);
|
||||
} else {
|
||||
@@ -2033,7 +2033,7 @@ static void on_quest_loaded(shared_ptr<Lobby> l) {
|
||||
lc->use_default_bank();
|
||||
lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table);
|
||||
lc->log.info("Created challenge overlay");
|
||||
l->assign_inventory_and_bank_item_ids(lc);
|
||||
l->assign_inventory_and_bank_item_ids(lc, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2178,7 +2178,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
|
||||
case MainMenuItemID::INFORMATION: {
|
||||
auto s = c->require_server_state();
|
||||
send_menu(c, s->information_menu_for_version(c->version()));
|
||||
send_menu(c, s->information_menu(c->version()));
|
||||
c->config.set_flag(Client::Flag::IN_INFORMATION_MENU);
|
||||
break;
|
||||
}
|
||||
@@ -2199,7 +2199,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
// always the download quest menu. (Episode 3 does actually have
|
||||
// online quests, but they're served via a server data request
|
||||
// instead of the file download paradigm that other versions use.)
|
||||
auto quest_index = s->quest_index_for_version(c->version());
|
||||
auto quest_index = s->quest_index(c->version());
|
||||
const auto& categories = quest_index->categories(menu_type, Episode::EP3, c->version());
|
||||
if (categories.size() == 1) {
|
||||
auto quests = quest_index->filter(menu_type, Episode::EP3, c->version(), categories[0]->category_id);
|
||||
@@ -2208,7 +2208,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
}
|
||||
}
|
||||
|
||||
send_quest_categories_menu(c, s->quest_index_for_version(c->version()), menu_type, Episode::NONE);
|
||||
send_quest_categories_menu(c, s->quest_index(c->version()), menu_type, Episode::NONE);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2346,7 +2346,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
auto s = c->require_server_state();
|
||||
const pair<string, uint16_t>* dest = nullptr;
|
||||
try {
|
||||
dest = &s->proxy_destinations_for_version(c->version()).at(item_id);
|
||||
dest = &s->proxy_destinations(c->version()).at(item_id);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
@@ -2453,7 +2453,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
|
||||
case MenuID::QUEST_CATEGORIES: {
|
||||
auto s = c->require_server_state();
|
||||
auto quest_index = s->quest_index_for_version(c->version());
|
||||
auto quest_index = s->quest_index(c->version());
|
||||
if (!quest_index) {
|
||||
send_lobby_message_box(c, "$C6Quests are not available.");
|
||||
break;
|
||||
@@ -2494,7 +2494,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
case MenuID::QUEST_EP1:
|
||||
case MenuID::QUEST_EP2: {
|
||||
auto s = c->require_server_state();
|
||||
auto quest_index = s->quest_index_for_version(c->version());
|
||||
auto quest_index = s->quest_index(c->version());
|
||||
if (!quest_index) {
|
||||
send_lobby_message_box(c, "$C6Quests are not\navailable.");
|
||||
break;
|
||||
@@ -2707,7 +2707,7 @@ static void on_08_E6(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
|
||||
static void on_1F(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
check_size_v(data.size(), 0);
|
||||
auto s = c->require_server_state();
|
||||
send_menu(c, s->information_menu_for_version(c->version()), true);
|
||||
send_menu(c, s->information_menu(c->version()), true);
|
||||
}
|
||||
|
||||
static void on_A0(shared_ptr<Client> c, uint16_t, uint32_t, string&) {
|
||||
@@ -2842,7 +2842,7 @@ static void on_A2(shared_ptr<Client> c, uint16_t, uint32_t flag, string& data) {
|
||||
throw logic_error("invalid game mode");
|
||||
}
|
||||
}
|
||||
send_quest_categories_menu(c, s->quest_index_for_version(c->version()), menu_type, l->episode);
|
||||
send_quest_categories_menu(c, s->quest_index(c->version()), menu_type, l->episode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3620,7 +3620,7 @@ static void on_DF_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
|
||||
lc->use_default_bank();
|
||||
lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table);
|
||||
lc->log.info("Created challenge overlay");
|
||||
l->assign_inventory_and_bank_item_ids(lc);
|
||||
l->assign_inventory_and_bank_item_ids(lc, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3671,7 +3671,7 @@ static void on_DF_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
|
||||
? p->challenge_records.ep2_online_award_state
|
||||
: p->challenge_records.ep1_online_award_state;
|
||||
award_state.rank_award_flags |= cmd.rank_bitmask;
|
||||
p->add_item(cmd.item);
|
||||
p->add_item(cmd.item, c->version());
|
||||
l->on_item_id_generated_externally(cmd.item.id);
|
||||
string desc = s->describe_item(Version::BB_V4, cmd.item, false);
|
||||
l->log.info("(Challenge mode) Item awarded to player %hhu: %s", c->lobby_client_id, desc.c_str());
|
||||
@@ -4361,17 +4361,23 @@ static void on_6F(shared_ptr<Client> c, uint16_t command, uint32_t, string& data
|
||||
if (!l->is_game()) {
|
||||
throw runtime_error("client sent ready command outside of game");
|
||||
}
|
||||
c->config.clear_flag(Client::Flag::LOADING);
|
||||
|
||||
// Episode 3 sends a 6F after a CAx21 (end battle) command, so we shouldn't
|
||||
// reassign the items IDs again in that case (even though item IDs really
|
||||
// don't matter for Ep3)
|
||||
if (c->config.check_flag(Client::Flag::LOADING)) {
|
||||
c->config.clear_flag(Client::Flag::LOADING);
|
||||
|
||||
// The client sends 6F when it has created its TObjPlayer and assigned its
|
||||
// item IDs. For the leader, however, this happens before any inbound commands
|
||||
// are processed, so we already did it when the client was added to the lobby.
|
||||
// So, we only assign item IDs here if the client is not the leader.
|
||||
if ((command == 0x006F) && (c->lobby_client_id != l->leader_id)) {
|
||||
l->assign_inventory_and_bank_item_ids(c, true);
|
||||
}
|
||||
}
|
||||
|
||||
send_server_time(c);
|
||||
if (l->base_version == Version::BB_V4) {
|
||||
send_set_exp_multiplier(l);
|
||||
}
|
||||
if (c->version() == Version::BB_V4) {
|
||||
send_update_team_reward_flags(c);
|
||||
send_all_nearby_team_metadatas_to_client(c, false);
|
||||
}
|
||||
|
||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
string variations_str;
|
||||
for (size_t z = 0; z < l->variations.size(); z++) {
|
||||
@@ -4382,6 +4388,10 @@ static void on_6F(shared_ptr<Client> c, uint16_t command, uint32_t, string& data
|
||||
|
||||
bool should_resume_game = true;
|
||||
if (c->version() == Version::BB_V4) {
|
||||
send_set_exp_multiplier(l);
|
||||
send_update_team_reward_flags(c);
|
||||
send_all_nearby_team_metadatas_to_client(c, false);
|
||||
|
||||
// BB sends 016F when the client is done loading a quest. In that case, we
|
||||
// shouldn't send the quest to them again!
|
||||
if ((command == 0x006F) && l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) {
|
||||
@@ -4533,9 +4543,9 @@ static void on_D2_V3_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data)
|
||||
auto to_p = to_c->character();
|
||||
auto from_p = from_c->character();
|
||||
for (const auto& trade_item : from_c->pending_item_trade->items) {
|
||||
size_t amount = trade_item.stack_size();
|
||||
size_t amount = trade_item.stack_size(from_c->version());
|
||||
|
||||
auto item = from_p->remove_item(trade_item.id, amount, false);
|
||||
auto item = from_p->remove_item(trade_item.id, amount, from_c->version());
|
||||
// This is a special case: when the trade is executed, the client
|
||||
// deletes the traded items from its own inventory automatically, so we
|
||||
// should NOT send the 6x29 to that client; we should only send it to
|
||||
@@ -4547,7 +4557,7 @@ static void on_D2_V3_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data)
|
||||
}
|
||||
}
|
||||
|
||||
to_p->add_item(trade_item);
|
||||
to_p->add_item(trade_item, to_c->version());
|
||||
send_create_inventory_item_to_lobby(to_c, to_c->lobby_client_id, item);
|
||||
}
|
||||
send_command(to_c, 0xD3, 0x00);
|
||||
@@ -4981,7 +4991,7 @@ static void on_EA_BB(shared_ptr<Client> c, uint16_t command, uint32_t flag, stri
|
||||
}
|
||||
}
|
||||
if (!reward.reward_item.empty()) {
|
||||
c->current_bank().add_item(reward.reward_item);
|
||||
c->current_bank().add_item(reward.reward_item, c->version());
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -5206,9 +5216,10 @@ typedef void (*on_command_t)(shared_ptr<Client> c, uint16_t command, uint32_t fl
|
||||
// Command handler table, indexed by command number and game version. Null
|
||||
// entries in this table cause on_unimplemented_command to be called, which
|
||||
// disconnects the client.
|
||||
static on_command_t handlers[0x100][14] = {
|
||||
static_assert(NUM_VERSIONS == 14, "Don\'t forget to update the ReceiveCommands handler table");
|
||||
static on_command_t handlers[0x100][NUM_VERSIONS] = {
|
||||
// clang-format off
|
||||
// PC_PATCH BB_PATCH DC_NTE DC_PROTO DCV1 DCV2 PC-NTE PC GCNTE GC EP3TE EP3 XB BB
|
||||
// PC_PATCH BB_PATCH DC_NTE DC_112000 DCV1 DCV2 PC_NTE PC GCNTE GC EP3TE EP3 XB BB
|
||||
/* 00 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
|
||||
/* 01 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
|
||||
/* 02 */ {on_02_P, on_02_P, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
|
||||
|
||||
+170
-109
@@ -832,7 +832,7 @@ static void on_sync_joining_player_disp_and_inventory(
|
||||
throw logic_error("6x70 command from unknown game version");
|
||||
}
|
||||
|
||||
parsed->transcode_inventory_items(c->version(), target->version(), s->item_parameter_table_for_version(target->version()));
|
||||
parsed->transcode_inventory_items(c->version(), target->version(), s->item_parameter_table(target->version()));
|
||||
parsed->visual.enforce_lobby_join_limits_for_version(target->version());
|
||||
|
||||
switch (target->version()) {
|
||||
@@ -1136,6 +1136,7 @@ static void on_change_floor_6x1F(shared_ptr<Client> c, uint8_t command, uint8_t
|
||||
if (c->config.check_flag(Client::Flag::LOADING)) {
|
||||
c->config.clear_flag(Client::Flag::LOADING);
|
||||
send_resume_game(c->require_lobby(), c);
|
||||
c->require_lobby()->assign_inventory_and_bank_item_ids(c, true);
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -1329,6 +1330,40 @@ void on_movement_with_floor(shared_ptr<Client> c, uint8_t command, uint8_t flag,
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
}
|
||||
|
||||
void on_set_animation_state(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
|
||||
auto& cmd = check_size_t<G_SetAnimationState_6x52>(data, size);
|
||||
if (cmd.header.client_id != c->lobby_client_id) {
|
||||
return;
|
||||
}
|
||||
if (command_is_private(command)) {
|
||||
return;
|
||||
}
|
||||
auto l = c->require_lobby();
|
||||
if (l->is_game()) {
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
return;
|
||||
}
|
||||
|
||||
// The animation numbers were changed on V3. This is the most common one to
|
||||
// see in the lobby (it occurs when a player talks to the counter), so we
|
||||
// take care to translate it specifically.
|
||||
bool c_is_v1_or_v2 = is_v1_or_v2(c->version());
|
||||
if (!((c_is_v1_or_v2 && (cmd.animation == 0x000A)) || (!c_is_v1_or_v2 && (cmd.animation == 0x0000)))) {
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
return;
|
||||
}
|
||||
|
||||
G_SetAnimationState_6x52 other_cmd = cmd;
|
||||
other_cmd.animation = 0x000A - cmd.animation;
|
||||
for (auto lc : l->clients) {
|
||||
if (lc && (lc != c)) {
|
||||
auto& out_cmd = (is_v1_or_v2(lc->version()) != c_is_v1_or_v2) ? other_cmd : cmd;
|
||||
out_cmd.header.subcommand = translate_subcommand_number(lc->version(), Version::BB_V4, 0x52);
|
||||
send_command_t(lc, command, flag, out_cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Item commands
|
||||
|
||||
@@ -1341,7 +1376,7 @@ static void on_player_drop_item(shared_ptr<Client> c, uint8_t command, uint8_t f
|
||||
|
||||
auto l = c->require_lobby();
|
||||
auto p = c->character();
|
||||
auto item = p->remove_item(cmd.item_id, 0, c->version() != Version::BB_V4);
|
||||
auto item = p->remove_item(cmd.item_id, 0, c->version());
|
||||
l->add_item(cmd.floor, item, cmd.x, cmd.z, 0x00F);
|
||||
|
||||
if (l->log.should_log(LogLevel::INFO)) {
|
||||
@@ -1349,7 +1384,7 @@ static void on_player_drop_item(shared_ptr<Client> c, uint8_t command, uint8_t f
|
||||
auto name = s->describe_item(c->version(), item, false);
|
||||
l->log.info("Player %hu dropped item %08" PRIX32 " (%s) at %hu:(%g, %g)",
|
||||
cmd.header.client_id.load(), cmd.item_id.load(), name.c_str(), cmd.floor.load(), cmd.x.load(), cmd.z.load());
|
||||
p->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
}
|
||||
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
@@ -1374,7 +1409,7 @@ void forward_subcommand_with_item_transcode_t(shared_ptr<Client> c, uint8_t comm
|
||||
out_cmd.header.subcommand = translate_subcommand_number(lc->version(), c->version(), out_cmd.header.subcommand);
|
||||
if (out_cmd.header.subcommand) {
|
||||
out_cmd.item_data.decode_for_version(c->version());
|
||||
out_cmd.item_data.encode_for_version(lc->version(), s->item_parameter_table_for_version(lc->version()));
|
||||
out_cmd.item_data.encode_for_version(lc->version(), s->item_parameter_table(lc->version()));
|
||||
send_command_t(lc, command, flag, out_cmd);
|
||||
} else {
|
||||
lc->log.info("Subcommand cannot be translated to client\'s version");
|
||||
@@ -1403,13 +1438,13 @@ static void on_create_inventory_item_t(shared_ptr<Client> c, uint8_t command, ui
|
||||
ItemData item = cmd.item_data;
|
||||
item.decode_for_version(c->version());
|
||||
l->on_item_id_generated_externally(item.id);
|
||||
p->add_item(item);
|
||||
p->add_item(item, c->version());
|
||||
|
||||
if (l->log.should_log(LogLevel::INFO)) {
|
||||
auto s = c->require_server_state();
|
||||
auto name = s->describe_item(c->version(), item, false);
|
||||
l->log.info("Player %hu created inventory item %08" PRIX32 " (%s)", c->lobby_client_id, item.id.load(), name.c_str());
|
||||
p->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
}
|
||||
|
||||
forward_subcommand_with_item_transcode_t(c, command, flag, cmd);
|
||||
@@ -1450,7 +1485,7 @@ static void on_drop_partial_stack_t(shared_ptr<Client> c, uint8_t command, uint8
|
||||
auto name = s->describe_item(c->version(), item, false);
|
||||
l->log.info("Player %hu split stack to create floor item %08" PRIX32 " (%s) at %hu:(%g, %g)",
|
||||
cmd.header.client_id.load(), item.id.load(), name.c_str(), cmd.floor.load(), cmd.x.load(), cmd.z.load());
|
||||
c->character()->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
}
|
||||
|
||||
forward_subcommand_with_item_transcode_t(c, command, flag, cmd);
|
||||
@@ -1476,7 +1511,7 @@ static void on_drop_partial_stack_bb(shared_ptr<Client> c, uint8_t command, uint
|
||||
}
|
||||
|
||||
auto p = c->character();
|
||||
auto item = p->remove_item(cmd.item_id, cmd.amount, c->version() != Version::BB_V4);
|
||||
auto item = p->remove_item(cmd.item_id, cmd.amount, c->version());
|
||||
|
||||
// If a stack was split, the original item still exists, so the dropped item
|
||||
// needs a new ID. remove_item signals this by returning an item with an ID
|
||||
@@ -1488,7 +1523,7 @@ static void on_drop_partial_stack_bb(shared_ptr<Client> c, uint8_t command, uint
|
||||
// PSOBB sends a 6x29 command after it receives the 6x5D, so we need to add
|
||||
// the item back to the player's inventory to correct for this (it will get
|
||||
// removed again by the 6x29 handler)
|
||||
p->add_item(item);
|
||||
p->add_item(item, c->version());
|
||||
|
||||
l->add_item(cmd.floor, item, cmd.x, cmd.z, 0x00F);
|
||||
send_drop_stacked_item_to_lobby(l, item, cmd.floor, cmd.x, cmd.z);
|
||||
@@ -1498,7 +1533,7 @@ static void on_drop_partial_stack_bb(shared_ptr<Client> c, uint8_t command, uint
|
||||
auto name = s->describe_item(c->version(), item, false);
|
||||
l->log.info("Player %hu split stack %08" PRIX32 " (removed: %s) at %hu:(%g, %g)",
|
||||
cmd.header.client_id.load(), cmd.item_id.load(), name.c_str(), cmd.floor.load(), cmd.x.load(), cmd.z.load());
|
||||
p->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -1523,21 +1558,31 @@ static void on_buy_shop_item(shared_ptr<Client> c, uint8_t command, uint8_t flag
|
||||
item.data2d = 0; // Clear the price field
|
||||
item.decode_for_version(c->version());
|
||||
l->on_item_id_generated_externally(item.id);
|
||||
p->add_item(item);
|
||||
p->add_item(item, c->version());
|
||||
|
||||
size_t price = s->item_parameter_table_for_version(c->version())->price_for_item(item);
|
||||
size_t price = s->item_parameter_table(c->version())->price_for_item(item);
|
||||
p->remove_meseta(price, c->version() != Version::BB_V4);
|
||||
|
||||
if (l->log.should_log(LogLevel::INFO)) {
|
||||
auto name = s->describe_item(c->version(), item, false);
|
||||
l->log.info("Player %hu bought item %08" PRIX32 " (%s) from shop (%zu Meseta)",
|
||||
cmd.header.client_id.load(), item.id.load(), name.c_str(), price);
|
||||
p->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
}
|
||||
|
||||
forward_subcommand_with_item_transcode_t(c, command, flag, cmd);
|
||||
}
|
||||
|
||||
static void send_rare_notification_if_needed(shared_ptr<Client> to_c, const ItemData& item) {
|
||||
auto s = to_c->require_server_state();
|
||||
if (!to_c->config.check_flag(Client::Flag::RARE_DROP_NOTIFICATIONS_ENABLED) ||
|
||||
!s->item_parameter_table(to_c->version())->is_item_rare(item)) {
|
||||
return;
|
||||
}
|
||||
string name = s->describe_item(to_c->version(), item, true);
|
||||
send_text_message_printf(to_c, "$C6Rare item dropped:\n%s", name.c_str());
|
||||
}
|
||||
|
||||
template <typename CmdT>
|
||||
static void on_box_or_enemy_item_drop_t(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
|
||||
// I'm lazy and this should never happen for item commands (since all players
|
||||
@@ -1567,22 +1612,25 @@ static void on_box_or_enemy_item_drop_t(shared_ptr<Client> c, uint8_t command, u
|
||||
l->leader_id, item.id.load(), name.c_str(), cmd.item.floor, cmd.item.x.load(), cmd.item.z.load());
|
||||
|
||||
for (auto& lc : l->clients) {
|
||||
if (!lc || lc == c) {
|
||||
if (!lc) {
|
||||
continue;
|
||||
}
|
||||
if (c->version() != lc->version()) {
|
||||
CmdT out_cmd = cmd;
|
||||
out_cmd.header.subcommand = translate_subcommand_number(lc->version(), c->version(), out_cmd.header.subcommand);
|
||||
if (out_cmd.header.subcommand) {
|
||||
out_cmd.item.item.decode_for_version(c->version());
|
||||
out_cmd.item.item.encode_for_version(lc->version(), s->item_parameter_table_for_version(lc->version()));
|
||||
send_command_t(lc, command, flag, out_cmd);
|
||||
if (lc != c) {
|
||||
if (c->version() != lc->version()) {
|
||||
CmdT out_cmd = cmd;
|
||||
out_cmd.header.subcommand = translate_subcommand_number(lc->version(), c->version(), out_cmd.header.subcommand);
|
||||
if (out_cmd.header.subcommand) {
|
||||
out_cmd.item.item.decode_for_version(c->version());
|
||||
out_cmd.item.item.encode_for_version(lc->version(), s->item_parameter_table(lc->version()));
|
||||
send_command_t(lc, command, flag, out_cmd);
|
||||
} else {
|
||||
lc->log.info("Subcommand cannot be translated to client\'s version");
|
||||
}
|
||||
} else {
|
||||
lc->log.info("Subcommand cannot be translated to client\'s version");
|
||||
send_command_t(lc, command, flag, cmd);
|
||||
}
|
||||
} else {
|
||||
send_command_t(lc, command, flag, cmd);
|
||||
}
|
||||
send_rare_notification_if_needed(lc, item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1626,7 +1674,7 @@ static void on_pick_up_item_generic(
|
||||
}
|
||||
|
||||
try {
|
||||
p->add_item(fi->data);
|
||||
p->add_item(fi->data, c->version());
|
||||
} catch (const out_of_range&) {
|
||||
// Inventory is full; put the item back where it was
|
||||
l->log.warning("Player %hu requests to pick up %08" PRIX32 ", but their inventory is full; dropping command",
|
||||
@@ -1639,7 +1687,7 @@ static void on_pick_up_item_generic(
|
||||
auto s = c->require_server_state();
|
||||
auto name = s->describe_item(c->version(), fi->data, false);
|
||||
l->log.info("Player %hu picked up %08" PRIX32 " (%s)", client_id, item_id, name.c_str());
|
||||
p->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
}
|
||||
|
||||
auto s = c->require_server_state();
|
||||
@@ -1725,7 +1773,7 @@ static void on_use_item(
|
||||
if (l->log.should_log(LogLevel::INFO)) {
|
||||
l->log.info("Player %hhu used item %hu:%08" PRIX32 " (%s)",
|
||||
c->lobby_client_id, cmd.header.client_id.load(), cmd.item_id.load(), name.c_str());
|
||||
p->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
}
|
||||
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
@@ -1763,15 +1811,15 @@ static void on_feed_mag(
|
||||
// a 6x29 immediately after to destroy the fed item. So on BB, we should
|
||||
// remove the fed item here, but on other versions, we allow the following
|
||||
// 6x29 command to do that.
|
||||
if (l->base_version == Version::BB_V4) {
|
||||
p->remove_item(cmd.fed_item_id, 1, false);
|
||||
if (c->version() == Version::BB_V4) {
|
||||
p->remove_item(cmd.fed_item_id, 1, c->version());
|
||||
}
|
||||
|
||||
if (l->log.should_log(LogLevel::INFO)) {
|
||||
l->log.info("Player %hhu fed item %hu:%08" PRIX32 " (%s) to mag %hu:%08" PRIX32 " (%s)",
|
||||
c->lobby_client_id, cmd.header.client_id.load(), cmd.fed_item_id.load(), fed_name.c_str(),
|
||||
cmd.header.client_id.load(), cmd.mag_item_id.load(), mag_name.c_str());
|
||||
p->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
}
|
||||
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
@@ -1806,7 +1854,7 @@ static void on_open_shop_bb_or_ep3_battle_subs(shared_ptr<Client> c, uint8_t com
|
||||
}
|
||||
for (auto& item : c->bb_shop_contents[cmd.shop_type]) {
|
||||
item.id = 0xFFFFFFFF;
|
||||
item.data2d = s->item_parameter_table_for_version(c->version())->price_for_item(item);
|
||||
item.data2d = s->item_parameter_table(c->version())->price_for_item(item);
|
||||
}
|
||||
|
||||
send_shop(c, cmd.shop_type);
|
||||
@@ -1851,7 +1899,7 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr<Client> c, uint
|
||||
}
|
||||
|
||||
} else { // Deposit item
|
||||
auto item = p->remove_item(cmd.item_id, cmd.item_amount, c->version() != Version::BB_V4);
|
||||
auto item = p->remove_item(cmd.item_id, cmd.item_amount, c->version());
|
||||
// If a stack was split, the bank item retains the same item ID as the
|
||||
// inventory item. This is annoying but doesn't cause any problems
|
||||
// because we always generate a new item ID when withdrawing from the
|
||||
@@ -1859,14 +1907,14 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr<Client> c, uint
|
||||
if (item.id == 0xFFFFFFFF) {
|
||||
item.id = cmd.item_id;
|
||||
}
|
||||
bank.add_item(item);
|
||||
bank.add_item(item, c->version());
|
||||
send_destroy_item_to_lobby(c, cmd.item_id, cmd.item_amount, true);
|
||||
|
||||
if (l->log.should_log(LogLevel::INFO)) {
|
||||
string name = s->item_name_index->describe_item(Version::BB_V4, item);
|
||||
string name = s->describe_item(Version::BB_V4, item, false);
|
||||
l->log.info("Player %hu deposited item %08" PRIX32 " (x%hhu) (%s) in the bank",
|
||||
c->lobby_client_id, cmd.item_id.load(), cmd.item_amount, name.c_str());
|
||||
c->character()->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1886,16 +1934,16 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr<Client> c, uint
|
||||
}
|
||||
|
||||
} else { // Take item
|
||||
auto item = bank.remove_item(cmd.item_id, cmd.item_amount);
|
||||
auto item = bank.remove_item(cmd.item_id, cmd.item_amount, c->version());
|
||||
item.id = l->generate_item_id(c->lobby_client_id);
|
||||
p->add_item(item);
|
||||
p->add_item(item, c->version());
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item);
|
||||
|
||||
if (l->log.should_log(LogLevel::INFO)) {
|
||||
string name = s->item_name_index->describe_item(Version::BB_V4, item);
|
||||
string name = s->describe_item(Version::BB_V4, item, false);
|
||||
l->log.info("Player %hu withdrew item %08" PRIX32 " (x%hhu) (%s) from the bank",
|
||||
c->lobby_client_id, item.id.load(), cmd.item_amount, name.c_str());
|
||||
c->character()->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2065,7 +2113,7 @@ static void on_entity_drop_item_request(shared_ptr<Client> c, uint8_t command, u
|
||||
cmd.effective_area = in_cmd.floor;
|
||||
}
|
||||
|
||||
auto generate_item = [&]() -> ItemData {
|
||||
auto generate_item = [&]() -> ItemCreator::DropResult {
|
||||
if (cmd.rt_index == 0x30) {
|
||||
if (l->map) {
|
||||
auto& object = l->map->objects.at(cmd.entity_id);
|
||||
@@ -2129,29 +2177,39 @@ static void on_entity_drop_item_request(shared_ptr<Client> c, uint8_t command, u
|
||||
case Lobby::DropMode::SERVER_DUPLICATE: {
|
||||
// TODO: In SERVER_DUPLICATE mode, should we reduce the rates for rare
|
||||
// items? Maybe by a factor of l->count_clients()?
|
||||
auto item = generate_item();
|
||||
if (item.empty()) {
|
||||
auto res = generate_item();
|
||||
if (res.item.empty()) {
|
||||
l->log.info("No item was created");
|
||||
} else {
|
||||
string name = s->item_name_index->describe_item(l->base_version, item);
|
||||
string name = s->describe_item(l->base_version, res.item, false);
|
||||
l->log.info("Entity %04hX (area %02hX) created item %s", cmd.entity_id.load(), cmd.effective_area, name.c_str());
|
||||
if (l->drop_mode == Lobby::DropMode::SERVER_DUPLICATE) {
|
||||
for (const auto& lc : l->clients) {
|
||||
if (lc && ((cmd.rt_index == 0x30) || (lc->floor == cmd.floor))) {
|
||||
item.id = l->generate_item_id(0xFF);
|
||||
res.item.id = l->generate_item_id(0xFF);
|
||||
l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for %s",
|
||||
item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load(), lc->channel.name.c_str());
|
||||
l->add_item(cmd.floor, item, cmd.x, cmd.z, (1 << lc->lobby_client_id));
|
||||
send_drop_item_to_channel(s, lc->channel, item, cmd.rt_index != 0x30, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
|
||||
res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load(), lc->channel.name.c_str());
|
||||
l->add_item(cmd.floor, res.item, cmd.x, cmd.z, (1 << lc->lobby_client_id));
|
||||
send_drop_item_to_channel(s, lc->channel, res.item, cmd.rt_index != 0x30, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
|
||||
if (res.is_from_rare_table) {
|
||||
send_rare_notification_if_needed(lc, res.item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
item.id = l->generate_item_id(0xFF);
|
||||
res.item.id = l->generate_item_id(0xFF);
|
||||
l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for all clients",
|
||||
item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load());
|
||||
l->add_item(cmd.floor, item, cmd.x, cmd.z, 0x00F);
|
||||
send_drop_item_to_lobby(l, item, cmd.rt_index != 0x30, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
|
||||
res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load());
|
||||
l->add_item(cmd.floor, res.item, cmd.x, cmd.z, 0x00F);
|
||||
send_drop_item_to_lobby(l, res.item, cmd.rt_index != 0x30, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
|
||||
if (res.is_from_rare_table) {
|
||||
for (auto lc : l->clients) {
|
||||
if (lc) {
|
||||
send_rare_notification_if_needed(lc, res.item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -2159,17 +2217,20 @@ static void on_entity_drop_item_request(shared_ptr<Client> c, uint8_t command, u
|
||||
case Lobby::DropMode::SERVER_PRIVATE: {
|
||||
for (const auto& lc : l->clients) {
|
||||
if (lc && ((cmd.rt_index == 0x30) || (lc->floor == cmd.floor))) {
|
||||
auto item = generate_item();
|
||||
if (item.empty()) {
|
||||
auto res = generate_item();
|
||||
if (res.item.empty()) {
|
||||
l->log.info("No item was created for %s", lc->channel.name.c_str());
|
||||
} else {
|
||||
string name = s->item_name_index->describe_item(l->base_version, item);
|
||||
string name = s->describe_item(l->base_version, res.item, false);
|
||||
l->log.info("Entity %04hX (area %02hX) created item %s", cmd.entity_id.load(), cmd.effective_area, name.c_str());
|
||||
item.id = l->generate_item_id(0xFF);
|
||||
res.item.id = l->generate_item_id(0xFF);
|
||||
l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for %s",
|
||||
item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load(), lc->channel.name.c_str());
|
||||
l->add_item(cmd.floor, item, cmd.x, cmd.z, (1 << lc->lobby_client_id));
|
||||
send_drop_item_to_channel(s, lc->channel, item, cmd.rt_index != 0x30, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
|
||||
res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load(), lc->channel.name.c_str());
|
||||
l->add_item(cmd.floor, res.item, cmd.x, cmd.z, (1 << lc->lobby_client_id));
|
||||
send_drop_item_to_channel(s, lc->channel, res.item, cmd.rt_index != 0x30, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
|
||||
if (res.is_from_rare_table) {
|
||||
send_rare_notification_if_needed(lc, res.item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2455,7 +2516,7 @@ static void on_steal_exp_bb(shared_ptr<Client> c, uint8_t, uint8_t, void* data,
|
||||
const auto& inventory = p->inventory;
|
||||
const auto& weapon = inventory.items[inventory.find_equipped_item(EquipSlot::WEAPON)];
|
||||
|
||||
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
|
||||
auto item_parameter_table = s->item_parameter_table(c->version());
|
||||
|
||||
uint8_t special_id = 0;
|
||||
if (((weapon.data.data1[1] < 0x0A) && (weapon.data.data1[2] < 0x05)) ||
|
||||
@@ -2574,7 +2635,7 @@ static void on_enemy_exp_request_bb(shared_ptr<Client> c, uint8_t, uint8_t, void
|
||||
for (size_t z = 0; z < inventory.num_items; z++) {
|
||||
auto& item = inventory.items[z];
|
||||
if ((item.flags & 0x08) &&
|
||||
s->item_parameter_table_for_version(c->version())->is_unsealable_item(item.data)) {
|
||||
s->item_parameter_table(c->version())->is_unsealable_item(item.data)) {
|
||||
item.data.set_sealed_item_kill_count(item.data.get_sealed_item_kill_count() + 1);
|
||||
}
|
||||
}
|
||||
@@ -2598,7 +2659,7 @@ void on_adjust_player_meseta_bb(shared_ptr<Client> c, uint8_t, uint8_t, void* da
|
||||
item.data1[0] = 0x04;
|
||||
item.data2d = cmd.amount.load();
|
||||
item.id = l->generate_item_id(c->lobby_client_id);
|
||||
p->add_item(item);
|
||||
p->add_item(item, c->version());
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item);
|
||||
}
|
||||
}
|
||||
@@ -2609,9 +2670,9 @@ void on_item_reward_request_bb(shared_ptr<Client> c, uint8_t, uint8_t, void* dat
|
||||
|
||||
ItemData item;
|
||||
item = cmd.item_data;
|
||||
item.enforce_min_stack_size();
|
||||
item.enforce_min_stack_size(c->version());
|
||||
item.id = l->generate_item_id(c->lobby_client_id);
|
||||
c->character()->add_item(item);
|
||||
c->character()->add_item(item, c->version());
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item);
|
||||
}
|
||||
|
||||
@@ -2638,13 +2699,13 @@ void on_transfer_item_via_mail_message_bb(shared_ptr<Client> c, uint8_t command,
|
||||
|
||||
auto s = c->require_server_state();
|
||||
auto p = c->character();
|
||||
auto item = p->remove_item(cmd.item_id, cmd.amount, c->version() != Version::BB_V4);
|
||||
auto item = p->remove_item(cmd.item_id, cmd.amount, c->version());
|
||||
|
||||
if (l->log.should_log(LogLevel::INFO)) {
|
||||
auto name = s->describe_item(c->version(), item, false);
|
||||
l->log.info("Player %hhu sent inventory item %hu:%08" PRIX32 " (%s) x%" PRIu32 " to player %08" PRIX32,
|
||||
c->lobby_client_id, cmd.header.client_id.load(), cmd.item_id.load(), name.c_str(), cmd.amount.load(), cmd.target_guild_card_number.load());
|
||||
p->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
}
|
||||
|
||||
// To receive an item, the player must be online, using BB, have a character
|
||||
@@ -2657,7 +2718,7 @@ void on_transfer_item_via_mail_message_bb(shared_ptr<Client> c, uint8_t command,
|
||||
(target_c->character(false) != nullptr) &&
|
||||
!target_c->config.check_flag(Client::Flag::AT_BANK_COUNTER)) {
|
||||
try {
|
||||
target_c->current_bank().add_item(item);
|
||||
target_c->current_bank().add_item(item, target_c->version());
|
||||
item_sent = true;
|
||||
} catch (const runtime_error&) {
|
||||
}
|
||||
@@ -2669,7 +2730,7 @@ void on_transfer_item_via_mail_message_bb(shared_ptr<Client> c, uint8_t command,
|
||||
send_command(c, 0x16EA, 0x00000000);
|
||||
// If the item failed to send, add it back to the sender's inventory
|
||||
item.id = l->generate_item_id(0xFF);
|
||||
p->add_item(item);
|
||||
p->add_item(item, c->version());
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item);
|
||||
}
|
||||
}
|
||||
@@ -2695,7 +2756,7 @@ void on_exchange_item_for_team_points_bb(shared_ptr<Client> c, uint8_t command,
|
||||
|
||||
auto s = c->require_server_state();
|
||||
auto p = c->character();
|
||||
auto item = p->remove_item(cmd.item_id, cmd.amount, c->version() != Version::BB_V4);
|
||||
auto item = p->remove_item(cmd.item_id, cmd.amount, c->version());
|
||||
|
||||
size_t points = s->item_parameter_table_v4->get_item_team_points(item);
|
||||
s->team_index->add_member_points(c->license->serial_number, points);
|
||||
@@ -2704,7 +2765,7 @@ void on_exchange_item_for_team_points_bb(shared_ptr<Client> c, uint8_t command,
|
||||
auto name = s->describe_item(c->version(), item, false);
|
||||
l->log.info("Player %hhu exchanged inventory item %hu:%08" PRIX32 " (%s) for %zu team points",
|
||||
c->lobby_client_id, cmd.header.client_id.load(), cmd.item_id.load(), name.c_str(), points);
|
||||
p->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
}
|
||||
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
@@ -2723,13 +2784,13 @@ static void on_destroy_inventory_item(shared_ptr<Client> c, uint8_t command, uin
|
||||
|
||||
auto s = c->require_server_state();
|
||||
auto p = c->character();
|
||||
auto item = p->remove_item(cmd.item_id, cmd.amount, c->version() != Version::BB_V4);
|
||||
auto item = p->remove_item(cmd.item_id, cmd.amount, c->version());
|
||||
|
||||
if (l->log.should_log(LogLevel::INFO)) {
|
||||
auto name = s->describe_item(c->version(), item, false);
|
||||
l->log.info("Player %hhu destroyed inventory item %hu:%08" PRIX32 " (%s)",
|
||||
c->lobby_client_id, cmd.header.client_id.load(), cmd.item_id.load(), name.c_str());
|
||||
p->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
}
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
}
|
||||
@@ -2815,7 +2876,7 @@ static void on_accept_identify_item_bb(shared_ptr<Client> c, uint8_t command, ui
|
||||
if (c->bb_identify_result.id != cmd.item_id) {
|
||||
throw runtime_error("accepted item ID does not match previous identify request");
|
||||
}
|
||||
c->character()->add_item(c->bb_identify_result);
|
||||
c->character()->add_item(c->bb_identify_result, c->version());
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, c->bb_identify_result);
|
||||
c->bb_identify_result.clear();
|
||||
|
||||
@@ -2832,15 +2893,15 @@ static void on_sell_item_at_shop_bb(shared_ptr<Client> c, uint8_t command, uint8
|
||||
|
||||
auto s = c->require_server_state();
|
||||
auto p = c->character();
|
||||
auto item = p->remove_item(cmd.item_id, cmd.amount, c->version() != Version::BB_V4);
|
||||
size_t price = (s->item_parameter_table_for_version(c->version())->price_for_item(item) >> 3) * cmd.amount;
|
||||
auto item = p->remove_item(cmd.item_id, cmd.amount, c->version());
|
||||
size_t price = (s->item_parameter_table(c->version())->price_for_item(item) >> 3) * cmd.amount;
|
||||
p->add_meseta(price);
|
||||
|
||||
if (l->log.should_log(LogLevel::INFO)) {
|
||||
auto name = s->describe_item(c->version(), item, false);
|
||||
l->log.info("Player %hhu sold inventory item %08" PRIX32 " (%s) for %zu Meseta",
|
||||
c->lobby_client_id, cmd.item_id.load(), name.c_str(), price);
|
||||
p->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
}
|
||||
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
@@ -2854,7 +2915,7 @@ static void on_buy_shop_item_bb(shared_ptr<Client> c, uint8_t, uint8_t, void* da
|
||||
|
||||
ItemData item;
|
||||
item = c->bb_shop_contents.at(cmd.shop_type).at(cmd.item_index);
|
||||
if (item.is_stackable()) {
|
||||
if (item.is_stackable(c->version())) {
|
||||
item.data1[5] = cmd.amount;
|
||||
} else if (cmd.amount != 1) {
|
||||
throw runtime_error("item is not stackable");
|
||||
@@ -2867,7 +2928,7 @@ static void on_buy_shop_item_bb(shared_ptr<Client> c, uint8_t, uint8_t, void* da
|
||||
|
||||
item.id = cmd.shop_item_id;
|
||||
l->on_item_id_generated_externally(item.id);
|
||||
p->add_item(item);
|
||||
p->add_item(item, c->version());
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item, true);
|
||||
|
||||
if (l->log.should_log(LogLevel::INFO)) {
|
||||
@@ -2875,7 +2936,7 @@ static void on_buy_shop_item_bb(shared_ptr<Client> c, uint8_t, uint8_t, void* da
|
||||
auto name = s->describe_item(c->version(), item, false);
|
||||
l->log.info("Player %hhu purchased item %08" PRIX32 " (%s) for %zu meseta",
|
||||
c->lobby_client_id, item.id.load(), name.c_str(), price);
|
||||
p->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2970,15 +3031,15 @@ static void on_quest_exchange_item_bb(shared_ptr<Client> c, uint8_t, uint8_t, vo
|
||||
auto p = c->character();
|
||||
|
||||
size_t found_index = p->inventory.find_item_by_primary_identifier(cmd.find_item.primary_identifier());
|
||||
auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 1, false);
|
||||
auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 1, c->version());
|
||||
send_destroy_item_to_lobby(c, found_item.id, 1);
|
||||
|
||||
// TODO: We probably should use an allow-list here to prevent the client
|
||||
// from creating arbitrary items if cheat mode is disabled.
|
||||
ItemData new_item = cmd.replace_item;
|
||||
new_item.enforce_min_stack_size();
|
||||
new_item.enforce_min_stack_size(c->version());
|
||||
new_item.id = l->generate_item_id(c->lobby_client_id);
|
||||
p->add_item(new_item);
|
||||
p->add_item(new_item, c->version());
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item);
|
||||
|
||||
send_quest_function_call(c, cmd.success_function_id);
|
||||
@@ -2996,10 +3057,10 @@ static void on_wrap_item_bb(shared_ptr<Client> c, uint8_t, uint8_t, void* data,
|
||||
const auto& cmd = check_size_t<G_WrapItem_BB_6xD6>(data, size);
|
||||
|
||||
auto p = c->character();
|
||||
auto item = p->remove_item(cmd.item.id, 1, false);
|
||||
auto item = p->remove_item(cmd.item.id, 1, c->version());
|
||||
send_destroy_item_to_lobby(c, item.id, 1);
|
||||
item.wrap();
|
||||
p->add_item(item);
|
||||
item.wrap(c->version());
|
||||
p->add_item(item, c->version());
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item);
|
||||
}
|
||||
}
|
||||
@@ -3013,15 +3074,15 @@ static void on_photon_drop_exchange_for_item_bb(shared_ptr<Client> c, uint8_t, u
|
||||
auto p = c->character();
|
||||
|
||||
size_t found_index = p->inventory.find_item_by_primary_identifier(0x031000);
|
||||
auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 0, false);
|
||||
send_destroy_item_to_lobby(c, found_item.id, found_item.stack_size());
|
||||
auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 0, c->version());
|
||||
send_destroy_item_to_lobby(c, found_item.id, found_item.stack_size(c->version()));
|
||||
|
||||
// TODO: We probably should use an allow-list here to prevent the client
|
||||
// from creating arbitrary items if cheat mode is disabled.
|
||||
ItemData new_item = cmd.new_item;
|
||||
new_item.enforce_min_stack_size();
|
||||
new_item.enforce_min_stack_size(c->version());
|
||||
new_item.id = l->generate_item_id(c->lobby_client_id);
|
||||
p->add_item(new_item);
|
||||
p->add_item(new_item, c->version());
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item);
|
||||
|
||||
send_quest_function_call(c, cmd.success_function_id);
|
||||
@@ -3049,13 +3110,13 @@ static void on_photon_drop_exchange_for_s_rank_special_bb(shared_ptr<Client> c,
|
||||
// consistent in case of error
|
||||
p->inventory.find_item(cmd.item_id);
|
||||
|
||||
auto payment_item = p->remove_item(p->inventory.items[payment_item_index].data.id, cost, false);
|
||||
auto payment_item = p->remove_item(p->inventory.items[payment_item_index].data.id, cost, c->version());
|
||||
send_destroy_item_to_lobby(c, payment_item.id, cost);
|
||||
|
||||
auto item = p->remove_item(cmd.item_id, 1, false);
|
||||
auto item = p->remove_item(cmd.item_id, 1, c->version());
|
||||
send_destroy_item_to_lobby(c, item.id, cost);
|
||||
item.data1[2] = cmd.special_type;
|
||||
p->add_item(item);
|
||||
p->add_item(item, c->version());
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item);
|
||||
|
||||
send_quest_function_call(c, cmd.success_function_id);
|
||||
@@ -3096,14 +3157,14 @@ static void on_secret_lottery_ticket_exchange_bb(shared_ptr<Client> c, uint8_t,
|
||||
exchange_cmd.amount = 1;
|
||||
send_command_t(c, 0x60, 0x00, exchange_cmd);
|
||||
|
||||
send_destroy_item_to_lobby(c, slt_item_id, 1);
|
||||
p->remove_item(slt_item_id, 1, c->version());
|
||||
|
||||
ItemData item = (s->secret_lottery_results.size() == 1)
|
||||
? s->secret_lottery_results[0]
|
||||
: s->secret_lottery_results[l->random_crypt->next() % s->secret_lottery_results.size()];
|
||||
item.enforce_min_stack_size();
|
||||
item.enforce_min_stack_size(c->version());
|
||||
item.id = l->generate_item_id(c->lobby_client_id);
|
||||
p->add_item(item);
|
||||
p->add_item(item, c->version());
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item);
|
||||
}
|
||||
|
||||
@@ -3129,7 +3190,7 @@ static void on_photon_crystal_exchange_bb(shared_ptr<Client> c, uint8_t, uint8_t
|
||||
check_size_t<G_ExchangePhotonCrystals_BB_6xDF>(data, size);
|
||||
auto p = c->character();
|
||||
size_t index = p->inventory.find_item_by_primary_identifier(0x031002);
|
||||
auto item = p->remove_item(p->inventory.items[index].data.id, 1, false);
|
||||
auto item = p->remove_item(p->inventory.items[index].data.id, 1, c->version());
|
||||
send_destroy_item_to_lobby(c, item.id, 1);
|
||||
}
|
||||
}
|
||||
@@ -3155,7 +3216,7 @@ static void on_quest_F95E_result_bb(shared_ptr<Client> c, uint8_t, uint8_t, void
|
||||
} else if (item.data1[0] == 0x00) {
|
||||
item.data1[4] |= 0x80; // Unidentified
|
||||
} else {
|
||||
item.enforce_min_stack_size();
|
||||
item.enforce_min_stack_size(c->version());
|
||||
}
|
||||
|
||||
item.id = l->generate_item_id(0xFF);
|
||||
@@ -3179,7 +3240,7 @@ static void on_quest_F95F_result_bb(shared_ptr<Client> c, uint8_t, uint8_t, void
|
||||
}
|
||||
|
||||
size_t index = p->inventory.find_item_by_primary_identifier(0x031004); // Photon Ticket
|
||||
auto ticket_item = p->remove_item(p->inventory.items[index].data.id, result.first, false);
|
||||
auto ticket_item = p->remove_item(p->inventory.items[index].data.id, result.first, c->version());
|
||||
// TODO: Shouldn't we send a 6x29 here? Check if this causes desync in an
|
||||
// actual game
|
||||
|
||||
@@ -3191,9 +3252,9 @@ static void on_quest_F95F_result_bb(shared_ptr<Client> c, uint8_t, uint8_t, void
|
||||
send_command_t(c, 0x60, 0x00, cmd_6xDB);
|
||||
|
||||
ItemData new_item = result.second;
|
||||
new_item.enforce_min_stack_size();
|
||||
new_item.enforce_min_stack_size(c->version());
|
||||
new_item.id = l->generate_item_id(c->lobby_client_id);
|
||||
p->add_item(new_item);
|
||||
p->add_item(new_item, c->version());
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item);
|
||||
|
||||
S_GallonPlanResult_BB_25 out_cmd;
|
||||
@@ -3246,7 +3307,7 @@ static void on_quest_F960_result_bb(shared_ptr<Client> c, uint8_t, uint8_t, void
|
||||
|
||||
item.id = l->generate_item_id(c->lobby_client_id);
|
||||
// If it's a weapon, make it unidentified
|
||||
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
|
||||
auto item_parameter_table = s->item_parameter_table(c->version());
|
||||
if ((item.data1[0] == 0x00) && (item_parameter_table->is_item_rare(item) || (item.data1[4] != 0))) {
|
||||
item.data1[4] |= 0x80;
|
||||
}
|
||||
@@ -3257,15 +3318,15 @@ static void on_quest_F960_result_bb(shared_ptr<Client> c, uint8_t, uint8_t, void
|
||||
send_command_t(c, 0x60, 0x00, cmd_6xE3);
|
||||
|
||||
try {
|
||||
p->add_item(item);
|
||||
p->add_item(item, c->version());
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item);
|
||||
if (c->log.should_log(LogLevel::INFO)) {
|
||||
string name = s->item_name_index->describe_item(c->version(), item);
|
||||
string name = s->describe_item(c->version(), item, false);
|
||||
c->log.info("Awarded item %s", name.c_str());
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
if (c->log.should_log(LogLevel::INFO)) {
|
||||
string name = s->item_name_index->describe_item(c->version(), item);
|
||||
string name = s->describe_item(c->version(), item, false);
|
||||
c->log.info("Attempted to award item %s, but inventory was full", name.c_str());
|
||||
}
|
||||
}
|
||||
@@ -3279,7 +3340,7 @@ static void on_momoka_item_exchange_bb(shared_ptr<Client> c, uint8_t, uint8_t, v
|
||||
auto p = c->character();
|
||||
try {
|
||||
size_t found_index = p->inventory.find_item_by_primary_identifier(cmd.find_item.primary_identifier());
|
||||
auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 1, false);
|
||||
auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 1, c->version());
|
||||
|
||||
G_ExchangeItemInQuest_BB_6xDB cmd_6xDB = {{0xDB, 0x04, c->lobby_client_id}, 1, found_item.id, 1};
|
||||
send_command_t(c, 0x60, 0x00, cmd_6xDB);
|
||||
@@ -3289,9 +3350,9 @@ static void on_momoka_item_exchange_bb(shared_ptr<Client> c, uint8_t, uint8_t, v
|
||||
// TODO: We probably should use an allow-list here to prevent the client
|
||||
// from creating arbitrary items if cheat mode is disabled.
|
||||
ItemData new_item = cmd.replace_item;
|
||||
new_item.enforce_min_stack_size();
|
||||
new_item.enforce_min_stack_size(c->version());
|
||||
new_item.id = l->generate_item_id(c->lobby_client_id);
|
||||
p->add_item(new_item);
|
||||
p->add_item(new_item, c->version());
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item);
|
||||
|
||||
send_command(c, 0x23, 0x00);
|
||||
@@ -3314,10 +3375,10 @@ static void on_upgrade_weapon_attribute_bb(shared_ptr<Client> c, uint8_t, uint8_
|
||||
uint32_t payment_primary_identifier = cmd.payment_type ? 0x031001 : 0x031000;
|
||||
size_t payment_index = p->inventory.find_item_by_primary_identifier(payment_primary_identifier);
|
||||
auto& payment_item = p->inventory.items[payment_index].data;
|
||||
if (payment_item.stack_size() < cmd.payment_count) {
|
||||
if (payment_item.stack_size(c->version()) < cmd.payment_count) {
|
||||
throw runtime_error("not enough payment items present");
|
||||
}
|
||||
p->remove_item(payment_item.id, cmd.payment_count, false);
|
||||
p->remove_item(payment_item.id, cmd.payment_count, c->version());
|
||||
send_destroy_item_to_lobby(c, payment_item.id, cmd.payment_count);
|
||||
|
||||
uint8_t attribute_amount = 0;
|
||||
@@ -3445,7 +3506,7 @@ const SubcommandDefinition subcommand_definitions[0x100] = {
|
||||
/* 6x4F */ {0x43, 0x49, 0x4F, on_forward_check_game_client},
|
||||
/* 6x50 */ {0x44, 0x4A, 0x50, on_forward_check_game_client},
|
||||
/* 6x51 */ {0x00, 0x00, 0x51, nullptr},
|
||||
/* 6x52 */ {0x46, 0x4C, 0x52, forward_subcommand_m},
|
||||
/* 6x52 */ {0x46, 0x4C, 0x52, on_set_animation_state},
|
||||
/* 6x53 */ {0x47, 0x4D, 0x53, on_forward_check_game},
|
||||
/* 6x54 */ {0x48, 0x4E, 0x54, nullptr},
|
||||
/* 6x55 */ {0x49, 0x4F, 0x55, on_forward_check_game_client},
|
||||
|
||||
+13
-1
@@ -390,7 +390,19 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
|
||||
}
|
||||
break;
|
||||
case 0x6D:
|
||||
if (!is_pre_v1(version)) {
|
||||
if (version == Version::DC_NTE) {
|
||||
const auto& header = check_size_t<G_UnusedHeader>(cmd_data, cmd_size, 0xFFFF);
|
||||
if (header.subcommand == 0x60) {
|
||||
auto& mask = check_size_t<G_SyncPlayerDispAndInventory_DCNTE_6x70>(mask_data, mask_size, 0xFFFF);
|
||||
mask.visual.name_color_checksum = 0;
|
||||
}
|
||||
} else if (version == Version::DC_V1_11_2000_PROTOTYPE) {
|
||||
const auto& header = check_size_t<G_UnusedHeader>(cmd_data, cmd_size, 0xFFFF);
|
||||
if (header.subcommand == 0x67) {
|
||||
auto& mask = check_size_t<G_SyncPlayerDispAndInventory_DC112000_6x70>(mask_data, mask_size, 0xFFFF);
|
||||
mask.visual.name_color_checksum = 0;
|
||||
}
|
||||
} else if (!is_pre_v1(version)) {
|
||||
const auto& header = check_size_t<G_UnusedHeader>(cmd_data, cmd_size, 0xFFFF);
|
||||
if (header.subcommand == 0x70) {
|
||||
auto& mask = check_size_t<G_SyncPlayerDispAndInventory_DC_PC_6x70>(mask_data, mask_size, 0xFFFF);
|
||||
|
||||
+5
-28
@@ -396,7 +396,7 @@ PSOBBCharacterFile::SymbolChatEntry PSOBBCharacterFile::DefaultSymbolChatEntry::
|
||||
|
||||
// TODO: Eliminate duplication between this function and the parallel function
|
||||
// in PlayerBank
|
||||
void PSOBBCharacterFile::add_item(const ItemData& item) {
|
||||
void PSOBBCharacterFile::add_item(const ItemData& item, Version version) {
|
||||
uint32_t pid = item.primary_identifier();
|
||||
|
||||
// Annoyingly, meseta is in the disp data, not in the inventory struct. If the
|
||||
@@ -407,7 +407,7 @@ void PSOBBCharacterFile::add_item(const ItemData& item) {
|
||||
}
|
||||
|
||||
// Handle combinable items
|
||||
size_t combine_max = item.max_stack_size();
|
||||
size_t combine_max = item.max_stack_size(version);
|
||||
if (combine_max > 1) {
|
||||
// Get the item index if there's already a stack of the same item in the
|
||||
// player's inventory
|
||||
@@ -444,13 +444,13 @@ void PSOBBCharacterFile::add_item(const ItemData& item) {
|
||||
|
||||
// TODO: Eliminate code duplication between this function and the parallel
|
||||
// function in PlayerBank
|
||||
ItemData PSOBBCharacterFile::remove_item(uint32_t item_id, uint32_t amount, bool allow_meseta_overdraft) {
|
||||
ItemData PSOBBCharacterFile::remove_item(uint32_t item_id, uint32_t amount, Version version) {
|
||||
ItemData ret;
|
||||
|
||||
// If we're removing meseta (signaled by an invalid item ID), then create a
|
||||
// meseta item.
|
||||
if (item_id == 0xFFFFFFFF) {
|
||||
this->remove_meseta(amount, allow_meseta_overdraft);
|
||||
this->remove_meseta(amount, !is_v4(version));
|
||||
ret.data1[0] = 0x04;
|
||||
ret.data2d = amount;
|
||||
return ret;
|
||||
@@ -464,7 +464,7 @@ ItemData PSOBBCharacterFile::remove_item(uint32_t item_id, uint32_t amount, bool
|
||||
// then create a new item and reduce the amount of the existing stack. Note
|
||||
// that passing amount == 0 means to remove the entire stack, so this only
|
||||
// applies if amount is nonzero.
|
||||
if (amount && (inventory_item.data.stack_size() > 1) &&
|
||||
if (amount && (inventory_item.data.stack_size(version) > 1) &&
|
||||
(amount < inventory_item.data.data1[5])) {
|
||||
if (is_equipped) {
|
||||
throw runtime_error("character has a combine item equipped");
|
||||
@@ -578,29 +578,6 @@ void PSOBBCharacterFile::clear_all_material_usage() {
|
||||
}
|
||||
}
|
||||
|
||||
void PSOBBCharacterFile::print_inventory(FILE* stream, Version version, shared_ptr<const ItemNameIndex> name_index) const {
|
||||
fprintf(stream, "[PlayerInventory] Meseta: %" PRIu32 "\n", this->disp.stats.meseta.load());
|
||||
fprintf(stream, "[PlayerInventory] %hhu items\n", this->inventory.num_items);
|
||||
for (size_t x = 0; x < this->inventory.num_items; x++) {
|
||||
const auto& item = this->inventory.items[x];
|
||||
auto name = name_index->describe_item(version, item.data);
|
||||
auto hex = item.data.hex();
|
||||
fprintf(stream, "[PlayerInventory] %2zu: [+%08" PRIX32 "] %s (%s)\n", x, item.flags.load(), hex.c_str(), name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void PSOBBCharacterFile::print_bank(FILE* stream, Version version, shared_ptr<const ItemNameIndex> name_index) const {
|
||||
fprintf(stream, "[PlayerBank] Meseta: %" PRIu32 "\n", this->bank.meseta.load());
|
||||
fprintf(stream, "[PlayerBank] %" PRIu32 " items\n", this->bank.num_items.load());
|
||||
for (size_t x = 0; x < this->bank.num_items; x++) {
|
||||
const auto& item = this->bank.items[x];
|
||||
const char* present_token = item.present ? "" : " (missing present flag)";
|
||||
auto name = name_index->describe_item(version, item.data);
|
||||
auto hex = item.data.hex();
|
||||
fprintf(stream, "[PlayerBank] %3zu: %s (%s) (x%hu) %s\n", x, hex.c_str(), name.c_str(), item.amount.load(), present_token);
|
||||
}
|
||||
}
|
||||
|
||||
const array<PSOBBCharacterFile::DefaultSymbolChatEntry, 6> PSOBBCharacterFile::DEFAULT_SYMBOL_CHATS = {
|
||||
DefaultSymbolChatEntry{"\tEHello", 0x28, {0xFFFF, 0x000D, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x05, 0x18, 0x1D, 0x00}, {0x05, 0x28, 0x1D, 0x01}, {0x36, 0x20, 0x2A, 0x00}, {0x3C, 0x00, 0x32, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}}},
|
||||
DefaultSymbolChatEntry{"\tEGood-bye", 0x74, {0x0476, 0x000C, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x06, 0x15, 0x14, 0x00}, {0x06, 0x2B, 0x14, 0x01}, {0x05, 0x18, 0x1F, 0x00}, {0x05, 0x28, 0x1F, 0x01}, {0x36, 0x20, 0x2A, 0x00}, {0x3C, 0x00, 0x32, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}}},
|
||||
|
||||
@@ -231,8 +231,8 @@ struct PSOBBCharacterFile {
|
||||
const PlayerDispDataBBPreview& preview,
|
||||
std::shared_ptr<const LevelTable> level_table);
|
||||
|
||||
void add_item(const ItemData& item);
|
||||
ItemData remove_item(uint32_t item_id, uint32_t amount, bool allow_meseta_overdraft);
|
||||
void add_item(const ItemData& item, Version version);
|
||||
ItemData remove_item(uint32_t item_id, uint32_t amount, Version version);
|
||||
void add_meseta(uint32_t amount);
|
||||
void remove_meseta(uint32_t amount, bool allow_overdraft);
|
||||
|
||||
@@ -252,9 +252,6 @@ struct PSOBBCharacterFile {
|
||||
uint8_t get_material_usage(MaterialType which) const;
|
||||
void set_material_usage(MaterialType which, uint8_t usage);
|
||||
void clear_all_material_usage();
|
||||
|
||||
void print_inventory(FILE* stream, Version version, std::shared_ptr<const ItemNameIndex> name_index) const;
|
||||
void print_bank(FILE* stream, Version version, std::shared_ptr<const ItemNameIndex> name_index) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PSOBBGuildCardFile {
|
||||
|
||||
+5
-16
@@ -2239,7 +2239,7 @@ void send_execute_item_trade(shared_ptr<Client> c, const vector<ItemData>& items
|
||||
cmd.item_count = items.size();
|
||||
for (size_t x = 0; x < items.size(); x++) {
|
||||
cmd.item_datas[x] = items[x];
|
||||
cmd.item_datas[x].encode_for_version(c->version(), s->item_parameter_table_for_version(c->version()));
|
||||
cmd.item_datas[x].encode_for_version(c->version(), s->item_parameter_table(c->version()));
|
||||
}
|
||||
send_command_t(c, 0xD3, 0x00, cmd);
|
||||
}
|
||||
@@ -2385,18 +2385,7 @@ void send_game_item_state(shared_ptr<Client> c) {
|
||||
|
||||
G_SyncItemState_6x6D_Decompressed decompressed_header;
|
||||
for (size_t z = 0; z < 12; z++) {
|
||||
if (z == c->lobby_client_id) {
|
||||
// If the player is joining, adjust the next item ID to use the value
|
||||
// before inventory item IDs are assigned
|
||||
size_t num_items = c->character()->inventory.num_items;
|
||||
uint32_t next_id = l->next_item_id_for_client[z] - num_items;
|
||||
if ((next_id & 0xFFE00000) != (l->next_item_id_for_client[z] & 0xFFE00000)) {
|
||||
throw runtime_error("next item ID underflow during joining player item state generation");
|
||||
}
|
||||
decompressed_header.next_item_id_per_player[z] = next_id;
|
||||
} else {
|
||||
decompressed_header.next_item_id_per_player[z] = l->next_item_id_for_client[z];
|
||||
}
|
||||
decompressed_header.next_item_id_per_player[z] = l->next_item_id_for_client[z];
|
||||
}
|
||||
l->log.info("Sending next item IDs to client: %08" PRIX32 " %08" PRIX32 " %08" PRIX32 " %08" PRIX32,
|
||||
decompressed_header.next_item_id_per_player[0].load(),
|
||||
@@ -2426,7 +2415,7 @@ void send_game_item_state(shared_ptr<Client> c) {
|
||||
fi.unknown_a2 = 0;
|
||||
fi.drop_number = (floor == 0) ? 0xFFFF : (decompressed_header.next_drop_number_per_floor.at(floor - 1)++);
|
||||
fi.item = item->data;
|
||||
fi.item.encode_for_version(c->version(), s->item_parameter_table_for_version(c->version()));
|
||||
fi.item.encode_for_version(c->version(), s->item_parameter_table(c->version()));
|
||||
floor_items_w.put(fi);
|
||||
|
||||
decompressed_header.floor_item_count_per_floor.at(floor)++;
|
||||
@@ -2504,7 +2493,7 @@ void send_drop_item_to_channel(shared_ptr<ServerState> s, Channel& ch, const Ite
|
||||
uint8_t subcommand = get_pre_v1_subcommand(ch.version, 0x51, 0x58, 0x5F);
|
||||
G_DropItem_PC_V3_BB_6x5F cmd = {
|
||||
{{subcommand, 0x0B, 0x0000}, {floor, from_enemy, entity_id, x, z, 0, 0, item}}, 0};
|
||||
cmd.item.item.encode_for_version(ch.version, s->item_parameter_table_for_version(ch.version));
|
||||
cmd.item.item.encode_for_version(ch.version, s->item_parameter_table(ch.version));
|
||||
ch.send(0x60, 0x00, &cmd, sizeof(cmd));
|
||||
}
|
||||
|
||||
@@ -2523,7 +2512,7 @@ void send_drop_stacked_item_to_channel(
|
||||
shared_ptr<ServerState> s, Channel& ch, const ItemData& item, uint8_t floor, float x, float z) {
|
||||
uint8_t subcommand = get_pre_v1_subcommand(ch.version, 0x4F, 0x56, 0x5D);
|
||||
G_DropStackedItem_PC_V3_BB_6x5D cmd = {{{subcommand, 0x0A, 0x0000}, floor, 0, x, z, item}, 0};
|
||||
cmd.item_data.encode_for_version(ch.version, s->item_parameter_table_for_version(ch.version));
|
||||
cmd.item_data.encode_for_version(ch.version, s->item_parameter_table(ch.version));
|
||||
ch.send(0x60, 0x00, &cmd, sizeof(cmd));
|
||||
}
|
||||
|
||||
|
||||
+24
-44
@@ -120,17 +120,24 @@ General commands:\n\
|
||||
\n\
|
||||
Server commands:\n\
|
||||
reload ITEM [ITEM...]\n\
|
||||
Reload various parts of the server configuration. ITEMs can be:\n\
|
||||
licenses - reload the license index file\n\
|
||||
patches - reindex the PC and BB patch directories\n\
|
||||
battle-params - reload the enemy stats files\n\
|
||||
level-table - reload the level-up tables\n\
|
||||
item-tables - reload the item generation tables\n\
|
||||
ep3 - reload Episode 3 card definitions and maps (not download quests)\n\
|
||||
quests - reindex all quests (including Episode 3 download quests)\n\
|
||||
functions - recompile all client-side functions\n\
|
||||
dol-files - reindex all DOL files\n\
|
||||
Reload various parts of the server configuration. When you reload any item,\n\
|
||||
any other item that depends on it will be reloaded as well. The items are:\n\
|
||||
all - reindex/reload everything\n\
|
||||
battle-params - reload the BB enemy stats files\n\
|
||||
bb-private-keys - reload BB private keys\n\
|
||||
config - reload most fields from config.json\n\
|
||||
dol-files - reindex all DOL files\n\
|
||||
drop-tables - reload drop tables\n\
|
||||
ep3-data - reload Episode 3 cards and maps (not download quests)\n\
|
||||
functions - recompile all client-side patches and functions\n\
|
||||
item-definitions - reload item definitions files\n\
|
||||
level-table - reload the level-up tables\n\
|
||||
licenses - reindex user licenses\n\
|
||||
patch-indexes - reindex the PC and BB patch directories\n\
|
||||
quest-index - reindex all quests (including Episode3 download quests)\n\
|
||||
teams - reindex all BB teams\n\
|
||||
text-index - reload in-game text\n\
|
||||
word-select-table - regenerate the Word Select translation table\n\
|
||||
Reloading will not affect items that are in use; for example, if an Episode\n\
|
||||
3 battle is in progress, it will continue to use the previous map and card\n\
|
||||
definitions. Similarly, BB clients are not forced to disconnect or reload\n\
|
||||
@@ -285,41 +292,14 @@ Proxy session commands:\n\
|
||||
if (types.empty()) {
|
||||
throw invalid_argument("no data type given");
|
||||
}
|
||||
for (const string& type : types) {
|
||||
if (type == "licenses") {
|
||||
this->state->load_licenses();
|
||||
} else if (type == "teams") {
|
||||
this->state->load_teams();
|
||||
} else if (type == "patches") {
|
||||
this->state->load_patch_indexes();
|
||||
} else if (type == "battle-params") {
|
||||
this->state->load_battle_params();
|
||||
} else if (type == "level-table") {
|
||||
this->state->load_level_table();
|
||||
} else if (type == "item-tables") {
|
||||
this->state->load_item_name_index();
|
||||
this->state->load_item_tables();
|
||||
} else if (type == "word-select") {
|
||||
this->state->load_word_select_table();
|
||||
} else if (type == "ep3") {
|
||||
this->state->load_ep3_data();
|
||||
} else if (type == "quests") {
|
||||
this->state->load_quest_index();
|
||||
} else if (type == "functions") {
|
||||
auto config_json = this->state->load_config();
|
||||
this->state->compile_functions();
|
||||
} else if (type == "dol-files") {
|
||||
auto config_json = this->state->load_config();
|
||||
this->state->load_dol_files();
|
||||
} else if (type == "config") {
|
||||
auto config_json = this->state->load_config();
|
||||
this->state->parse_config(config_json, true);
|
||||
this->state->resolve_ep3_card_names();
|
||||
this->state->load_teams();
|
||||
} else {
|
||||
throw invalid_argument("incorrect data type");
|
||||
for (auto& type : types) {
|
||||
for (char& ch : type) {
|
||||
if (ch == '-') {
|
||||
ch = '_';
|
||||
}
|
||||
}
|
||||
}
|
||||
this->state->load_objects_and_downstream_dependents(types);
|
||||
|
||||
} else if (command_name == "add-license") {
|
||||
auto l = this->state->license_index->create_license();
|
||||
@@ -806,7 +786,7 @@ Proxy session commands:\n\
|
||||
}
|
||||
|
||||
auto s = ses->require_server_state();
|
||||
ItemData item = s->item_name_index->parse_item_description(ses->version(), command_args);
|
||||
ItemData item = s->parse_item_description(ses->version(), command_args);
|
||||
item.id = random_object<uint32_t>() | 0x80000000;
|
||||
|
||||
if (command_name == "set-next-item") {
|
||||
|
||||
+375
-248
@@ -14,7 +14,7 @@
|
||||
#include "NetworkAddresses.hh"
|
||||
#include "SendCommands.hh"
|
||||
#include "Text.hh"
|
||||
#include "UnicodeTextSet.hh"
|
||||
#include "TextIndex.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -25,137 +25,39 @@ ServerState::QuestF960Result::QuestF960Result(const JSON& json, std::shared_ptr<
|
||||
this->probability_upgrade = json.get_int("ProbabilityUpgrade", 0);
|
||||
for (size_t day = 0; day < 7; day++) {
|
||||
for (const auto& item_it : json.get_list(day_names[day])) {
|
||||
this->results[day].emplace_back(name_index->parse_item_description(Version::BB_V4, item_it->as_string()));
|
||||
this->results[day].emplace_back(name_index->parse_item_description(item_it->as_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServerState::ServerState() : creation_time(now()) {
|
||||
this->create_load_step_graph();
|
||||
}
|
||||
|
||||
ServerState::ServerState(shared_ptr<struct event_base> base, const string& config_filename, bool is_replay)
|
||||
: creation_time(now()),
|
||||
base(base),
|
||||
config_filename(config_filename),
|
||||
is_replay(is_replay),
|
||||
dns_server_port(0),
|
||||
ip_stack_debug(false),
|
||||
allow_unregistered_users(false),
|
||||
allow_pc_nte(false),
|
||||
use_temp_licenses_for_prototypes(true),
|
||||
allow_dc_pc_games(false),
|
||||
allow_gc_xb_games(true),
|
||||
allowed_drop_modes_v1_v2_normal(0x1F),
|
||||
allowed_drop_modes_v1_v2_battle(0x07),
|
||||
allowed_drop_modes_v1_v2_challenge(0x07),
|
||||
allowed_drop_modes_v3_normal(0x1F),
|
||||
allowed_drop_modes_v3_battle(0x07),
|
||||
allowed_drop_modes_v3_challenge(0x07),
|
||||
allowed_drop_modes_v4_normal(0x1D), // CLIENT not allowed
|
||||
allowed_drop_modes_v4_battle(0x05),
|
||||
allowed_drop_modes_v4_challenge(0x05),
|
||||
default_drop_mode_v1_v2_normal(Lobby::DropMode::CLIENT),
|
||||
default_drop_mode_v1_v2_battle(Lobby::DropMode::CLIENT),
|
||||
default_drop_mode_v1_v2_challenge(Lobby::DropMode::CLIENT),
|
||||
default_drop_mode_v3_normal(Lobby::DropMode::CLIENT),
|
||||
default_drop_mode_v3_battle(Lobby::DropMode::CLIENT),
|
||||
default_drop_mode_v3_challenge(Lobby::DropMode::CLIENT),
|
||||
default_drop_mode_v4_normal(Lobby::DropMode::SERVER_SHARED),
|
||||
default_drop_mode_v4_battle(Lobby::DropMode::SERVER_SHARED),
|
||||
default_drop_mode_v4_challenge(Lobby::DropMode::SERVER_SHARED),
|
||||
persistent_game_idle_timeout_usecs(0),
|
||||
ep3_send_function_call_enabled(false),
|
||||
catch_handler_exceptions(true),
|
||||
ep3_infinite_meseta(false),
|
||||
ep3_defeat_player_meseta_rewards({400, 500, 600, 700, 800}),
|
||||
ep3_defeat_com_meseta_rewards({100, 200, 300, 400, 500}),
|
||||
ep3_final_round_meseta_bonus(300),
|
||||
ep3_jukebox_is_free(false),
|
||||
ep3_behavior_flags(0),
|
||||
hide_download_commands(true),
|
||||
run_shell_behavior(RunShellBehavior::DEFAULT),
|
||||
cheat_mode_behavior(BehaviorSwitch::OFF_BY_DEFAULT),
|
||||
bb_global_exp_multiplier(1),
|
||||
ep3_card_auction_points(0),
|
||||
ep3_card_auction_min_size(0),
|
||||
ep3_card_auction_max_size(0),
|
||||
player_files_manager(make_shared<PlayerFilesManager>(base)),
|
||||
destroy_lobbies_event(event_new(base.get(), -1, EV_TIMEOUT, &ServerState::dispatch_destroy_lobbies, this), event_free),
|
||||
next_lobby_id(1),
|
||||
pre_lobby_event(0),
|
||||
ep3_menu_song(-1),
|
||||
local_address(0),
|
||||
external_address(0),
|
||||
proxy_allow_save_files(true),
|
||||
proxy_enable_login_options(false) {}
|
||||
player_files_manager(this->base ? make_shared<PlayerFilesManager>(base) : nullptr),
|
||||
destroy_lobbies_event(this->base ? event_new(base.get(), -1, EV_TIMEOUT, &ServerState::dispatch_destroy_lobbies, this) : nullptr, event_free) {
|
||||
this->create_load_step_graph();
|
||||
}
|
||||
|
||||
void ServerState::init() {
|
||||
vector<shared_ptr<Lobby>> non_v1_only_lobbies;
|
||||
vector<shared_ptr<Lobby>> ep3_only_lobbies;
|
||||
void ServerState::load_objects_and_downstream_dependents(const std::string& what) {
|
||||
this->load_step_graph.run(what, false);
|
||||
}
|
||||
|
||||
for (size_t x = 0; x < 20; x++) {
|
||||
auto lobby_name = string_printf("LOBBY%zu", x + 1);
|
||||
bool allow_v1 = (x <= 9);
|
||||
bool allow_non_ep3 = (x <= 14);
|
||||
void ServerState::load_objects_and_downstream_dependents(const std::vector<std::string>& what) {
|
||||
this->load_step_graph.run(what, false);
|
||||
}
|
||||
|
||||
shared_ptr<Lobby> l = this->create_lobby(false);
|
||||
l->set_flag(Lobby::Flag::PUBLIC);
|
||||
l->set_flag(Lobby::Flag::DEFAULT);
|
||||
l->set_flag(Lobby::Flag::PERSISTENT);
|
||||
if (allow_non_ep3) {
|
||||
if (allow_v1) {
|
||||
l->allow_version(Version::DC_NTE);
|
||||
l->allow_version(Version::DC_V1_11_2000_PROTOTYPE);
|
||||
l->allow_version(Version::DC_V1);
|
||||
}
|
||||
l->allow_version(Version::DC_V2);
|
||||
l->allow_version(Version::PC_NTE);
|
||||
l->allow_version(Version::PC_V2);
|
||||
l->allow_version(Version::GC_NTE);
|
||||
l->allow_version(Version::GC_V3);
|
||||
l->allow_version(Version::XB_V3);
|
||||
l->allow_version(Version::BB_V4);
|
||||
}
|
||||
l->allow_version(Version::GC_EP3_NTE);
|
||||
l->allow_version(Version::GC_EP3);
|
||||
void ServerState::load_objects_and_upstream_dependents(const std::string& what) {
|
||||
this->load_step_graph.run(what, true);
|
||||
}
|
||||
|
||||
l->block = x + 1;
|
||||
l->name = lobby_name;
|
||||
l->max_clients = 12;
|
||||
if (!allow_non_ep3) {
|
||||
l->episode = Episode::EP3;
|
||||
}
|
||||
|
||||
if (allow_non_ep3) {
|
||||
this->public_lobby_search_order.emplace_back(l);
|
||||
} else {
|
||||
ep3_only_lobbies.emplace_back(l);
|
||||
}
|
||||
}
|
||||
|
||||
// Annoyingly, the CARD lobbies should be searched first, but are sent at the
|
||||
// end of the lobby list command, so we have to change the search order
|
||||
// manually here.
|
||||
this->public_lobby_search_order.insert(
|
||||
this->public_lobby_search_order.begin(),
|
||||
ep3_only_lobbies.begin(),
|
||||
ep3_only_lobbies.end());
|
||||
|
||||
// Load all the necessary data
|
||||
auto config = this->load_config();
|
||||
this->collect_network_addresses();
|
||||
this->load_item_name_index();
|
||||
this->parse_config(config, false);
|
||||
this->load_bb_private_keys();
|
||||
this->load_licenses();
|
||||
this->load_teams();
|
||||
this->load_patch_indexes();
|
||||
this->load_battle_params();
|
||||
this->load_level_table();
|
||||
this->load_item_tables();
|
||||
this->load_word_select_table();
|
||||
this->load_ep3_data();
|
||||
this->resolve_ep3_card_names();
|
||||
this->load_quest_index();
|
||||
this->compile_functions();
|
||||
this->load_dol_files();
|
||||
void ServerState::load_objects_and_upstream_dependents(const std::vector<std::string>& what) {
|
||||
this->load_step_graph.run(what, true);
|
||||
}
|
||||
|
||||
void ServerState::add_client_to_available_lobby(shared_ptr<Client> c) {
|
||||
@@ -379,7 +281,7 @@ uint32_t ServerState::connect_address_for_client(shared_ptr<Client> c) const {
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<const Menu> ServerState::information_menu_for_version(Version version) const {
|
||||
shared_ptr<const Menu> ServerState::information_menu(Version version) const {
|
||||
if (is_v1_or_v2(version)) {
|
||||
return this->information_menu_v2;
|
||||
} else if (is_v3(version)) {
|
||||
@@ -388,7 +290,7 @@ shared_ptr<const Menu> ServerState::information_menu_for_version(Version version
|
||||
throw out_of_range("no information menu exists for this version");
|
||||
}
|
||||
|
||||
shared_ptr<const Menu> ServerState::proxy_destinations_menu_for_version(Version version) const {
|
||||
shared_ptr<const Menu> ServerState::proxy_destinations_menu(Version version) const {
|
||||
switch (version) {
|
||||
case Version::DC_NTE:
|
||||
case Version::DC_V1_11_2000_PROTOTYPE:
|
||||
@@ -410,7 +312,7 @@ shared_ptr<const Menu> ServerState::proxy_destinations_menu_for_version(Version
|
||||
}
|
||||
}
|
||||
|
||||
const vector<pair<string, uint16_t>>& ServerState::proxy_destinations_for_version(Version version) const {
|
||||
const vector<pair<string, uint16_t>>& ServerState::proxy_destinations(Version version) const {
|
||||
switch (version) {
|
||||
case Version::DC_NTE:
|
||||
case Version::DC_V1_11_2000_PROTOTYPE:
|
||||
@@ -432,7 +334,19 @@ const vector<pair<string, uint16_t>>& ServerState::proxy_destinations_for_versio
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<const ItemParameterTable> ServerState::item_parameter_table_for_version(Version version) const {
|
||||
shared_ptr<const vector<string>> ServerState::information_contents_for_client(shared_ptr<const Client> c) const {
|
||||
return is_v1_or_v2(c->version()) ? this->information_contents_v2 : this->information_contents_v3;
|
||||
}
|
||||
|
||||
shared_ptr<const QuestIndex> ServerState::quest_index(Version version) const {
|
||||
return is_ep3(version) ? this->ep3_download_quest_index : this->default_quest_index;
|
||||
}
|
||||
|
||||
void ServerState::dispatch_destroy_lobbies(evutil_socket_t, short, void* ctx) {
|
||||
reinterpret_cast<ServerState*>(ctx)->lobbies_to_destroy.clear();
|
||||
}
|
||||
|
||||
shared_ptr<const ItemParameterTable> ServerState::item_parameter_table(Version version) const {
|
||||
switch (version) {
|
||||
case Version::DC_NTE:
|
||||
case Version::DC_V1_11_2000_PROTOTYPE:
|
||||
@@ -454,11 +368,24 @@ shared_ptr<const ItemParameterTable> ServerState::item_parameter_table_for_versi
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<const ItemNameIndex> ServerState::item_name_index(Version version) const {
|
||||
auto ret = this->item_name_indexes.at(static_cast<size_t>(version));
|
||||
if (ret == nullptr) {
|
||||
throw runtime_error("no item name index exists for this version");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ServerState::set_item_name_index(Version version, shared_ptr<const ItemNameIndex> new_index) {
|
||||
this->item_name_indexes.at(static_cast<size_t>(version)) = new_index;
|
||||
}
|
||||
|
||||
string ServerState::describe_item(Version version, const ItemData& item, bool include_color_codes) const {
|
||||
return this->item_name_index->describe_item(
|
||||
version,
|
||||
item,
|
||||
include_color_codes ? this->item_parameter_table_for_version(version) : nullptr);
|
||||
return this->item_name_index(version)->describe_item(item, include_color_codes);
|
||||
}
|
||||
|
||||
ItemData ServerState::parse_item_description(Version version, const string& description) const {
|
||||
return this->item_name_index(version)->parse_item_description(description);
|
||||
}
|
||||
|
||||
void ServerState::set_port_configuration(
|
||||
@@ -553,6 +480,34 @@ shared_ptr<const string> ServerState::load_bb_file(
|
||||
}
|
||||
}
|
||||
|
||||
pair<string, uint16_t> ServerState::parse_port_spec(const JSON& json) const {
|
||||
if (json.is_list()) {
|
||||
string addr = json.at(0).as_string();
|
||||
try {
|
||||
addr = string_for_address(this->all_addresses.at(addr));
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
return make_pair(addr, json.at(1).as_int());
|
||||
} else {
|
||||
return make_pair("", json.as_int());
|
||||
}
|
||||
}
|
||||
|
||||
vector<PortConfiguration> ServerState::parse_port_configuration(const JSON& json) const {
|
||||
vector<PortConfiguration> ret;
|
||||
for (const auto& item_json_it : json.as_dict()) {
|
||||
const auto& item_list = item_json_it.second;
|
||||
PortConfiguration& pc = ret.emplace_back();
|
||||
pc.name = item_json_it.first;
|
||||
auto spec = this->parse_port_spec(item_list->at(0));
|
||||
pc.addr = std::move(spec.first);
|
||||
pc.port = spec.second;
|
||||
pc.version = enum_for_name<Version>(item_list->at(1).as_string().c_str());
|
||||
pc.behavior = enum_for_name<ServerBehavior>(item_list->at(2).as_string().c_str());
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ServerState::collect_network_addresses() {
|
||||
config_log.info("Reading network addresses");
|
||||
this->all_addresses = get_local_addresses();
|
||||
@@ -562,26 +517,13 @@ void ServerState::collect_network_addresses() {
|
||||
}
|
||||
}
|
||||
|
||||
JSON ServerState::load_config() const {
|
||||
config_log.info("Loading configuration");
|
||||
return JSON::parse(load_file(this->config_filename));
|
||||
}
|
||||
|
||||
static vector<PortConfiguration> parse_port_configuration(const JSON& json) {
|
||||
vector<PortConfiguration> ret;
|
||||
for (const auto& item_json_it : json.as_dict()) {
|
||||
const auto& item_list = item_json_it.second;
|
||||
PortConfiguration& pc = ret.emplace_back();
|
||||
pc.name = item_json_it.first;
|
||||
pc.port = item_list->at(0).as_int();
|
||||
pc.version = enum_for_name<Version>(item_list->at(1).as_string().c_str());
|
||||
pc.behavior = enum_for_name<ServerBehavior>(item_list->at(2).as_string().c_str());
|
||||
void ServerState::load_config() {
|
||||
if (this->config_filename.empty()) {
|
||||
throw logic_error("configuration filename is missing");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ServerState::parse_config(const JSON& json, bool is_reload) {
|
||||
config_log.info("Parsing configuration");
|
||||
config_log.info("Loading configuration");
|
||||
auto json = JSON::parse(load_file(this->config_filename));
|
||||
|
||||
auto parse_behavior_switch = [&](const string& json_key, BehaviorSwitch default_value) -> ServerState::BehaviorSwitch {
|
||||
try {
|
||||
@@ -604,7 +546,7 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
|
||||
|
||||
this->name = json.at("ServerName").as_string();
|
||||
|
||||
if (!is_reload) {
|
||||
if (!this->config_loaded) {
|
||||
try {
|
||||
this->username = json.at("User").as_string();
|
||||
if (this->username == "$SUDO_USER") {
|
||||
@@ -618,7 +560,12 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
|
||||
}
|
||||
|
||||
this->set_port_configuration(parse_port_configuration(json.at("PortConfiguration")));
|
||||
this->dns_server_port = json.get_int("DNSServerPort", this->dns_server_port);
|
||||
try {
|
||||
auto spec = this->parse_port_spec(json.at("DNSServerPort"));
|
||||
this->dns_server_addr = std::move(spec.first);
|
||||
this->dns_server_port = spec.second;
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
for (const auto& item : json.at("IPStackListen").as_list()) {
|
||||
if (item->is_int()) {
|
||||
@@ -709,6 +656,7 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
|
||||
|
||||
this->persistent_game_idle_timeout_usecs = json.get_int("PersistentGameIdleTimeout", this->persistent_game_idle_timeout_usecs);
|
||||
this->cheat_mode_behavior = parse_behavior_switch("CheatModeBehavior", this->cheat_mode_behavior);
|
||||
this->default_rare_notifs_enabled = json.get_bool("RareNotificationsEnabledByDefault", this->default_rare_notifs_enabled);
|
||||
this->ep3_send_function_call_enabled = json.get_bool("EnableEpisode3SendFunctionCall", this->ep3_send_function_call_enabled);
|
||||
this->catch_handler_exceptions = json.get_bool("CatchHandlerExceptions", this->catch_handler_exceptions);
|
||||
|
||||
@@ -747,12 +695,17 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
|
||||
|
||||
try {
|
||||
for (const auto& it : json.get_dict("CardAuctionPool")) {
|
||||
uint16_t card_id;
|
||||
try {
|
||||
card_id = this->ep3_card_index->definition_for_name_normalized(it.first)->def.card_id;
|
||||
} catch (const out_of_range&) {
|
||||
throw runtime_error(string_printf("Ep3 card \"%s\" in auction pool does not exist", it.first.c_str()));
|
||||
}
|
||||
this->ep3_card_auction_pool.emplace_back(
|
||||
CardAuctionPoolEntry{
|
||||
.probability = static_cast<uint64_t>(it.second->at(0).as_int()),
|
||||
.card_id = 0,
|
||||
.min_price = static_cast<uint16_t>(it.second->at(1).as_int()),
|
||||
.card_name = it.first});
|
||||
.card_id = card_id,
|
||||
.min_price = static_cast<uint16_t>(it.second->at(1).as_int())});
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
@@ -763,11 +716,18 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
|
||||
if (ep3_trap_cards_json.size() != 5) {
|
||||
throw runtime_error("Episode3TrapCards must be a list of 5 lists");
|
||||
}
|
||||
this->ep3_trap_card_names.clear();
|
||||
for (const auto& trap_type_it : ep3_trap_cards_json) {
|
||||
auto& names = this->ep3_trap_card_names.emplace_back();
|
||||
for (const auto& card_it : trap_type_it->as_list()) {
|
||||
names.emplace_back(card_it->as_string());
|
||||
for (size_t trap_type = 0; trap_type < 5; trap_type++) {
|
||||
auto& trap_card_ids = this->ep3_trap_card_ids[trap_type];
|
||||
for (const auto& card_it : ep3_trap_cards_json.at(trap_type)->as_list()) {
|
||||
try {
|
||||
const auto& card = this->ep3_card_index->definition_for_name_normalized(card_it->as_string());
|
||||
if (card->def.type != Episode3::CardType::ASSIST) {
|
||||
throw runtime_error(string_printf("Ep3 card \"%s\" in trap card list is not an assist card", name.c_str()));
|
||||
}
|
||||
trap_card_ids.emplace_back(card->def.card_id);
|
||||
} catch (const out_of_range&) {
|
||||
throw runtime_error(string_printf("Ep3 card \"%s\" in trap card list does not exist", name.c_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -775,6 +735,7 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
|
||||
}
|
||||
|
||||
if (!this->is_replay) {
|
||||
this->ep3_lobby_banners.clear();
|
||||
for (const auto& it : json.get("Episode3LobbyBanners", JSON::list()).as_list()) {
|
||||
Image img("system/ep3/banners/" + it->at(2).as_string());
|
||||
string gvm = encode_gvm(img, img.get_has_alpha() ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565);
|
||||
@@ -829,7 +790,7 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
|
||||
for (const auto& difficulty_it : type_it->as_list()) {
|
||||
auto& difficulty_res = type_res.emplace_back();
|
||||
for (const auto& item_it : difficulty_it->as_list()) {
|
||||
difficulty_res.emplace_back(this->item_name_index->parse_item_description(Version::BB_V4, item_it->as_string()));
|
||||
difficulty_res.emplace_back(this->parse_item_description(Version::BB_V4, item_it->as_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -840,22 +801,22 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
|
||||
for (const auto& it : json.get_list("QuestF95FResultItems")) {
|
||||
auto& list = it->as_list();
|
||||
size_t price = list.at(0)->as_int();
|
||||
this->quest_F95F_results.emplace_back(make_pair(price, this->item_name_index->parse_item_description(Version::BB_V4, list.at(1)->as_string())));
|
||||
this->quest_F95F_results.emplace_back(make_pair(price, this->parse_item_description(Version::BB_V4, list.at(1)->as_string())));
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
this->quest_F960_success_results.clear();
|
||||
this->quest_F960_failure_results = QuestF960Result(json.at("QuestF960FailureResultItems"), this->item_name_index);
|
||||
this->quest_F960_failure_results = QuestF960Result(json.at("QuestF960FailureResultItems"), this->item_name_index(Version::BB_V4));
|
||||
for (const auto& it : json.get_list("QuestF960SuccessResultItems")) {
|
||||
this->quest_F960_success_results.emplace_back(*it, this->item_name_index);
|
||||
this->quest_F960_success_results.emplace_back(*it, this->item_name_index(Version::BB_V4));
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
this->secret_lottery_results.clear();
|
||||
for (const auto& it : json.get_list("SecretLotteryResultItems")) {
|
||||
this->secret_lottery_results.emplace_back(this->item_name_index->parse_item_description(Version::BB_V4, it->as_string()));
|
||||
this->secret_lottery_results.emplace_back(this->parse_item_description(Version::BB_V4, it->as_string()));
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
@@ -864,13 +825,11 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
|
||||
|
||||
set_log_levels_from_json(json.get("LogLevels", JSON::dict()));
|
||||
|
||||
if (!is_reload) {
|
||||
try {
|
||||
this->run_shell_behavior = json.at("RunInteractiveShell").as_bool()
|
||||
? ServerState::RunShellBehavior::ALWAYS
|
||||
: ServerState::RunShellBehavior::NEVER;
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
this->run_shell_behavior = json.at("RunInteractiveShell").as_bool()
|
||||
? ServerState::RunShellBehavior::ALWAYS
|
||||
: ServerState::RunShellBehavior::NEVER;
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
this->allow_dc_pc_games = json.get_bool("AllowDCPCGames", this->allow_dc_pc_games);
|
||||
@@ -888,13 +847,11 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
|
||||
|
||||
this->ep3_menu_song = json.get_int("Episode3MenuSong", this->ep3_menu_song);
|
||||
|
||||
if (!is_reload) {
|
||||
try {
|
||||
this->quest_category_index = make_shared<QuestCategoryIndex>(json.at("QuestCategories"));
|
||||
} catch (const exception& e) {
|
||||
throw runtime_error(string_printf(
|
||||
"QuestCategories is missing or invalid in config.json (%s) - see config.example.json for an example", e.what()));
|
||||
}
|
||||
try {
|
||||
this->quest_category_index = make_shared<QuestCategoryIndex>(json.at("QuestCategories"));
|
||||
} catch (const exception& e) {
|
||||
throw runtime_error(string_printf(
|
||||
"QuestCategories is missing or invalid in config.json (%s) - see config.example.json for an example", e.what()));
|
||||
}
|
||||
|
||||
config_log.info("Creating menus");
|
||||
@@ -1046,6 +1003,8 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
this->config_loaded = true;
|
||||
}
|
||||
|
||||
void ServerState::load_bb_private_keys() {
|
||||
@@ -1068,7 +1027,6 @@ void ServerState::load_licenses() {
|
||||
void ServerState::load_teams() {
|
||||
config_log.info("Indexing teams");
|
||||
this->team_index = make_shared<TeamIndex>("system/teams", this->team_reward_defs_json);
|
||||
this->team_reward_defs_json = nullptr;
|
||||
}
|
||||
|
||||
void ServerState::load_patch_indexes() {
|
||||
@@ -1109,9 +1067,27 @@ void ServerState::load_level_table() {
|
||||
this->level_table = make_shared<LevelTableV4>(*this->load_bb_file("PlyLevelTbl.prs"), true);
|
||||
}
|
||||
|
||||
shared_ptr<WordSelectTable> ServerState::load_word_select_table_from_system() {
|
||||
void ServerState::load_text_index() {
|
||||
this->text_index = make_shared<TextIndex>("system/text-sets", [&](Version version, const string& filename) -> shared_ptr<const string> {
|
||||
try {
|
||||
if (version == Version::BB_V4) {
|
||||
return this->load_bb_file(filename);
|
||||
} else {
|
||||
return this->pc_patch_file_index->get("Media/PSO/" + filename)->load_data();
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
return nullptr;
|
||||
} catch (const cannot_open_file&) {
|
||||
return nullptr;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ServerState::load_word_select_table() {
|
||||
config_log.info("Loading Word Select table");
|
||||
|
||||
vector<vector<string>> name_alias_lists;
|
||||
auto json = JSON::parse(load_file("system/word-select/name-alias-lists.json"));
|
||||
auto json = JSON::parse(load_file("system/text-sets/ws-name-alias-lists.json"));
|
||||
for (const auto& coll_it : json.as_list()) {
|
||||
auto& coll = name_alias_lists.emplace_back();
|
||||
for (const auto& str_it : coll_it->as_list()) {
|
||||
@@ -1119,60 +1095,117 @@ shared_ptr<WordSelectTable> ServerState::load_word_select_table_from_system() {
|
||||
}
|
||||
}
|
||||
|
||||
config_log.info("(Word select) Loading pc_unitxt.prs");
|
||||
vector<vector<string>> pc_unitxt_data = parse_unicode_text_set(load_file("system/word-select/pc_unitxt.prs"));
|
||||
config_log.info("(Word select) Loading bb_unitxt_ws.prs");
|
||||
vector<vector<string>> bb_unitxt_data = parse_unicode_text_set(load_file("system/word-select/bb_unitxt_ws.prs"));
|
||||
vector<string> pc_unitxt_collection = std::move(pc_unitxt_data.at(35));
|
||||
vector<string> bb_unitxt_collection = std::move(bb_unitxt_data.at(0));
|
||||
const vector<string>* pc_unitxt_collection = nullptr;
|
||||
const vector<string>* bb_unitxt_collection = nullptr;
|
||||
unique_ptr<UnicodeTextSet> pc_unitxt_data;
|
||||
if (this->text_index) {
|
||||
config_log.info("(Word select) Using PC_V2 unitxt_e.prs from text index");
|
||||
pc_unitxt_collection = &this->text_index->get(Version::PC_V2, 1, 35);
|
||||
} else {
|
||||
config_log.info("(Word select) Loading PC_V2 unitxt_e.prs");
|
||||
pc_unitxt_data = make_unique<UnicodeTextSet>(load_file("system/text-sets/pc-v2/unitxt_e.prs"));
|
||||
pc_unitxt_collection = &pc_unitxt_data->get(35);
|
||||
}
|
||||
config_log.info("(Word select) Loading BB_V4 unitxt_ws_e.prs");
|
||||
auto bb_unitxt_data = make_unique<UnicodeTextSet>(load_file("system/text-sets/bb-v4/unitxt_ws_e.prs"));
|
||||
bb_unitxt_collection = &bb_unitxt_data->get(0);
|
||||
|
||||
config_log.info("(Word select) Loading DC_NTE data");
|
||||
WordSelectSet dc_nte_ws(load_file("system/word-select/dc_nte_ws_data.bin"), Version::DC_NTE, nullptr, true);
|
||||
WordSelectSet dc_nte_ws(load_file("system/text-sets/dc-nte/ws_data.bin"), Version::DC_NTE, nullptr, true);
|
||||
config_log.info("(Word select) Loading DC_V1_11_2000_PROTOTYPE data");
|
||||
WordSelectSet dc_112000_ws(load_file("system/word-select/dc_112000_ws_data.bin"), Version::DC_V1_11_2000_PROTOTYPE, nullptr, false);
|
||||
WordSelectSet dc_112000_ws(load_file("system/text-sets/dc-11-2000/ws_data.bin"), Version::DC_V1_11_2000_PROTOTYPE, nullptr, false);
|
||||
config_log.info("(Word select) Loading DC_V1 data");
|
||||
WordSelectSet dc_v1_ws(load_file("system/word-select/dcv1_ws_data.bin"), Version::DC_V1, nullptr, false);
|
||||
WordSelectSet dc_v1_ws(load_file("system/text-sets/dc-v1/ws_data.bin"), Version::DC_V1, nullptr, false);
|
||||
config_log.info("(Word select) Loading DC_V2 data");
|
||||
WordSelectSet dc_v2_ws(load_file("system/word-select/dcv2_ws_data.bin"), Version::DC_V2, nullptr, false);
|
||||
WordSelectSet dc_v2_ws(load_file("system/text-sets/dc-v2/ws_data.bin"), Version::DC_V2, nullptr, false);
|
||||
config_log.info("(Word select) Loading PC_NTE data");
|
||||
WordSelectSet pc_nte_ws(load_file("system/word-select/pc_nte_ws_data.bin"), Version::PC_NTE, &pc_unitxt_collection, false);
|
||||
WordSelectSet pc_nte_ws(load_file("system/text-sets/pc-nte/ws_data.bin"), Version::PC_NTE, pc_unitxt_collection, false);
|
||||
config_log.info("(Word select) Loading PC_V2 data");
|
||||
WordSelectSet pc_v2_ws(load_file("system/word-select/pc_ws_data.bin"), Version::PC_V2, &pc_unitxt_collection, false);
|
||||
WordSelectSet pc_v2_ws(load_file("system/text-sets/pc-v2/ws_data.bin"), Version::PC_V2, pc_unitxt_collection, false);
|
||||
config_log.info("(Word select) Loading GC_NTE data");
|
||||
WordSelectSet gc_nte_ws(load_file("system/word-select/gc_nte_ws_data.bin"), Version::GC_NTE, nullptr, false);
|
||||
WordSelectSet gc_nte_ws(load_file("system/text-sets/gc-nte/ws_data.bin"), Version::GC_NTE, nullptr, false);
|
||||
config_log.info("(Word select) Loading GC_V3 data");
|
||||
WordSelectSet gc_v3_ws(load_file("system/word-select/gc_ws_data.bin"), Version::GC_V3, nullptr, false);
|
||||
WordSelectSet gc_v3_ws(load_file("system/text-sets/gc-v3/ws_data.bin"), Version::GC_V3, nullptr, false);
|
||||
config_log.info("(Word select) Loading GC_EP3_NTE data");
|
||||
WordSelectSet gc_ep3_nte_ws(load_file("system/word-select/gc_ep3_nte_ws_data.bin"), Version::GC_EP3_NTE, nullptr, false);
|
||||
WordSelectSet gc_ep3_nte_ws(load_file("system/text-sets/gc-ep3-nte/ws_data.bin"), Version::GC_EP3_NTE, nullptr, false);
|
||||
config_log.info("(Word select) Loading GC_EP3 data");
|
||||
WordSelectSet gc_ep3_ws(load_file("system/word-select/gc_ep3_ws_data.bin"), Version::GC_EP3, nullptr, false);
|
||||
WordSelectSet gc_ep3_ws(load_file("system/text-sets/gc-ep3/ws_data.bin"), Version::GC_EP3, nullptr, false);
|
||||
config_log.info("(Word select) Loading XB_V3 data");
|
||||
WordSelectSet xb_v3_ws(load_file("system/word-select/xb_ws_data.bin"), Version::XB_V3, nullptr, false);
|
||||
WordSelectSet xb_v3_ws(load_file("system/text-sets/xb-v3/ws_data.bin"), Version::XB_V3, nullptr, false);
|
||||
config_log.info("(Word select) Loading BB_V4 data");
|
||||
WordSelectSet bb_v4_ws(load_file("system/word-select/bb_ws_data.bin"), Version::BB_V4, &bb_unitxt_collection, false);
|
||||
WordSelectSet bb_v4_ws(load_file("system/text-sets/bb-v4/ws_data.bin"), Version::BB_V4, bb_unitxt_collection, false);
|
||||
|
||||
config_log.info("(Word select) Generating table");
|
||||
return make_shared<WordSelectTable>(
|
||||
this->word_select_table = make_shared<WordSelectTable>(
|
||||
dc_nte_ws, dc_112000_ws, dc_v1_ws, dc_v2_ws,
|
||||
pc_nte_ws, pc_v2_ws, gc_nte_ws, gc_v3_ws,
|
||||
gc_ep3_nte_ws, gc_ep3_ws, xb_v3_ws, bb_v4_ws,
|
||||
name_alias_lists);
|
||||
}
|
||||
|
||||
void ServerState::load_word_select_table() {
|
||||
config_log.info("Loading Word Select table");
|
||||
this->word_select_table = this->load_word_select_table_from_system();
|
||||
shared_ptr<ItemNameIndex> ServerState::create_item_name_index_for_version(
|
||||
Version version, shared_ptr<const ItemParameterTable> pmt, shared_ptr<const TextIndex> text_index) {
|
||||
switch (version) {
|
||||
case Version::DC_NTE:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::DC_NTE, 0, 2));
|
||||
case Version::DC_V1_11_2000_PROTOTYPE:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::DC_V1_11_2000_PROTOTYPE, 1, 2));
|
||||
case Version::DC_V1:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::DC_V1, 1, 2));
|
||||
case Version::DC_V2:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::DC_V2, 1, 3));
|
||||
case Version::PC_NTE:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::PC_NTE, 1, 3));
|
||||
case Version::PC_V2:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::PC_V2, 1, 3));
|
||||
case Version::GC_NTE:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::GC_NTE, 1, 0));
|
||||
case Version::GC_V3:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::GC_V3, 1, 0));
|
||||
case Version::XB_V3:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::XB_V3, 1, 0));
|
||||
case Version::BB_V4:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::BB_V4, 1, 1));
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ServerState::load_item_name_index() {
|
||||
config_log.info("Loading item name index");
|
||||
this->item_name_index = make_shared<ItemNameIndex>(
|
||||
JSON::parse(load_file("system/item-tables/names-v2.json")),
|
||||
JSON::parse(load_file("system/item-tables/names-v3.json")),
|
||||
JSON::parse(load_file("system/item-tables/names-v4.json")));
|
||||
void ServerState::load_item_name_indexes() {
|
||||
config_log.info("Generating item name indexes");
|
||||
// TODO: Get ItemPMT files for the versions for which we don't have them
|
||||
// (especially DC_V1) and add support for them. Currently we only have three
|
||||
// ItemPMTs (PC, GC, and BB), so we can't use them to generate all the name
|
||||
// indexes.
|
||||
|
||||
auto pc_v2_index = create_item_name_index_for_version(
|
||||
Version::PC_V2, this->item_parameter_table(Version::PC_V2), this->text_index);
|
||||
this->set_item_name_index(Version::DC_NTE, pc_v2_index);
|
||||
this->set_item_name_index(Version::DC_V1, pc_v2_index);
|
||||
this->set_item_name_index(Version::DC_V2, pc_v2_index);
|
||||
this->set_item_name_index(Version::PC_NTE, pc_v2_index);
|
||||
this->set_item_name_index(Version::PC_V2, pc_v2_index);
|
||||
|
||||
// All tools are stackable on 11/2000, so make a separate index (still using
|
||||
// V2 data) with the correct version
|
||||
auto dc_112000_index = make_shared<ItemNameIndex>(
|
||||
Version::DC_V1_11_2000_PROTOTYPE,
|
||||
this->item_parameter_table(Version::PC_V2),
|
||||
this->text_index->get(Version::PC_V2, 1, 3));
|
||||
this->set_item_name_index(Version::DC_V1_11_2000_PROTOTYPE, dc_112000_index);
|
||||
|
||||
auto gc_v3_index = create_item_name_index_for_version(
|
||||
Version::GC_V3, this->item_parameter_table(Version::GC_V3), this->text_index);
|
||||
this->set_item_name_index(Version::GC_NTE, gc_v3_index);
|
||||
this->set_item_name_index(Version::GC_V3, gc_v3_index);
|
||||
this->set_item_name_index(Version::XB_V3, gc_v3_index);
|
||||
|
||||
auto bb_v4_index = create_item_name_index_for_version(
|
||||
Version::BB_V4, this->item_parameter_table(Version::BB_V4), this->text_index);
|
||||
this->set_item_name_index(Version::BB_V4, bb_v4_index);
|
||||
}
|
||||
|
||||
void ServerState::load_item_tables() {
|
||||
void ServerState::load_drop_tables() {
|
||||
config_log.info("Loading rare item sets");
|
||||
unordered_map<string, shared_ptr<const RareItemSet>> new_rare_item_sets;
|
||||
for (const auto& filename : list_directory_sorted("system/item-tables")) {
|
||||
@@ -1186,16 +1219,16 @@ void ServerState::load_item_tables() {
|
||||
|
||||
if (ends_with(filename, "-v1.json")) {
|
||||
config_log.info("Loading v1 JSON rare item table %s", filename.c_str());
|
||||
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(JSON::parse(load_file(path)), Version::DC_V1, this->item_name_index));
|
||||
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(JSON::parse(load_file(path)), this->item_name_index(Version::DC_V1)));
|
||||
} else if (ends_with(filename, "-v2.json")) {
|
||||
config_log.info("Loading v2 JSON rare item table %s", filename.c_str());
|
||||
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(JSON::parse(load_file(path)), Version::PC_V2, this->item_name_index));
|
||||
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(JSON::parse(load_file(path)), this->item_name_index(Version::PC_V2)));
|
||||
} else if (ends_with(filename, "-v3.json")) {
|
||||
config_log.info("Loading v3 JSON rare item table %s", filename.c_str());
|
||||
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(JSON::parse(load_file(path)), Version::GC_V3, this->item_name_index));
|
||||
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(JSON::parse(load_file(path)), this->item_name_index(Version::GC_V3)));
|
||||
} else if (ends_with(filename, "-v4.json")) {
|
||||
config_log.info("Loading v4 JSON rare item table %s", filename.c_str());
|
||||
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(JSON::parse(load_file(path)), Version::BB_V4, this->item_name_index));
|
||||
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(JSON::parse(load_file(path)), this->item_name_index(Version::BB_V4)));
|
||||
|
||||
} else if (ends_with(filename, ".afs")) {
|
||||
config_log.info("Loading AFS rare item table %s", filename.c_str());
|
||||
@@ -1255,7 +1288,9 @@ void ServerState::load_item_tables() {
|
||||
config_log.info("Loading tekker adjustment table");
|
||||
auto tekker_data = make_shared<string>(load_file("system/item-tables/JudgeItem-gc.rel"));
|
||||
this->tekker_adjustment_set = make_shared<TekkerAdjustmentSet>(tekker_data);
|
||||
}
|
||||
|
||||
void ServerState::load_item_definitions() {
|
||||
config_log.info("Loading item definition tables");
|
||||
auto pmt_data_v2 = make_shared<string>(prs_decompress(load_file("system/item-tables/ItemPMT-v2.prs")));
|
||||
this->item_parameter_table_v2 = make_shared<ItemParameterTable>(pmt_data_v2, ItemParameterTable::Version::V2);
|
||||
@@ -1298,37 +1333,6 @@ void ServerState::load_ep3_data() {
|
||||
config_log.info("Loaded Episode 3 tournament state");
|
||||
}
|
||||
|
||||
void ServerState::resolve_ep3_card_names() {
|
||||
config_log.info("Resolving Episode 3 card names");
|
||||
for (auto& e : this->ep3_card_auction_pool) {
|
||||
try {
|
||||
const auto& card = this->ep3_card_index->definition_for_name_normalized(e.card_name);
|
||||
e.card_id = card->def.card_id;
|
||||
} catch (const out_of_range&) {
|
||||
throw runtime_error(string_printf("Ep3 card \"%s\" in auction pool does not exist", e.card_name.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t z = 0; z < this->ep3_trap_card_ids.size(); z++) {
|
||||
auto& ids = this->ep3_trap_card_ids[z];
|
||||
ids.clear();
|
||||
if (z < this->ep3_trap_card_names.size()) {
|
||||
auto& names = this->ep3_trap_card_names[z];
|
||||
for (const auto& name : names) {
|
||||
try {
|
||||
const auto& card = this->ep3_card_index->definition_for_name_normalized(name);
|
||||
if (card->def.type != Episode3::CardType::ASSIST) {
|
||||
throw runtime_error(string_printf("Ep3 card \"%s\" in trap card list is not an assist card", name.c_str()));
|
||||
}
|
||||
ids.emplace_back(card->def.card_id);
|
||||
} catch (const out_of_range&) {
|
||||
throw runtime_error(string_printf("Ep3 card \"%s\" in trap card list does not exist", name.c_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ServerState::load_quest_index() {
|
||||
config_log.info("Collecting quests");
|
||||
this->default_quest_index = make_shared<QuestIndex>("system/quests", this->quest_category_index, false);
|
||||
@@ -1346,14 +1350,137 @@ void ServerState::load_dol_files() {
|
||||
this->dol_file_index = make_shared<DOLFileIndex>("system/dol");
|
||||
}
|
||||
|
||||
shared_ptr<const vector<string>> ServerState::information_contents_for_client(shared_ptr<const Client> c) const {
|
||||
return is_v1_or_v2(c->version()) ? this->information_contents_v2 : this->information_contents_v3;
|
||||
void ServerState::create_default_lobbies() {
|
||||
if (this->default_lobbies_created) {
|
||||
return;
|
||||
}
|
||||
this->default_lobbies_created = true;
|
||||
|
||||
vector<shared_ptr<Lobby>> non_v1_only_lobbies;
|
||||
vector<shared_ptr<Lobby>> ep3_only_lobbies;
|
||||
|
||||
for (size_t x = 0; x < 20; x++) {
|
||||
auto lobby_name = string_printf("LOBBY%zu", x + 1);
|
||||
bool allow_v1 = (x <= 9);
|
||||
bool allow_non_ep3 = (x <= 14);
|
||||
|
||||
shared_ptr<Lobby> l = this->create_lobby(false);
|
||||
l->event = this->pre_lobby_event;
|
||||
l->set_flag(Lobby::Flag::PUBLIC);
|
||||
l->set_flag(Lobby::Flag::DEFAULT);
|
||||
l->set_flag(Lobby::Flag::PERSISTENT);
|
||||
if (allow_non_ep3) {
|
||||
if (allow_v1) {
|
||||
l->allow_version(Version::DC_NTE);
|
||||
l->allow_version(Version::DC_V1_11_2000_PROTOTYPE);
|
||||
l->allow_version(Version::DC_V1);
|
||||
}
|
||||
l->allow_version(Version::DC_V2);
|
||||
l->allow_version(Version::PC_NTE);
|
||||
l->allow_version(Version::PC_V2);
|
||||
l->allow_version(Version::GC_NTE);
|
||||
l->allow_version(Version::GC_V3);
|
||||
l->allow_version(Version::XB_V3);
|
||||
l->allow_version(Version::BB_V4);
|
||||
}
|
||||
l->allow_version(Version::GC_EP3_NTE);
|
||||
l->allow_version(Version::GC_EP3);
|
||||
|
||||
l->block = x + 1;
|
||||
l->name = lobby_name;
|
||||
l->max_clients = 12;
|
||||
if (!allow_non_ep3) {
|
||||
l->episode = Episode::EP3;
|
||||
}
|
||||
|
||||
if (allow_non_ep3) {
|
||||
this->public_lobby_search_order.emplace_back(l);
|
||||
} else {
|
||||
ep3_only_lobbies.emplace_back(l);
|
||||
}
|
||||
}
|
||||
|
||||
// Annoyingly, the CARD lobbies should be searched first, but are sent at the
|
||||
// end of the lobby list command, so we have to change the search order
|
||||
// manually here.
|
||||
this->public_lobby_search_order.insert(
|
||||
this->public_lobby_search_order.begin(),
|
||||
ep3_only_lobbies.begin(),
|
||||
ep3_only_lobbies.end());
|
||||
}
|
||||
|
||||
shared_ptr<const QuestIndex> ServerState::quest_index_for_version(Version version) const {
|
||||
return is_ep3(version) ? this->ep3_download_quest_index : this->default_quest_index;
|
||||
}
|
||||
void ServerState::create_load_step_graph() {
|
||||
this->load_step_graph.add_step("all", {}, nullptr);
|
||||
|
||||
void ServerState::dispatch_destroy_lobbies(evutil_socket_t, short, void* ctx) {
|
||||
reinterpret_cast<ServerState*>(ctx)->lobbies_to_destroy.clear();
|
||||
// In: none
|
||||
// Out: all_addresses
|
||||
this->load_step_graph.add_step("network_addresses", {"all"}, bind(&ServerState::collect_network_addresses, this));
|
||||
|
||||
// In: none
|
||||
// Out: bb_private_keys
|
||||
this->load_step_graph.add_step("bb_private_keys", {"all"}, bind(&ServerState::load_bb_private_keys, this));
|
||||
|
||||
// In: none
|
||||
// Out: license_index
|
||||
this->load_step_graph.add_step("licenses", {"all"}, bind(&ServerState::load_licenses, this));
|
||||
|
||||
// In: none
|
||||
// Out: pc_patch_file_index, bb_patch_file_index, bb_data_gsl
|
||||
this->load_step_graph.add_step("patch_indexes", {"all"}, bind(&ServerState::load_patch_indexes, this));
|
||||
|
||||
// In: none
|
||||
// Out: ep3_map_index, ep3_card_index, ep3_card_index_trial, ep3_com_deck_index, ep3_tournament_index
|
||||
this->load_step_graph.add_step("ep3_data", {"all"}, bind(&ServerState::load_ep3_data, this));
|
||||
|
||||
// In: none
|
||||
// Out: function_code_index
|
||||
this->load_step_graph.add_step("functions", {"all"}, bind(&ServerState::compile_functions, this));
|
||||
|
||||
// In: none
|
||||
// Out: dol_file_index
|
||||
this->load_step_graph.add_step("dol_files", {"all"}, bind(&ServerState::load_dol_files, this));
|
||||
|
||||
// In: none
|
||||
// Out: lobbies
|
||||
this->load_step_graph.add_step("lobbies", {"all"}, bind(&ServerState::create_default_lobbies, this));
|
||||
|
||||
// In: bb_patch_file_index
|
||||
// Out: battle_params
|
||||
this->load_step_graph.add_step("battle_params", {"all", "patch_indexes"}, bind(&ServerState::load_battle_params, this));
|
||||
|
||||
// In: bb_patch_file_index
|
||||
// Out: level_table
|
||||
this->load_step_graph.add_step("level_table", {"all", "patch_indexes"}, bind(&ServerState::load_level_table, this));
|
||||
|
||||
// In: bb_patch_file_index
|
||||
// Out: text_index
|
||||
this->load_step_graph.add_step("text_index", {"all", "patch_indexes"}, bind(&ServerState::load_text_index, this));
|
||||
|
||||
// In: text_index (optional)
|
||||
// Out: word_select_table
|
||||
this->load_step_graph.add_step("word_select_table", {"all"}, bind(&ServerState::load_word_select_table, this));
|
||||
|
||||
// In: none
|
||||
// Out: item_parameter_tables, mag_evolution_table
|
||||
this->load_step_graph.add_step("item_definitions", {"all"}, bind(&ServerState::load_item_definitions, this));
|
||||
|
||||
// In: text_index, item_parameter_tables
|
||||
// Out: item_name_indexes
|
||||
this->load_step_graph.add_step("item_name_indexes", {"all", "text_index", "item_definitions"}, bind(&ServerState::load_item_name_indexes, this));
|
||||
|
||||
// In: none
|
||||
// Out: rare_item_sets, common_item_sets, armor_random_set, tool_random_set, weapon_random_sets, tekker_adjustment_set
|
||||
this->load_step_graph.add_step("drop_tables", {"all", "item_definitions", "item_name_indexes"}, bind(&ServerState::load_drop_tables, this));
|
||||
|
||||
// In: all_addresses, ep3_card_index, item_name_indexes
|
||||
// Out: config, ep3_lobby_banners, quest_category_index, information menus, proxy destinations menus, team_reward_defs_json
|
||||
this->load_step_graph.add_step("config", {"all", "network_addresses", "ep3_data", "item_name_indexes"}, bind(&ServerState::load_config, this));
|
||||
|
||||
// In: team_reward_defs_json
|
||||
// Out: team_index
|
||||
this->load_step_graph.add_step("teams", {"all", "config"}, bind(&ServerState::load_teams, this));
|
||||
|
||||
// In: quest_category_index
|
||||
// Out: default_quest_index, ep3_download_quest_index
|
||||
this->load_step_graph.add_step("quest_index", {"all", "config"}, bind(&ServerState::load_quest_index, this));
|
||||
}
|
||||
|
||||
+85
-64
@@ -25,6 +25,7 @@
|
||||
#include "Menu.hh"
|
||||
#include "PlayerFilesManager.hh"
|
||||
#include "Quest.hh"
|
||||
#include "StepGraph.hh"
|
||||
#include "TeamIndex.hh"
|
||||
#include "WordSelectTable.hh"
|
||||
|
||||
@@ -34,6 +35,7 @@ class Server;
|
||||
|
||||
struct PortConfiguration {
|
||||
std::string name;
|
||||
std::string addr; // Blank = listen on all interfaces (default)
|
||||
uint16_t port;
|
||||
Version version;
|
||||
ServerBehavior behavior;
|
||||
@@ -63,52 +65,58 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::shared_ptr<struct event_base> base;
|
||||
|
||||
std::string config_filename;
|
||||
bool is_replay;
|
||||
bool is_replay = false;
|
||||
bool config_loaded = false;
|
||||
bool default_lobbies_created = false;
|
||||
|
||||
StepGraph load_step_graph;
|
||||
|
||||
std::string name;
|
||||
std::unordered_map<std::string, std::shared_ptr<PortConfiguration>> name_to_port_config;
|
||||
std::unordered_map<uint16_t, std::shared_ptr<PortConfiguration>> number_to_port_config;
|
||||
std::string username;
|
||||
uint16_t dns_server_port;
|
||||
std::string dns_server_addr;
|
||||
uint16_t dns_server_port = 0;
|
||||
std::vector<std::string> ip_stack_addresses;
|
||||
std::vector<std::string> ppp_stack_addresses;
|
||||
bool ip_stack_debug;
|
||||
bool allow_unregistered_users;
|
||||
bool allow_pc_nte;
|
||||
bool use_temp_licenses_for_prototypes;
|
||||
bool allow_dc_pc_games;
|
||||
bool allow_gc_xb_games;
|
||||
uint8_t allowed_drop_modes_v1_v2_normal;
|
||||
uint8_t allowed_drop_modes_v1_v2_battle;
|
||||
uint8_t allowed_drop_modes_v1_v2_challenge;
|
||||
uint8_t allowed_drop_modes_v3_normal;
|
||||
uint8_t allowed_drop_modes_v3_battle;
|
||||
uint8_t allowed_drop_modes_v3_challenge;
|
||||
uint8_t allowed_drop_modes_v4_normal;
|
||||
uint8_t allowed_drop_modes_v4_battle;
|
||||
uint8_t allowed_drop_modes_v4_challenge;
|
||||
Lobby::DropMode default_drop_mode_v1_v2_normal;
|
||||
Lobby::DropMode default_drop_mode_v1_v2_battle;
|
||||
Lobby::DropMode default_drop_mode_v1_v2_challenge;
|
||||
Lobby::DropMode default_drop_mode_v3_normal;
|
||||
Lobby::DropMode default_drop_mode_v3_battle;
|
||||
Lobby::DropMode default_drop_mode_v3_challenge;
|
||||
Lobby::DropMode default_drop_mode_v4_normal;
|
||||
Lobby::DropMode default_drop_mode_v4_battle;
|
||||
Lobby::DropMode default_drop_mode_v4_challenge;
|
||||
bool ip_stack_debug = false;
|
||||
bool allow_unregistered_users = false;
|
||||
bool allow_pc_nte = false;
|
||||
bool use_temp_licenses_for_prototypes = true;
|
||||
bool allow_dc_pc_games = true;
|
||||
bool allow_gc_xb_games = true;
|
||||
uint8_t allowed_drop_modes_v1_v2_normal = 0x1F;
|
||||
uint8_t allowed_drop_modes_v1_v2_battle = 0x07;
|
||||
uint8_t allowed_drop_modes_v1_v2_challenge = 0x07;
|
||||
uint8_t allowed_drop_modes_v3_normal = 0x1F;
|
||||
uint8_t allowed_drop_modes_v3_battle = 0x07;
|
||||
uint8_t allowed_drop_modes_v3_challenge = 0x07;
|
||||
uint8_t allowed_drop_modes_v4_normal = 0x1D; // CLIENT not allowed
|
||||
uint8_t allowed_drop_modes_v4_battle = 0x05;
|
||||
uint8_t allowed_drop_modes_v4_challenge = 0x05;
|
||||
Lobby::DropMode default_drop_mode_v1_v2_normal = Lobby::DropMode::CLIENT;
|
||||
Lobby::DropMode default_drop_mode_v1_v2_battle = Lobby::DropMode::CLIENT;
|
||||
Lobby::DropMode default_drop_mode_v1_v2_challenge = Lobby::DropMode::CLIENT;
|
||||
Lobby::DropMode default_drop_mode_v3_normal = Lobby::DropMode::CLIENT;
|
||||
Lobby::DropMode default_drop_mode_v3_battle = Lobby::DropMode::CLIENT;
|
||||
Lobby::DropMode default_drop_mode_v3_challenge = Lobby::DropMode::CLIENT;
|
||||
Lobby::DropMode default_drop_mode_v4_normal = Lobby::DropMode::SERVER_SHARED;
|
||||
Lobby::DropMode default_drop_mode_v4_battle = Lobby::DropMode::SERVER_SHARED;
|
||||
Lobby::DropMode default_drop_mode_v4_challenge = Lobby::DropMode::SERVER_SHARED;
|
||||
QuestFlagsForDifficulty quest_flag_persist_mask;
|
||||
uint64_t persistent_game_idle_timeout_usecs;
|
||||
bool ep3_send_function_call_enabled;
|
||||
bool catch_handler_exceptions;
|
||||
bool ep3_infinite_meseta;
|
||||
std::vector<uint32_t> ep3_defeat_player_meseta_rewards;
|
||||
std::vector<uint32_t> ep3_defeat_com_meseta_rewards;
|
||||
uint32_t ep3_final_round_meseta_bonus;
|
||||
bool ep3_jukebox_is_free;
|
||||
uint32_t ep3_behavior_flags;
|
||||
bool hide_download_commands;
|
||||
RunShellBehavior run_shell_behavior;
|
||||
BehaviorSwitch cheat_mode_behavior;
|
||||
uint64_t persistent_game_idle_timeout_usecs = 0;
|
||||
bool ep3_send_function_call_enabled = false;
|
||||
bool catch_handler_exceptions = true;
|
||||
bool ep3_infinite_meseta = false;
|
||||
std::vector<uint32_t> ep3_defeat_player_meseta_rewards = {400, 500, 600, 700, 800};
|
||||
std::vector<uint32_t> ep3_defeat_com_meseta_rewards = {100, 200, 300, 400, 500};
|
||||
uint32_t ep3_final_round_meseta_bonus = 300;
|
||||
bool ep3_jukebox_is_free = false;
|
||||
uint32_t ep3_behavior_flags = 0;
|
||||
bool hide_download_commands = true;
|
||||
RunShellBehavior run_shell_behavior = RunShellBehavior::DEFAULT;
|
||||
BehaviorSwitch cheat_mode_behavior = BehaviorSwitch::OFF_BY_DEFAULT;
|
||||
bool default_rare_notifs_enabled = false;
|
||||
std::vector<std::shared_ptr<const PSOBBEncryption::KeyFile>> bb_private_keys;
|
||||
std::shared_ptr<const FunctionCodeIndex> function_code_index;
|
||||
std::shared_ptr<const PatchFileIndex> pc_patch_file_index;
|
||||
@@ -138,7 +146,8 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table_v3;
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table_v4;
|
||||
std::shared_ptr<const MagEvolutionTable> mag_evolution_table;
|
||||
std::shared_ptr<const ItemNameIndex> item_name_index;
|
||||
std::shared_ptr<const TextIndex> text_index;
|
||||
std::array<std::shared_ptr<const ItemNameIndex>, NUM_VERSIONS> item_name_indexes;
|
||||
std::shared_ptr<const WordSelectTable> word_select_table;
|
||||
std::array<std::shared_ptr<const Map::RareEnemyRates>, 4> rare_enemy_rates_by_difficulty;
|
||||
std::shared_ptr<const Map::RareEnemyRates> rare_enemy_rates_challenge;
|
||||
@@ -160,21 +169,19 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::vector<QuestF960Result> quest_F960_success_results;
|
||||
QuestF960Result quest_F960_failure_results;
|
||||
std::vector<ItemData> secret_lottery_results;
|
||||
uint16_t bb_global_exp_multiplier;
|
||||
uint16_t bb_global_exp_multiplier = 1;
|
||||
|
||||
std::shared_ptr<Episode3::TournamentIndex> ep3_tournament_index;
|
||||
|
||||
uint16_t ep3_card_auction_points;
|
||||
uint16_t ep3_card_auction_min_size;
|
||||
uint16_t ep3_card_auction_max_size;
|
||||
uint16_t ep3_card_auction_points = 0;
|
||||
uint16_t ep3_card_auction_min_size = 0;
|
||||
uint16_t ep3_card_auction_max_size = 0;
|
||||
struct CardAuctionPoolEntry {
|
||||
uint64_t probability;
|
||||
uint16_t card_id;
|
||||
uint16_t min_price;
|
||||
std::string card_name;
|
||||
};
|
||||
std::vector<CardAuctionPoolEntry> ep3_card_auction_pool;
|
||||
std::vector<std::vector<std::string>> ep3_trap_card_names;
|
||||
std::array<std::vector<uint16_t>, 5> ep3_trap_card_ids;
|
||||
struct Ep3LobbyBannerEntry {
|
||||
uint32_t type = 1;
|
||||
@@ -211,26 +218,31 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::unordered_set<std::shared_ptr<Lobby>> lobbies_to_destroy;
|
||||
std::shared_ptr<struct event> destroy_lobbies_event;
|
||||
std::vector<std::shared_ptr<Lobby>> public_lobby_search_order;
|
||||
std::atomic<int32_t> next_lobby_id;
|
||||
uint8_t pre_lobby_event;
|
||||
int32_t ep3_menu_song;
|
||||
std::atomic<int32_t> next_lobby_id = 1;
|
||||
uint8_t pre_lobby_event = 0;
|
||||
int32_t ep3_menu_song = -1;
|
||||
|
||||
std::map<std::string, uint32_t> all_addresses;
|
||||
uint32_t local_address;
|
||||
uint32_t external_address;
|
||||
uint32_t local_address = 0;
|
||||
uint32_t external_address = 0;
|
||||
|
||||
bool proxy_allow_save_files;
|
||||
bool proxy_enable_login_options;
|
||||
bool proxy_allow_save_files = true;
|
||||
bool proxy_enable_login_options = false;
|
||||
|
||||
std::shared_ptr<ProxyServer> proxy_server;
|
||||
std::shared_ptr<Server> game_server;
|
||||
|
||||
ServerState();
|
||||
ServerState(std::shared_ptr<struct event_base> base, const std::string& config_filename, bool is_replay);
|
||||
ServerState(const ServerState&) = delete;
|
||||
ServerState(ServerState&&) = delete;
|
||||
ServerState& operator=(const ServerState&) = delete;
|
||||
ServerState& operator=(ServerState&&) = delete;
|
||||
void init();
|
||||
|
||||
void load_objects_and_downstream_dependents(const std::string& what);
|
||||
void load_objects_and_downstream_dependents(const std::vector<std::string>& what);
|
||||
void load_objects_and_upstream_dependents(const std::string& what);
|
||||
void load_objects_and_upstream_dependents(const std::vector<std::string>& what);
|
||||
|
||||
void add_client_to_available_lobby(std::shared_ptr<Client> c);
|
||||
void remove_client_from_lobby(std::shared_ptr<Client> c);
|
||||
@@ -257,15 +269,18 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
|
||||
uint32_t connect_address_for_client(std::shared_ptr<Client> c) const;
|
||||
|
||||
std::shared_ptr<const Menu> information_menu_for_version(Version version) const;
|
||||
std::shared_ptr<const Menu> proxy_destinations_menu_for_version(Version version) const;
|
||||
const std::vector<std::pair<std::string, uint16_t>>& proxy_destinations_for_version(Version version) const;
|
||||
std::shared_ptr<const Menu> information_menu(Version version) const;
|
||||
std::shared_ptr<const Menu> proxy_destinations_menu(Version version) const;
|
||||
const std::vector<std::pair<std::string, uint16_t>>& proxy_destinations(Version version) const;
|
||||
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table_for_version(Version version) const;
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table(Version version) const;
|
||||
std::shared_ptr<const ItemNameIndex> item_name_index(Version version) const;
|
||||
void set_item_name_index(Version version, std::shared_ptr<const ItemNameIndex>);
|
||||
std::string describe_item(Version version, const ItemData& item, bool include_color_codes) const;
|
||||
ItemData parse_item_description(Version version, const std::string& description) const;
|
||||
|
||||
std::shared_ptr<const std::vector<std::string>> information_contents_for_client(std::shared_ptr<const Client> c) const;
|
||||
std::shared_ptr<const QuestIndex> quest_index_for_version(Version version) const;
|
||||
std::shared_ptr<const QuestIndex> quest_index(Version version) const;
|
||||
|
||||
void set_port_configuration(const std::vector<PortConfiguration>& port_configs);
|
||||
|
||||
@@ -274,21 +289,27 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
const std::string& gsl_filename = "",
|
||||
const std::string& bb_directory_filename = "") const;
|
||||
|
||||
JSON load_config() const;
|
||||
std::pair<std::string, uint16_t> parse_port_spec(const JSON& json) const;
|
||||
std::vector<PortConfiguration> parse_port_configuration(const JSON& json) const;
|
||||
|
||||
void create_load_step_graph();
|
||||
void create_default_lobbies();
|
||||
void collect_network_addresses();
|
||||
void parse_config(const JSON& config_json, bool is_reload);
|
||||
void load_config();
|
||||
void load_bb_private_keys();
|
||||
void load_licenses();
|
||||
void load_teams();
|
||||
void load_patch_indexes();
|
||||
void load_battle_params();
|
||||
void load_level_table();
|
||||
void load_item_name_index();
|
||||
void load_item_tables();
|
||||
static std::shared_ptr<WordSelectTable> load_word_select_table_from_system();
|
||||
void load_text_index();
|
||||
static std::shared_ptr<ItemNameIndex> create_item_name_index_for_version(
|
||||
Version version, std::shared_ptr<const ItemParameterTable> pmt, std::shared_ptr<const TextIndex> text_index);
|
||||
void load_item_name_indexes();
|
||||
void load_drop_tables();
|
||||
void load_item_definitions();
|
||||
void load_word_select_table();
|
||||
void load_ep3_data();
|
||||
void resolve_ep3_card_names();
|
||||
void load_quest_index();
|
||||
void compile_functions();
|
||||
void load_dol_files();
|
||||
|
||||
@@ -493,12 +493,15 @@ uint8_t language_code_for_char(char language_char) {
|
||||
}
|
||||
}
|
||||
|
||||
size_t max_stack_size_for_item(uint8_t data0, uint8_t data1) {
|
||||
size_t max_stack_size_for_item(Version version, uint8_t data0, uint8_t data1) {
|
||||
if (data0 == 4) {
|
||||
return 999999;
|
||||
}
|
||||
if (data0 == 3) {
|
||||
if ((data1 < 9) && (data1 != 2)) {
|
||||
if (version == Version::DC_V1_11_2000_PROTOTYPE) {
|
||||
// All tool items are stackable up to x10 on this version
|
||||
return 10;
|
||||
} else if ((data1 < 9) && (data1 != 2)) {
|
||||
return 10;
|
||||
} else if (data1 == 0x10) {
|
||||
return 99;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "FileContentsCache.hh"
|
||||
#include "Version.hh"
|
||||
|
||||
enum class Episode {
|
||||
NONE = 0,
|
||||
@@ -32,7 +33,7 @@ enum class GameMode {
|
||||
const char* name_for_mode(GameMode mode);
|
||||
const char* abbreviation_for_mode(GameMode mode);
|
||||
|
||||
size_t max_stack_size_for_item(uint8_t data0, uint8_t data1);
|
||||
size_t max_stack_size_for_item(Version version, uint8_t data0, uint8_t data1);
|
||||
|
||||
extern const std::vector<std::string> tech_id_to_name;
|
||||
extern const std::unordered_map<std::string, uint8_t> name_to_tech_id;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "StepGraph.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
void StepGraph::add_step(const string& name, const vector<string>& depends_on_names, function<void()>&& execute) {
|
||||
auto new_step = make_shared<Step>();
|
||||
new_step->execute = std::move(execute);
|
||||
this->steps.emplace(name, new_step);
|
||||
|
||||
for (const auto& depends_on_name : depends_on_names) {
|
||||
auto upstream_step = this->steps.at(depends_on_name);
|
||||
upstream_step->downstream_dependencies.emplace_back(new_step);
|
||||
new_step->upstream_dependencies.emplace_back(upstream_step);
|
||||
}
|
||||
}
|
||||
|
||||
void StepGraph::run(const string& start_step_name, bool run_upstreams) {
|
||||
vector<string> start_step_names({start_step_name});
|
||||
this->run(start_step_names, run_upstreams);
|
||||
}
|
||||
|
||||
void StepGraph::run(const vector<string>& start_step_names, bool run_upstreams) {
|
||||
// Collect all steps to run
|
||||
deque<shared_ptr<Step>> steps_to_visit;
|
||||
try {
|
||||
for (const auto& start_step_name : start_step_names) {
|
||||
steps_to_visit.emplace_back(this->steps.at(start_step_name));
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
throw runtime_error("invalid step name");
|
||||
}
|
||||
unordered_set<shared_ptr<Step>> steps_to_run;
|
||||
while (!steps_to_visit.empty()) {
|
||||
auto step = std::move(steps_to_visit.front());
|
||||
steps_to_visit.pop_front();
|
||||
if (steps_to_run.emplace(step).second) {
|
||||
if (run_upstreams) {
|
||||
for (const auto& w_other_step : step->upstream_dependencies) {
|
||||
auto other_step = w_other_step.lock();
|
||||
if (!other_step) {
|
||||
throw runtime_error("upstream step is deleted");
|
||||
}
|
||||
steps_to_visit.emplace_back(other_step);
|
||||
}
|
||||
} else {
|
||||
for (const auto& other_step : step->downstream_dependencies) {
|
||||
steps_to_visit.emplace_back(other_step);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Topological sort: repeatedly take all steps that are not a downstream
|
||||
// dependency of any other step in the set
|
||||
vector<shared_ptr<Step>> steps_order;
|
||||
steps_order.reserve(steps_to_run.size());
|
||||
while (!steps_to_run.empty()) {
|
||||
unordered_set<shared_ptr<Step>> candidate_steps = steps_to_run;
|
||||
for (const auto& step : steps_to_run) {
|
||||
for (const auto& downstream_step : step->downstream_dependencies) {
|
||||
candidate_steps.erase(downstream_step);
|
||||
}
|
||||
}
|
||||
if (candidate_steps.empty()) {
|
||||
throw logic_error("dependency graph contains a cycle");
|
||||
}
|
||||
for (const auto& step : candidate_steps) {
|
||||
steps_to_run.erase(step);
|
||||
steps_order.emplace_back(step);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the steps in order
|
||||
uint64_t run_id = ++this->last_run_id;
|
||||
for (auto step : steps_order) {
|
||||
if (step->last_run_id < run_id) {
|
||||
step->last_run_id = run_id;
|
||||
if (step->execute) {
|
||||
step->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <phosg/Strings.hh>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
struct StepGraph {
|
||||
struct Step {
|
||||
std::vector<std::shared_ptr<Step>> downstream_dependencies;
|
||||
std::vector<std::weak_ptr<Step>> upstream_dependencies;
|
||||
std::function<void()> execute;
|
||||
uint64_t last_run_id = 0;
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, std::shared_ptr<Step>> steps;
|
||||
uint64_t last_run_id = 0;
|
||||
|
||||
StepGraph() = default;
|
||||
|
||||
void add_step(const std::string& name, const std::vector<std::string>& depends_on_names, std::function<void()>&& execute);
|
||||
void run(const std::string& start_step, bool run_upstreams);
|
||||
void run(const std::vector<std::string>& start_steps, bool run_upstreams);
|
||||
};
|
||||
@@ -1,302 +0,0 @@
|
||||
#include "TextArchive.hh"
|
||||
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Random.hh>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "Compression.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
#include "Text.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
TextArchive::TextArchive(const string& pr2_data, bool big_endian) {
|
||||
if (big_endian) {
|
||||
this->load_t<true>(pr2_data);
|
||||
} else {
|
||||
this->load_t<false>(pr2_data);
|
||||
}
|
||||
}
|
||||
|
||||
TextArchive::TextArchive(const JSON& json) {
|
||||
for (const auto& collection_json : json.at("collections").as_list()) {
|
||||
auto& collection = this->collections.emplace_back();
|
||||
for (const auto& string_json : collection_json->as_list()) {
|
||||
collection.emplace_back(string_json->as_string());
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& keyboard_json : json.at("keyboards").as_list()) {
|
||||
auto& keyboard = this->keyboards.emplace_back(make_unique<Keyboard>());
|
||||
for (size_t y = 0; y < keyboard->size(); y++) {
|
||||
auto& row = keyboard->at(y);
|
||||
const auto& row_json = keyboard_json->at(y);
|
||||
for (size_t x = 0; x < row.size(); x++) {
|
||||
row[x] = row_json.at(x).as_int();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->keyboard_selector_width = json.at("keyboard_selector_width").as_int();
|
||||
}
|
||||
|
||||
JSON TextArchive::json() const {
|
||||
auto collections_json = JSON::list();
|
||||
for (const auto& collection : this->collections) {
|
||||
auto collection_json = JSON::list();
|
||||
for (const auto& s : collection) {
|
||||
collection_json.emplace_back(s);
|
||||
}
|
||||
collections_json.emplace_back(std::move(collection_json));
|
||||
}
|
||||
auto keyboards_json = JSON::list();
|
||||
for (const auto& kb : this->keyboards) {
|
||||
JSON keyboard_json = JSON::list();
|
||||
for (size_t y = 0; y < kb->size(); y++) {
|
||||
const auto& row = kb->at(y);
|
||||
JSON row_json = JSON::list();
|
||||
for (size_t x = 0; x < row.size(); x++) {
|
||||
row_json.emplace_back(row[x]);
|
||||
}
|
||||
keyboard_json.emplace_back(std::move(row_json));
|
||||
}
|
||||
keyboards_json.emplace_back(std::move(keyboard_json));
|
||||
}
|
||||
return JSON::dict({
|
||||
{"collections", std::move(collections_json)},
|
||||
{"keyboards", std::move(keyboards_json)},
|
||||
{"keyboard_selector_width", this->keyboard_selector_width},
|
||||
});
|
||||
}
|
||||
|
||||
const string& TextArchive::get_string(size_t collection_index, size_t index) const {
|
||||
return this->collections.at(collection_index).at(index);
|
||||
}
|
||||
|
||||
void TextArchive::set_string(size_t collection_index, size_t index, const string& data) {
|
||||
if (collection_index >= this->collections.size()) {
|
||||
this->collections.resize(collection_index + 1);
|
||||
}
|
||||
auto& coll = this->collections[collection_index];
|
||||
if (index >= coll.size()) {
|
||||
coll.resize(index + 1);
|
||||
}
|
||||
coll[index] = data;
|
||||
}
|
||||
|
||||
void TextArchive::set_string(size_t collection_index, size_t index, string&& data) {
|
||||
if (collection_index >= this->collections.size()) {
|
||||
this->collections.resize(collection_index + 1);
|
||||
}
|
||||
auto& coll = this->collections[collection_index];
|
||||
if (index >= coll.size()) {
|
||||
coll.resize(index + 1);
|
||||
}
|
||||
coll[index] = std::move(data);
|
||||
}
|
||||
|
||||
void TextArchive::resize_collection(size_t collection_index, size_t size) {
|
||||
if (collection_index >= this->collections.size()) {
|
||||
this->collections.resize(collection_index + 1);
|
||||
}
|
||||
this->collections[collection_index].resize(size);
|
||||
}
|
||||
|
||||
void TextArchive::resize_collection(size_t num_collections) {
|
||||
this->collections.resize(num_collections);
|
||||
}
|
||||
|
||||
TextArchive::Keyboard TextArchive::get_keyboard(size_t kb_index) const {
|
||||
return *this->keyboards.at(kb_index);
|
||||
}
|
||||
|
||||
void TextArchive::set_keyboard(size_t kb_index, const Keyboard& kb) {
|
||||
if (kb_index >= this->keyboards.size()) {
|
||||
this->keyboards.resize(kb_index + 1);
|
||||
}
|
||||
this->keyboards[kb_index] = make_unique<Keyboard>(kb);
|
||||
}
|
||||
|
||||
void TextArchive::resize_keyboards(size_t num_keyboards) {
|
||||
this->keyboards.resize(num_keyboards);
|
||||
}
|
||||
|
||||
pair<string, string> TextArchive::serialize(bool big_endian) const {
|
||||
if (big_endian) {
|
||||
return this->serialize_t<true>();
|
||||
} else {
|
||||
return this->serialize_t<false>();
|
||||
}
|
||||
}
|
||||
|
||||
template <bool IsBigEndian>
|
||||
void TextArchive::load_t(const string& pr2_data) {
|
||||
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
|
||||
using U16T = std::conditional_t<IsBigEndian, be_uint16_t, le_uint16_t>;
|
||||
|
||||
// The structure is as follows:
|
||||
// Footer:
|
||||
// U32T keyboard_index_offset ->:
|
||||
// U8 num_keyboards
|
||||
// U8 keyboard_selector_width
|
||||
// U8 unused[2]
|
||||
// U32T keyboards_offset ->:
|
||||
// U32T keyboard_offset[num_keyboards] ->:
|
||||
// U16T key_defs[7][16]
|
||||
// U32T collections_offset ->:
|
||||
// U32T[...] strings_offset ->:
|
||||
// U32T[...] string_offset ->:
|
||||
// char string[...\0]
|
||||
// <EOF>
|
||||
|
||||
auto pr2_decrypted = decrypt_pr2_data<IsBigEndian>(pr2_data);
|
||||
auto decompressed = prs_decompress(pr2_decrypted.compressed_data);
|
||||
StringReader r(decompressed);
|
||||
|
||||
// Annoyingly, there doesn't appear to be any bounds-checking on the language
|
||||
// functions, so there are no counts of strings in each collection. We have to
|
||||
// figure out where each collection ends by collecting all the relevant
|
||||
// offsets in the file instead.
|
||||
set<uint32_t> used_offsets;
|
||||
used_offsets.emplace(r.size() - 8);
|
||||
|
||||
uint32_t keyboard_index_offset = r.pget<U32T>(r.size() - 8);
|
||||
used_offsets.emplace(keyboard_index_offset);
|
||||
size_t num_keyboards = r.pget_u8(keyboard_index_offset);
|
||||
this->keyboard_selector_width = r.pget_u8(keyboard_index_offset + 1);
|
||||
uint32_t keyboards_offset = r.pget<U32T>(keyboard_index_offset + 4);
|
||||
used_offsets.emplace(keyboards_offset);
|
||||
while (this->keyboards.size() < num_keyboards) {
|
||||
uint32_t keyboard_offset = r.pget<U32T>(keyboards_offset + 4 * this->keyboards.size());
|
||||
used_offsets.emplace(keyboard_offset);
|
||||
auto& kb = this->keyboards.emplace_back(make_unique<Keyboard>());
|
||||
auto key_r = r.sub(keyboard_offset, sizeof(Keyboard));
|
||||
for (size_t y = 0; y < kb->size(); y++) {
|
||||
auto& row = kb->at(y);
|
||||
for (size_t x = 0; x < row.size(); x++) {
|
||||
row[x] = key_r.get<U16T>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t collections_offset = r.pget<U32T>(r.size() - 4);
|
||||
for (uint32_t offset = collections_offset; !used_offsets.count(offset); offset += 4) {
|
||||
used_offsets.emplace(r.pget<U32T>(offset));
|
||||
}
|
||||
used_offsets.emplace(collections_offset);
|
||||
|
||||
for (uint32_t offset = collections_offset; (offset == collections_offset) || !used_offsets.count(offset); offset += 4) {
|
||||
auto& collection = this->collections.emplace_back();
|
||||
uint32_t first_string_offset_offset = r.pget<U32T>(offset);
|
||||
for (uint32_t string_offset_offset = first_string_offset_offset;
|
||||
(string_offset_offset == first_string_offset_offset) || !used_offsets.count(string_offset_offset);
|
||||
string_offset_offset += 4) {
|
||||
collection.emplace_back(r.pget_cstr(r.pget<U32T>(string_offset_offset)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <bool IsBigEndian>
|
||||
pair<string, string> TextArchive::serialize_t() const {
|
||||
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
|
||||
using U16T = std::conditional_t<IsBigEndian, be_uint16_t, le_uint16_t>;
|
||||
|
||||
StringWriter w;
|
||||
set<size_t> relocation_offsets;
|
||||
auto put_offset_u32 = [&](uint32_t v) {
|
||||
relocation_offsets.emplace(w.size());
|
||||
w.put<U32T>(v);
|
||||
};
|
||||
|
||||
uint32_t collections_offset;
|
||||
{
|
||||
unordered_map<string, uint32_t> string_to_offset;
|
||||
for (const auto& collection : this->collections) {
|
||||
for (const auto& s : collection) {
|
||||
if (string_to_offset.emplace(s, w.size()).second) {
|
||||
w.write(s);
|
||||
w.put_u8(0);
|
||||
while (w.size() & 3) {
|
||||
w.put_u8(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vector<uint32_t> collection_offsets;
|
||||
for (const auto& collection : this->collections) {
|
||||
collection_offsets.emplace_back(w.size());
|
||||
for (const auto& s : collection) {
|
||||
put_offset_u32(string_to_offset.at(s));
|
||||
}
|
||||
}
|
||||
|
||||
collections_offset = w.size();
|
||||
for (uint32_t collection_offset : collection_offsets) {
|
||||
put_offset_u32(collection_offset);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t keyboard_index_offset;
|
||||
{
|
||||
vector<uint32_t> keyboard_offsets;
|
||||
for (const auto& keyboard : this->keyboards) {
|
||||
keyboard_offsets.emplace_back(w.size());
|
||||
for (size_t y = 0; y < keyboard->size(); y++) {
|
||||
const auto& row = keyboard->at(y);
|
||||
for (size_t x = 0; x < row.size(); x++) {
|
||||
w.put<U16T>(row[x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t keyboards_offset = w.size();
|
||||
for (uint32_t keyboard_offset : keyboard_offsets) {
|
||||
put_offset_u32(keyboard_offset);
|
||||
}
|
||||
|
||||
keyboard_index_offset = w.size();
|
||||
w.put_u8(keyboard_offsets.size());
|
||||
w.put_u8(this->keyboard_selector_width);
|
||||
w.put_u16(0);
|
||||
put_offset_u32(keyboards_offset);
|
||||
}
|
||||
|
||||
put_offset_u32(keyboard_index_offset);
|
||||
put_offset_u32(collections_offset);
|
||||
|
||||
StringWriter reloc_w;
|
||||
reloc_w.put_u32(0);
|
||||
reloc_w.put<U32T>(relocation_offsets.size());
|
||||
reloc_w.put_u64(0);
|
||||
reloc_w.put<U32T>(w.size() - 8);
|
||||
reloc_w.put_u32(0);
|
||||
reloc_w.put_u64(0);
|
||||
{
|
||||
size_t offset = 0;
|
||||
for (size_t reloc_offset : relocation_offsets) {
|
||||
if (reloc_offset & 3) {
|
||||
throw logic_error("misaligned relocation");
|
||||
}
|
||||
size_t num_words = (reloc_offset - offset) >> 2;
|
||||
if (num_words > 0xFFFF) {
|
||||
throw runtime_error("relocation offset too far away");
|
||||
}
|
||||
reloc_w.put<U16T>(num_words);
|
||||
offset = reloc_offset;
|
||||
}
|
||||
}
|
||||
|
||||
const string& pr2_data = w.str();
|
||||
const string& pr3_data = reloc_w.str();
|
||||
print_data(stderr, pr2_data);
|
||||
string pr2_compressed = prs_compress_optimal(pr2_data.data(), pr2_data.size());
|
||||
string pr3_compressed = prs_compress_optimal(pr3_data.data(), pr3_data.size());
|
||||
print_data(stderr, pr2_compressed);
|
||||
string pr2_ret = encrypt_pr2_data<IsBigEndian>(pr2_compressed, pr2_data.size(), random_object<uint32_t>());
|
||||
string pr3_ret = encrypt_pr2_data<IsBigEndian>(pr3_compressed, pr3_data.size(), random_object<uint32_t>());
|
||||
print_data(stderr, pr2_ret);
|
||||
return make_pair(std::move(pr2_ret), std::move(pr3_ret));
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <phosg/JSON.hh>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "Text.hh"
|
||||
|
||||
// This class implements loading and saving of text archives, commonly found in
|
||||
// PSO games with filenames like TextEnglish.pr2 and TextEnglish.pr3. The game
|
||||
// requires both files, but newserv needs only the pr2 file to load a text
|
||||
// archive. When saving (serializing), both pr2 and pr3 files are generated.
|
||||
class TextArchive {
|
||||
public:
|
||||
using Keyboard = parray<parray<uint16_t, 0x10>, 7>;
|
||||
|
||||
explicit TextArchive(const JSON& json);
|
||||
TextArchive(const std::string& pr2_data, bool big_endian);
|
||||
~TextArchive() = default;
|
||||
|
||||
JSON json() const;
|
||||
|
||||
const std::string& get_string(size_t collection_index, size_t index) const;
|
||||
void set_string(size_t collection_index, size_t index, const std::string& data);
|
||||
void set_string(size_t collection_index, size_t index, std::string&& data);
|
||||
void resize_collection(size_t collection_index, size_t size);
|
||||
void resize_collection(size_t num_collections);
|
||||
|
||||
Keyboard get_keyboard(size_t kb_index) const;
|
||||
void set_keyboard(size_t kb_index, const Keyboard& kb);
|
||||
void resize_keyboards(size_t num_keyboards);
|
||||
|
||||
uint8_t get_keyboard_selector_width() const;
|
||||
void set_keyboard_selector_width(uint8_t width);
|
||||
|
||||
// Returns (pr2_data, pr3_data)
|
||||
std::pair<std::string, std::string> serialize(bool big_endian) const;
|
||||
|
||||
private:
|
||||
template <bool IsBigEndian>
|
||||
void load_t(const std::string& pr2_data);
|
||||
template <bool IsBigEndian>
|
||||
std::pair<std::string, std::string> serialize_t() const;
|
||||
|
||||
std::vector<std::vector<std::string>> collections;
|
||||
std::vector<std::unique_ptr<Keyboard>> keyboards;
|
||||
uint8_t keyboard_selector_width;
|
||||
};
|
||||
@@ -0,0 +1,557 @@
|
||||
#include "TextIndex.hh"
|
||||
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Random.hh>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "Compression.hh"
|
||||
#include "Loggers.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
#include "StaticGameData.hh"
|
||||
#include "Text.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
TextSet::TextSet(const JSON& json) {
|
||||
for (const auto& coll_json : json.as_list()) {
|
||||
auto& collection = this->collections.emplace_back();
|
||||
for (const auto& s_json : coll_json->as_list()) {
|
||||
collection.emplace_back(s_json->as_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextSet::TextSet(JSON&& json) {
|
||||
for (const auto& coll_json : json.as_list()) {
|
||||
auto& collection = this->collections.emplace_back();
|
||||
for (const auto& s_json : coll_json->as_list()) {
|
||||
collection.emplace_back(std::move(s_json->as_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JSON TextSet::json() const {
|
||||
JSON j = JSON::list();
|
||||
for (const auto& collection : this->collections) {
|
||||
JSON& coll_j = j.emplace_back(JSON::list());
|
||||
for (const auto& s : collection) {
|
||||
coll_j.emplace_back(s);
|
||||
}
|
||||
}
|
||||
return j;
|
||||
}
|
||||
|
||||
size_t TextSet::count(size_t collection_index) const {
|
||||
return this->collections.at(collection_index).size();
|
||||
}
|
||||
|
||||
size_t TextSet::count() const {
|
||||
return this->collections.size();
|
||||
}
|
||||
|
||||
const std::string& TextSet::get(size_t collection_index, size_t string_index) const {
|
||||
return this->get(collection_index).at(string_index);
|
||||
}
|
||||
|
||||
const vector<string>& TextSet::get(size_t collection_index) const {
|
||||
return this->collections.at(collection_index);
|
||||
}
|
||||
|
||||
void TextSet::set(size_t collection_index, size_t string_index, const std::string& data) {
|
||||
this->ensure_slot_exists(collection_index, string_index);
|
||||
this->collections[collection_index][string_index] = data;
|
||||
}
|
||||
|
||||
void TextSet::set(size_t collection_index, size_t string_index, std::string&& data) {
|
||||
this->ensure_slot_exists(collection_index, string_index);
|
||||
this->collections[collection_index][string_index] = std::move(data);
|
||||
}
|
||||
|
||||
void TextSet::set(size_t collection_index, const std::vector<std::string>& coll) {
|
||||
this->ensure_collection_exists(collection_index);
|
||||
this->collections[collection_index] = coll;
|
||||
}
|
||||
|
||||
void TextSet::set(size_t collection_index, std::vector<std::string>&& coll) {
|
||||
this->ensure_collection_exists(collection_index);
|
||||
this->collections[collection_index] = std::move(coll);
|
||||
}
|
||||
|
||||
void TextSet::truncate_collection(size_t collection_index, size_t new_entry_count) {
|
||||
if (collection_index >= this->collections.size()) {
|
||||
this->collections.resize(collection_index + 1);
|
||||
}
|
||||
this->collections[collection_index].resize(new_entry_count);
|
||||
}
|
||||
|
||||
void TextSet::truncate(size_t new_collection_count) {
|
||||
this->collections.resize(new_collection_count);
|
||||
}
|
||||
|
||||
void TextSet::ensure_slot_exists(size_t collection_index, size_t string_index) {
|
||||
this->ensure_collection_exists(collection_index);
|
||||
auto& coll = this->collections[collection_index];
|
||||
if (string_index >= coll.size()) {
|
||||
coll.resize(string_index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void TextSet::ensure_collection_exists(size_t collection_index) {
|
||||
if (collection_index >= this->collections.size()) {
|
||||
this->collections.resize(collection_index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
UnicodeTextSet::UnicodeTextSet(const string& prs_data) {
|
||||
string data = prs_decompress(prs_data);
|
||||
StringReader r(data);
|
||||
|
||||
uint32_t num_collections = r.get_u32l();
|
||||
deque<uint32_t> collection_sizes;
|
||||
while (collection_sizes.size() < num_collections) {
|
||||
collection_sizes.emplace_back(r.get_u32l());
|
||||
}
|
||||
|
||||
this->collections.reserve(collection_sizes.size());
|
||||
while (!collection_sizes.empty()) {
|
||||
uint32_t num_strings = collection_sizes.front();
|
||||
collection_sizes.pop_front();
|
||||
|
||||
auto& strings = this->collections.emplace_back();
|
||||
strings.reserve(num_strings);
|
||||
while (strings.size() < num_strings) {
|
||||
StringReader sub_r = r.sub(r.get_u32l());
|
||||
StringWriter w;
|
||||
for (uint16_t ch = sub_r.get_u16l(); ch != 0; ch = sub_r.get_u16l()) {
|
||||
w.put_u16l(ch);
|
||||
}
|
||||
strings.emplace_back(tt_utf16_to_utf8(w.str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string UnicodeTextSet::serialize() const {
|
||||
StringWriter header_w;
|
||||
StringWriter data_w;
|
||||
|
||||
size_t total_num_strings = 0;
|
||||
header_w.put_u32l(this->collections.size());
|
||||
for (const auto& collection : this->collections) {
|
||||
header_w.put_u32l(collection.size());
|
||||
total_num_strings += collection.size();
|
||||
}
|
||||
|
||||
unordered_map<string, uint32_t> encoded;
|
||||
|
||||
size_t data_base_offset = (total_num_strings * 4) + header_w.size();
|
||||
for (const auto& collection : this->collections) {
|
||||
for (const auto& s : collection) {
|
||||
auto encoded_it = encoded.find(s);
|
||||
if (encoded_it == encoded.end()) {
|
||||
uint32_t offset = data_base_offset + data_w.size();
|
||||
encoded_it = encoded.emplace(s, offset).first;
|
||||
string s_utf16 = tt_utf8_to_utf16(s);
|
||||
data_w.write(s_utf16.data(), s_utf16.size());
|
||||
data_w.put_u16(0);
|
||||
while (data_w.size() & 3) {
|
||||
data_w.put_u8(0);
|
||||
}
|
||||
}
|
||||
header_w.put_u32l(encoded_it->second);
|
||||
}
|
||||
}
|
||||
|
||||
header_w.write(data_w.str());
|
||||
return prs_compress_optimal(header_w.str());
|
||||
}
|
||||
|
||||
BinaryTextSet::BinaryTextSet(const std::string& pr2_data, size_t collection_count, bool has_rel_footer) {
|
||||
auto pr2_decrypted = decrypt_pr2_data<false>(pr2_data);
|
||||
auto decompressed = prs_decompress(pr2_decrypted.compressed_data);
|
||||
StringReader r(decompressed);
|
||||
|
||||
// Annoyingly, there doesn't appear to be any bounds-checking on the language
|
||||
// functions, so there are no counts of strings in each collection. We have to
|
||||
// figure out where each collection ends by collecting all the relevant
|
||||
// offsets in the file instead.
|
||||
::set<uint32_t> used_offsets;
|
||||
size_t root_offset = has_rel_footer
|
||||
? r.pget_u32l(r.size() - 0x10)
|
||||
: (r.size() - collection_count * sizeof(le_uint32_t));
|
||||
|
||||
StringReader collection_offsets_r = r.sub(root_offset, collection_count * sizeof(le_uint32_t));
|
||||
while (!collection_offsets_r.eof()) {
|
||||
used_offsets.emplace(collection_offsets_r.get_u32l());
|
||||
}
|
||||
used_offsets.emplace(root_offset);
|
||||
|
||||
collection_offsets_r.go(0);
|
||||
while (!collection_offsets_r.eof()) {
|
||||
auto& collection = this->collections.emplace_back();
|
||||
uint32_t first_string_offset_offset = collection_offsets_r.get_u32l();
|
||||
// TODO: Apparently the early formats do actually include keyboards, but
|
||||
// they're just in the middle of the collections list. Sigh...
|
||||
try {
|
||||
for (uint32_t string_offset_offset = first_string_offset_offset;
|
||||
(string_offset_offset == first_string_offset_offset) || !used_offsets.count(string_offset_offset);
|
||||
string_offset_offset += 4) {
|
||||
collection.emplace_back(r.pget_cstr(r.pget_u32l(string_offset_offset)));
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BinaryTextAndKeyboardsSet::BinaryTextAndKeyboardsSet(const string& pr2_data, bool big_endian) {
|
||||
if (big_endian) {
|
||||
this->parse_t<true>(pr2_data);
|
||||
} else {
|
||||
this->parse_t<false>(pr2_data);
|
||||
}
|
||||
}
|
||||
|
||||
BinaryTextAndKeyboardsSet::BinaryTextAndKeyboardsSet(const JSON& json) {
|
||||
for (const auto& collection_json : json.at("collections").as_list()) {
|
||||
auto& collection = this->collections.emplace_back();
|
||||
for (const auto& string_json : collection_json->as_list()) {
|
||||
collection.emplace_back(string_json->as_string());
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& keyboard_json : json.at("keyboards").as_list()) {
|
||||
auto& keyboard = this->keyboards.emplace_back(make_unique<Keyboard>());
|
||||
for (size_t y = 0; y < keyboard->size(); y++) {
|
||||
auto& row = keyboard->at(y);
|
||||
const auto& row_json = keyboard_json->at(y);
|
||||
for (size_t x = 0; x < row.size(); x++) {
|
||||
row[x] = row_json.at(x).as_int();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->keyboard_selector_width = json.at("keyboard_selector_width").as_int();
|
||||
}
|
||||
|
||||
JSON BinaryTextAndKeyboardsSet::json() const {
|
||||
auto collections_json = this->TextSet::json();
|
||||
auto keyboards_json = JSON::list();
|
||||
for (const auto& kb : this->keyboards) {
|
||||
JSON keyboard_json = JSON::list();
|
||||
for (size_t y = 0; y < kb->size(); y++) {
|
||||
const auto& row = kb->at(y);
|
||||
JSON row_json = JSON::list();
|
||||
for (size_t x = 0; x < row.size(); x++) {
|
||||
row_json.emplace_back(row[x]);
|
||||
}
|
||||
keyboard_json.emplace_back(std::move(row_json));
|
||||
}
|
||||
keyboards_json.emplace_back(std::move(keyboard_json));
|
||||
}
|
||||
return JSON::dict({
|
||||
{"collections", std::move(collections_json)},
|
||||
{"keyboards", std::move(keyboards_json)},
|
||||
{"keyboard_selector_width", this->keyboard_selector_width},
|
||||
});
|
||||
}
|
||||
|
||||
const BinaryTextAndKeyboardsSet::Keyboard& BinaryTextAndKeyboardsSet::get_keyboard(size_t kb_index) const {
|
||||
return *this->keyboards.at(kb_index);
|
||||
}
|
||||
|
||||
void BinaryTextAndKeyboardsSet::set_keyboard(size_t kb_index, const Keyboard& kb) {
|
||||
if (kb_index >= this->keyboards.size()) {
|
||||
this->keyboards.resize(kb_index + 1);
|
||||
}
|
||||
this->keyboards[kb_index] = make_unique<Keyboard>(kb);
|
||||
}
|
||||
|
||||
void BinaryTextAndKeyboardsSet::resize_keyboards(size_t num_keyboards) {
|
||||
this->keyboards.resize(num_keyboards);
|
||||
}
|
||||
|
||||
pair<string, string> BinaryTextAndKeyboardsSet::serialize(bool big_endian) const {
|
||||
if (big_endian) {
|
||||
return this->serialize_t<true>();
|
||||
} else {
|
||||
return this->serialize_t<false>();
|
||||
}
|
||||
}
|
||||
|
||||
template <bool IsBigEndian>
|
||||
void BinaryTextAndKeyboardsSet::parse_t(const string& pr2_data) {
|
||||
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
|
||||
using U16T = std::conditional_t<IsBigEndian, be_uint16_t, le_uint16_t>;
|
||||
|
||||
// The structure is as follows:
|
||||
// Footer:
|
||||
// U32T keyboard_index_offset ->:
|
||||
// U8 num_keyboards
|
||||
// U8 keyboard_selector_width
|
||||
// U8 unused[2]
|
||||
// U32T keyboards_offset ->:
|
||||
// U32T keyboard_offset[num_keyboards] ->:
|
||||
// U16T key_defs[7][16]
|
||||
// U32T collections_offset ->:
|
||||
// U32T[...] strings_offset ->:
|
||||
// U32T[...] string_offset ->:
|
||||
// char string[...\0]
|
||||
// <EOF>
|
||||
|
||||
auto pr2_decrypted = decrypt_pr2_data<IsBigEndian>(pr2_data);
|
||||
auto decompressed = prs_decompress(pr2_decrypted.compressed_data);
|
||||
StringReader r(decompressed);
|
||||
|
||||
// Annoyingly, there doesn't appear to be any bounds-checking on the language
|
||||
// functions, so there are no counts of strings in each collection. We have to
|
||||
// figure out where each collection ends by collecting all the relevant
|
||||
// offsets in the file instead.
|
||||
::set<uint32_t> used_offsets;
|
||||
used_offsets.emplace(r.size() - 8);
|
||||
|
||||
uint32_t keyboard_index_offset = r.pget<U32T>(r.size() - 8);
|
||||
used_offsets.emplace(keyboard_index_offset);
|
||||
size_t num_keyboards = r.pget_u8(keyboard_index_offset);
|
||||
this->keyboard_selector_width = r.pget_u8(keyboard_index_offset + 1);
|
||||
uint32_t keyboards_offset = r.pget<U32T>(keyboard_index_offset + 4);
|
||||
used_offsets.emplace(keyboards_offset);
|
||||
while (this->keyboards.size() < num_keyboards) {
|
||||
uint32_t keyboard_offset = r.pget<U32T>(keyboards_offset + 4 * this->keyboards.size());
|
||||
used_offsets.emplace(keyboard_offset);
|
||||
auto& kb = this->keyboards.emplace_back(make_unique<Keyboard>());
|
||||
auto key_r = r.sub(keyboard_offset, sizeof(Keyboard));
|
||||
for (size_t y = 0; y < kb->size(); y++) {
|
||||
auto& row = kb->at(y);
|
||||
for (size_t x = 0; x < row.size(); x++) {
|
||||
row[x] = key_r.get<U16T>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t collections_offset = r.pget<U32T>(r.size() - 4);
|
||||
for (uint32_t offset = collections_offset; !used_offsets.count(offset); offset += 4) {
|
||||
used_offsets.emplace(r.pget<U32T>(offset));
|
||||
}
|
||||
used_offsets.emplace(collections_offset);
|
||||
|
||||
for (uint32_t offset = collections_offset; (offset == collections_offset) || !used_offsets.count(offset); offset += 4) {
|
||||
auto& collection = this->collections.emplace_back();
|
||||
uint32_t first_string_offset_offset = r.pget<U32T>(offset);
|
||||
for (uint32_t string_offset_offset = first_string_offset_offset;
|
||||
(string_offset_offset == first_string_offset_offset) || !used_offsets.count(string_offset_offset);
|
||||
string_offset_offset += 4) {
|
||||
collection.emplace_back(r.pget_cstr(r.pget<U32T>(string_offset_offset)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <bool IsBigEndian>
|
||||
pair<string, string> BinaryTextAndKeyboardsSet::serialize_t() const {
|
||||
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
|
||||
using U16T = std::conditional_t<IsBigEndian, be_uint16_t, le_uint16_t>;
|
||||
|
||||
StringWriter w;
|
||||
::set<size_t> relocation_offsets;
|
||||
auto put_offset_u32 = [&](uint32_t v) {
|
||||
relocation_offsets.emplace(w.size());
|
||||
w.put<U32T>(v);
|
||||
};
|
||||
|
||||
uint32_t collections_offset;
|
||||
{
|
||||
unordered_map<string, uint32_t> string_to_offset;
|
||||
for (const auto& collection : this->collections) {
|
||||
for (const auto& s : collection) {
|
||||
if (string_to_offset.emplace(s, w.size()).second) {
|
||||
w.write(s);
|
||||
w.put_u8(0);
|
||||
while (w.size() & 3) {
|
||||
w.put_u8(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vector<uint32_t> collection_offsets;
|
||||
for (const auto& collection : this->collections) {
|
||||
collection_offsets.emplace_back(w.size());
|
||||
for (const auto& s : collection) {
|
||||
put_offset_u32(string_to_offset.at(s));
|
||||
}
|
||||
}
|
||||
|
||||
collections_offset = w.size();
|
||||
for (uint32_t collection_offset : collection_offsets) {
|
||||
put_offset_u32(collection_offset);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t keyboard_index_offset;
|
||||
{
|
||||
vector<uint32_t> keyboard_offsets;
|
||||
for (const auto& keyboard : this->keyboards) {
|
||||
keyboard_offsets.emplace_back(w.size());
|
||||
for (size_t y = 0; y < keyboard->size(); y++) {
|
||||
const auto& row = keyboard->at(y);
|
||||
for (size_t x = 0; x < row.size(); x++) {
|
||||
w.put<U16T>(row[x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t keyboards_offset = w.size();
|
||||
for (uint32_t keyboard_offset : keyboard_offsets) {
|
||||
put_offset_u32(keyboard_offset);
|
||||
}
|
||||
|
||||
keyboard_index_offset = w.size();
|
||||
w.put_u8(keyboard_offsets.size());
|
||||
w.put_u8(this->keyboard_selector_width);
|
||||
w.put_u16(0);
|
||||
put_offset_u32(keyboards_offset);
|
||||
}
|
||||
|
||||
put_offset_u32(keyboard_index_offset);
|
||||
put_offset_u32(collections_offset);
|
||||
|
||||
StringWriter reloc_w;
|
||||
reloc_w.put_u32(0);
|
||||
reloc_w.put<U32T>(relocation_offsets.size());
|
||||
reloc_w.put_u64(0);
|
||||
reloc_w.put<U32T>(w.size() - 8);
|
||||
reloc_w.put_u32(0);
|
||||
reloc_w.put_u64(0);
|
||||
{
|
||||
size_t offset = 0;
|
||||
for (size_t reloc_offset : relocation_offsets) {
|
||||
if (reloc_offset & 3) {
|
||||
throw logic_error("misaligned relocation");
|
||||
}
|
||||
size_t num_words = (reloc_offset - offset) >> 2;
|
||||
if (num_words > 0xFFFF) {
|
||||
throw runtime_error("relocation offset too far away");
|
||||
}
|
||||
reloc_w.put<U16T>(num_words);
|
||||
offset = reloc_offset;
|
||||
}
|
||||
}
|
||||
|
||||
const string& pr2_data = w.str();
|
||||
const string& pr3_data = reloc_w.str();
|
||||
print_data(stderr, pr2_data);
|
||||
string pr2_compressed = prs_compress_optimal(pr2_data.data(), pr2_data.size());
|
||||
string pr3_compressed = prs_compress_optimal(pr3_data.data(), pr3_data.size());
|
||||
print_data(stderr, pr2_compressed);
|
||||
string pr2_ret = encrypt_pr2_data<IsBigEndian>(pr2_compressed, pr2_data.size(), random_object<uint32_t>());
|
||||
string pr3_ret = encrypt_pr2_data<IsBigEndian>(pr3_compressed, pr3_data.size(), random_object<uint32_t>());
|
||||
print_data(stderr, pr2_ret);
|
||||
return make_pair(std::move(pr2_ret), std::move(pr3_ret));
|
||||
}
|
||||
|
||||
TextIndex::TextIndex(
|
||||
const string& directory,
|
||||
function<shared_ptr<const string>(Version, const string&)> get_patch_file)
|
||||
: log("[TextIndex] ", static_game_data_log.min_level) {
|
||||
if (!directory.empty()) {
|
||||
auto add_version = [&](Version version, const string& subdirectory, function<shared_ptr<TextSet>(const string&)> make_set) -> void {
|
||||
static const map<string, uint8_t> bintext_filenames({
|
||||
{"TextJapanese.pr2", 0x00},
|
||||
{"TextEnglish.pr2", 0x01},
|
||||
{"TextGerman.pr2", 0x02},
|
||||
{"TextFrench.pr2", 0x03},
|
||||
{"TextSpanish.pr2", 0x04},
|
||||
});
|
||||
static const map<string, uint8_t> unitext_filenames({
|
||||
{"unitxt_j.prs", 0x00}, // PC/BB Japanese
|
||||
{"unitxt_e.prs", 0x01}, // PC/BB English
|
||||
{"unitxt_g.prs", 0x02}, // PC/BB German
|
||||
{"unitxt_f.prs", 0x03}, // PC/BB French
|
||||
{"unitxt_s.prs", 0x04}, // PC/BB Spanish
|
||||
{"unitxt_b.prs", 0x05}, // PC Simplified Chinese
|
||||
{"unitxt_cs.prs", 0x05}, // BB Simplified Chinese
|
||||
{"unitxt_t.prs", 0x06}, // PC Traditional Chinese
|
||||
{"unitxt_ct.prs", 0x06}, // BB Traditional Chinese
|
||||
{"unitxt_k.prs", 0x07}, // PC Korean
|
||||
{"unitxt_h.prs", 0x07}, // BB Korean
|
||||
});
|
||||
if (!uses_utf16(version)) {
|
||||
for (const auto& it : bintext_filenames) {
|
||||
string file_path = directory + "/" + subdirectory + "/" + it.first;
|
||||
string json_path = file_path + ".json";
|
||||
if (isfile(json_path)) {
|
||||
this->log.info("Loading %s %c JSON text set from %s", name_for_enum(version), char_for_language_code(it.second), json_path.c_str());
|
||||
this->add_set(version, it.second, make_shared<BinaryTextSet>(JSON::parse(load_file(json_path))));
|
||||
} else if (isfile(file_path)) {
|
||||
this->log.info("Loading %s %c binary text set from %s", name_for_enum(version), char_for_language_code(it.second), file_path.c_str());
|
||||
this->add_set(version, it.second, make_set(load_file(file_path)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const auto& it : unitext_filenames) {
|
||||
string file_path = directory + "/" + subdirectory + "/" + it.first;
|
||||
string json_path = file_path + ".json";
|
||||
if (isfile(json_path)) {
|
||||
this->log.info("Loading %s %c JSON text set from %s", name_for_enum(version), char_for_language_code(it.second), json_path.c_str());
|
||||
this->add_set(version, it.second, make_shared<UnicodeTextSet>(JSON::parse(load_file(json_path))));
|
||||
} else {
|
||||
auto patch_file = get_patch_file ? get_patch_file(version, it.first) : nullptr;
|
||||
if (patch_file) {
|
||||
this->log.info("Loading %s %c Unicode text set from %s in patch tree", name_for_enum(version), char_for_language_code(it.second), it.first.c_str());
|
||||
this->add_set(version, it.second, make_set(*patch_file));
|
||||
} else {
|
||||
if (isfile(file_path)) {
|
||||
this->log.info("Loading %s %c Unicode text set from %s", name_for_enum(version), char_for_language_code(it.second), file_path.c_str());
|
||||
this->add_set(version, it.second, make_set(load_file(file_path)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto make_binary_dc112000 = +[](const string& data) { return make_shared<BinaryTextSet>(data, 21, true); };
|
||||
auto make_binary_dcnte_dcv1 = +[](const string& data) { return make_shared<BinaryTextSet>(data, 26, true); };
|
||||
auto make_binary_dcv2 = +[](const string& data) { return make_shared<BinaryTextSet>(data, 37, false); };
|
||||
auto make_binary_gc = +[](const string& data) { return make_shared<BinaryTextAndKeyboardsSet>(data, true); };
|
||||
auto make_binary_xb = +[](const string& data) { return make_shared<BinaryTextAndKeyboardsSet>(data, false); };
|
||||
auto make_unitxt = +[](const string& data) { return make_shared<UnicodeTextSet>(data); };
|
||||
|
||||
add_version(Version::DC_NTE, "dc-nte", make_binary_dcnte_dcv1);
|
||||
add_version(Version::DC_V1_11_2000_PROTOTYPE, "dc-11-2000", make_binary_dc112000);
|
||||
add_version(Version::DC_V1, "dc-v1", make_binary_dcnte_dcv1);
|
||||
add_version(Version::DC_V2, "dc-v2", make_binary_dcv2);
|
||||
add_version(Version::PC_NTE, "pc-nte", make_unitxt);
|
||||
add_version(Version::PC_V2, "pc-v2", make_unitxt);
|
||||
add_version(Version::GC_NTE, "gc-nte", make_binary_gc);
|
||||
add_version(Version::GC_V3, "gc-v3", make_binary_gc);
|
||||
add_version(Version::GC_EP3_NTE, "gc-ep3-nte", make_binary_gc);
|
||||
add_version(Version::GC_EP3, "gc-ep3", make_binary_gc);
|
||||
add_version(Version::XB_V3, "xb-v3", make_binary_xb);
|
||||
add_version(Version::BB_V4, "bb-v4", make_unitxt);
|
||||
}
|
||||
}
|
||||
|
||||
void TextIndex::add_set(Version version, uint8_t language, std::shared_ptr<const TextSet> ts) {
|
||||
this->sets[this->key_for_set(version, language)] = ts;
|
||||
}
|
||||
|
||||
void TextIndex::delete_set(Version version, uint8_t language) {
|
||||
this->sets.erase(this->key_for_set(version, language));
|
||||
}
|
||||
|
||||
const std::string& TextIndex::get(Version version, uint8_t language, size_t collection_index, size_t string_index) const {
|
||||
return this->get(version, language)->get(collection_index, string_index);
|
||||
}
|
||||
|
||||
const std::vector<std::string>& TextIndex::get(Version version, uint8_t language, size_t collection_index) const {
|
||||
return this->get(version, language)->get(collection_index);
|
||||
}
|
||||
|
||||
std::shared_ptr<const TextSet> TextIndex::get(Version version, uint8_t language) const {
|
||||
return this->sets.at(this->key_for_set(version, language));
|
||||
}
|
||||
|
||||
uint32_t TextIndex::key_for_set(Version version, uint8_t language) {
|
||||
return (static_cast<uint32_t>(version) << 8) | language;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <phosg/JSON.hh>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "Text.hh"
|
||||
#include "Version.hh"
|
||||
|
||||
class TextSet {
|
||||
public:
|
||||
virtual ~TextSet() = default;
|
||||
virtual JSON json() const;
|
||||
|
||||
size_t count(size_t collection_index) const;
|
||||
size_t count() const;
|
||||
|
||||
const std::string& get(size_t collection, size_t index) const;
|
||||
const std::vector<std::string>& get(size_t collection) const;
|
||||
|
||||
void set(size_t collection_index, size_t string_index, const std::string& data);
|
||||
void set(size_t collection_index, size_t string_index, std::string&& data);
|
||||
void set(size_t collection_index, const std::vector<std::string>& coll);
|
||||
void set(size_t collection_index, std::vector<std::string>&& coll);
|
||||
|
||||
void truncate_collection(size_t collection, size_t new_entry_count);
|
||||
void truncate(size_t new_collection_count);
|
||||
|
||||
protected:
|
||||
std::vector<std::vector<std::string>> collections;
|
||||
|
||||
TextSet() = default;
|
||||
TextSet(const JSON& json);
|
||||
TextSet(JSON&& json);
|
||||
|
||||
void ensure_slot_exists(size_t collection_index, size_t string_index);
|
||||
void ensure_collection_exists(size_t collection_index);
|
||||
};
|
||||
|
||||
class UnicodeTextSet : public TextSet {
|
||||
public:
|
||||
explicit UnicodeTextSet(const JSON& json) : TextSet(json) {}
|
||||
explicit UnicodeTextSet(JSON&& json) : TextSet(json) {}
|
||||
explicit UnicodeTextSet(const std::string& unitxt_prs_data);
|
||||
virtual ~UnicodeTextSet() = default;
|
||||
std::string serialize() const;
|
||||
};
|
||||
|
||||
class BinaryTextSet : public TextSet {
|
||||
public:
|
||||
explicit BinaryTextSet(const JSON& json) : TextSet(json) {}
|
||||
explicit BinaryTextSet(JSON&& json) : TextSet(json) {}
|
||||
BinaryTextSet(const std::string& pr2_data, size_t collection_count, bool has_rel_footer);
|
||||
~BinaryTextSet() = default;
|
||||
// TODO: Implement serialize functions
|
||||
};
|
||||
|
||||
class BinaryTextAndKeyboardsSet : public TextSet {
|
||||
public:
|
||||
using Keyboard = parray<parray<uint16_t, 0x10>, 7>;
|
||||
|
||||
explicit BinaryTextAndKeyboardsSet(const JSON& json);
|
||||
explicit BinaryTextAndKeyboardsSet(JSON&& json);
|
||||
BinaryTextAndKeyboardsSet(const std::string& pr2_data, bool big_endian);
|
||||
~BinaryTextAndKeyboardsSet() = default;
|
||||
|
||||
virtual JSON json() const;
|
||||
|
||||
const Keyboard& get_keyboard(size_t kb_index) const;
|
||||
void set_keyboard(size_t kb_index, const Keyboard& kb);
|
||||
void resize_keyboards(size_t num_keyboards);
|
||||
|
||||
uint8_t get_keyboard_selector_width() const;
|
||||
void set_keyboard_selector_width(uint8_t width);
|
||||
|
||||
// Returns (pr2_data, pr3_data)
|
||||
std::pair<std::string, std::string> serialize(bool big_endian) const;
|
||||
|
||||
protected:
|
||||
template <bool IsBigEndian>
|
||||
void parse_t(const std::string& pr2_data);
|
||||
template <bool IsBigEndian>
|
||||
std::pair<std::string, std::string> serialize_t() const;
|
||||
|
||||
std::vector<std::unique_ptr<Keyboard>> keyboards;
|
||||
uint8_t keyboard_selector_width;
|
||||
};
|
||||
|
||||
class TextIndex {
|
||||
public:
|
||||
explicit TextIndex(
|
||||
const std::string& directory = "",
|
||||
std::function<std::shared_ptr<const std::string>(Version, const std::string&)> get_patch_file = nullptr);
|
||||
~TextIndex() = default;
|
||||
|
||||
void add_set(Version version, uint8_t language, std::shared_ptr<const TextSet> ts);
|
||||
void delete_set(Version version, uint8_t language);
|
||||
|
||||
const std::string& get(Version version, uint8_t language, size_t collection_index, size_t string_index) const;
|
||||
const std::vector<std::string>& get(Version version, uint8_t language, size_t collection_index) const;
|
||||
std::shared_ptr<const TextSet> get(Version version, uint8_t language) const;
|
||||
|
||||
protected:
|
||||
static uint32_t key_for_set(Version version, uint8_t language);
|
||||
|
||||
PrefixedLogger log;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<const TextSet>> sets;
|
||||
};
|
||||
@@ -1,75 +0,0 @@
|
||||
#include "UnicodeTextSet.hh"
|
||||
|
||||
#include <phosg/Strings.hh>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Compression.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
vector<vector<string>> parse_unicode_text_set(const string& prs_data) {
|
||||
string data = prs_decompress(prs_data);
|
||||
StringReader r(data);
|
||||
|
||||
uint32_t num_collections = r.get_u32l();
|
||||
deque<uint32_t> collection_sizes;
|
||||
while (collection_sizes.size() < num_collections) {
|
||||
collection_sizes.emplace_back(r.get_u32l());
|
||||
}
|
||||
|
||||
vector<vector<string>> ret;
|
||||
ret.reserve(collection_sizes.size());
|
||||
while (!collection_sizes.empty()) {
|
||||
uint32_t num_strings = collection_sizes.front();
|
||||
collection_sizes.pop_front();
|
||||
|
||||
auto& strings = ret.emplace_back();
|
||||
strings.reserve(num_strings);
|
||||
while (strings.size() < num_strings) {
|
||||
StringReader sub_r = r.sub(r.get_u32l());
|
||||
StringWriter w;
|
||||
for (uint16_t ch = sub_r.get_u16l(); ch != 0; ch = sub_r.get_u16l()) {
|
||||
w.put_u16l(ch);
|
||||
}
|
||||
strings.emplace_back(tt_utf16_to_utf8(w.str()));
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
string serialize_unicode_text_set(const vector<vector<string>>& collections) {
|
||||
StringWriter header_w;
|
||||
StringWriter data_w;
|
||||
|
||||
size_t total_num_strings = 0;
|
||||
header_w.put_u32l(collections.size());
|
||||
for (const auto& collection : collections) {
|
||||
header_w.put_u32l(collection.size());
|
||||
total_num_strings += collection.size();
|
||||
}
|
||||
|
||||
unordered_map<string, uint32_t> encoded;
|
||||
|
||||
size_t data_base_offset = (total_num_strings * 4) + header_w.size();
|
||||
for (const auto& collection : collections) {
|
||||
for (const auto& s : collection) {
|
||||
auto encoded_it = encoded.find(s);
|
||||
if (encoded_it == encoded.end()) {
|
||||
uint32_t offset = data_base_offset + data_w.size();
|
||||
encoded.emplace(s, offset);
|
||||
string s_utf16 = tt_utf8_to_utf16(s);
|
||||
data_w.write(s_utf16.data(), s_utf16.size());
|
||||
data_w.put_u16(0);
|
||||
while (data_w.size() & 3) {
|
||||
data_w.put_u8(0);
|
||||
}
|
||||
} else {
|
||||
header_w.put_u32l(encoded_it->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header_w.write(data_w.str());
|
||||
return std::move(header_w.str());
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
std::vector<std::vector<std::string>> parse_unicode_text_set(const std::string& prs_data);
|
||||
std::string serialize_unicode_text_set(const std::vector<std::vector<std::string>>& collections);
|
||||
@@ -24,6 +24,10 @@ enum class Version {
|
||||
UNKNOWN = 15,
|
||||
};
|
||||
|
||||
constexpr size_t NUM_VERSIONS = static_cast<size_t>(Version::BB_V4) + 1;
|
||||
constexpr size_t NUM_PATCH_VERSIONS = static_cast<size_t>(Version::BB_PATCH) + 1;
|
||||
constexpr size_t NUM_NON_PATCH_VERSIONS = NUM_VERSIONS - NUM_PATCH_VERSIONS;
|
||||
|
||||
template <>
|
||||
const char* name_for_enum<Version>(Version v);
|
||||
template <>
|
||||
|
||||
@@ -205,7 +205,8 @@ WordSelectTable::WordSelectTable(
|
||||
this->name_to_token.emplace(token->canonical_name, token);
|
||||
}
|
||||
|
||||
array<const WordSelectSet*, 12> ws_sets = {
|
||||
static_assert(NUM_NON_PATCH_VERSIONS == 12, "Don\'t forget to update the WordSelectTable constructor");
|
||||
array<const WordSelectSet*, NUM_NON_PATCH_VERSIONS> ws_sets = {
|
||||
&dc_nte_ws, &dc_112000_ws, &dc_v1_ws, &dc_v2_ws,
|
||||
&pc_nte_ws, &pc_v2_ws, &gc_nte_ws, &gc_v3_ws,
|
||||
&gc_ep3_nte_ws, &gc_ep3_ws, &xb_v3_ws, &bb_v4_ws};
|
||||
|
||||
@@ -37,7 +37,9 @@
|
||||
"ExternalAddress": "10.0.1.5",
|
||||
|
||||
// Port to listen for DNS queries on. To disable the DNS server, comment this
|
||||
// out or set it to zero.
|
||||
// out or set it to zero. By default, the DNS server listens on all
|
||||
// interfaces, but you can specify an interface by replacing this with a list
|
||||
// of [interface_addr_or_name, port].
|
||||
"DNSServerPort": 53,
|
||||
|
||||
// Ports to listen for game connections on.
|
||||
@@ -45,6 +47,12 @@
|
||||
// Format of entries in this dictionary:
|
||||
// name: [port, version, behavior]
|
||||
|
||||
// port is normally just an integer (which will cause the server to listen
|
||||
// on that port on all interfaces), but you can also replace the integer
|
||||
// with a 2-list of [address, port] to listen in a specific port. For
|
||||
// example, that might look like:
|
||||
// "xb-login": [["en0", 9500], "xb", "login_server"],
|
||||
|
||||
// Various versions of PSO hardcode these ports in the clients. Don't change
|
||||
// these unless you don't want to support certain versions of PSO.
|
||||
// Note: The pc_console_detect behavior is used for separating PSO PC and
|
||||
@@ -767,6 +775,10 @@
|
||||
// available on the proxy server.
|
||||
"CheatModeBehavior": "OnByDefault",
|
||||
|
||||
// Whether to enable rare drop notifications by default. Players can toggle
|
||||
// this behavior for themselves with the $rarenotifs command.
|
||||
"RareNotificationsEnabledByDefault": false,
|
||||
|
||||
// Whether to enable patches on Episode 3 USA. This functionality depends on
|
||||
// exploiting a bug in Episode 3, and while it seems to work reliably on
|
||||
// Dolphin, it hasn't been tested on a real GameCube. So, newserv doesn't
|
||||
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user