switch to CMake
This commit is contained in:
@@ -0,0 +1,934 @@
|
||||
#include "ChatCommands.hh"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <phosg/Strings.hh>
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "Server.hh"
|
||||
#include "Lobby.hh"
|
||||
#include "Client.hh"
|
||||
#include "SendCommands.hh"
|
||||
#include "Text.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const vector<string> section_id_to_name({
|
||||
"Viridia", "Greennill", "Skyly", "Bluefull", "Purplenum", "Pinkal", "Redria",
|
||||
"Oran", "Yellowboze", "Whitill"});
|
||||
|
||||
const unordered_map<string, uint8_t> name_to_section_id({
|
||||
{"viridia", 0},
|
||||
{"greennill", 1},
|
||||
{"skyly", 2},
|
||||
{"bluefull", 3},
|
||||
{"purplenum", 4},
|
||||
{"pinkal", 5},
|
||||
{"redria", 6},
|
||||
{"oran", 7},
|
||||
{"yellowboze", 8},
|
||||
{"whitill", 9}});
|
||||
|
||||
const vector<string> lobby_event_to_name({
|
||||
"none", "xmas", "none", "val", "easter", "hallo", "sonic", "newyear",
|
||||
"summer", "white", "wedding", "fall", "s-spring", "s-summer", "spring"});
|
||||
|
||||
const unordered_map<string, uint8_t> name_to_lobby_event({
|
||||
{"none", 0},
|
||||
{"xmas", 1},
|
||||
{"val", 3},
|
||||
{"easter", 4},
|
||||
{"hallo", 5},
|
||||
{"sonic", 6},
|
||||
{"newyear", 7},
|
||||
{"summer", 8},
|
||||
{"white", 9},
|
||||
{"wedding", 10},
|
||||
{"fall", 11},
|
||||
{"s-spring", 12},
|
||||
{"s-summer", 13},
|
||||
{"spring", 14},
|
||||
});
|
||||
|
||||
const unordered_map<uint8_t, string> lobby_type_to_name({
|
||||
{0x00, "normal"},
|
||||
{0x0F, "inormal"},
|
||||
{0x10, "ipc"},
|
||||
{0x11, "iball"},
|
||||
{0x67, "cave2u"},
|
||||
{0xD4, "cave1"},
|
||||
{0xE9, "planet"},
|
||||
{0xEA, "clouds"},
|
||||
{0xED, "cave"},
|
||||
{0xEE, "jungle"},
|
||||
{0xEF, "forest2-2"},
|
||||
{0xF0, "forest2-1"},
|
||||
{0xF1, "windpower"},
|
||||
{0xF2, "overview"},
|
||||
{0xF3, "seaside"},
|
||||
{0xF4, "some?"},
|
||||
{0xF5, "dmorgue"},
|
||||
{0xF6, "caelum"},
|
||||
{0xF8, "digital"},
|
||||
{0xF9, "boss1"},
|
||||
{0xFA, "boss2"},
|
||||
{0xFB, "boss3"},
|
||||
{0xFC, "dragon"},
|
||||
{0xFD, "derolle"},
|
||||
{0xFE, "volopt"},
|
||||
{0xFF, "darkfalz"},
|
||||
});
|
||||
|
||||
const unordered_map<string, uint8_t> name_to_lobby_type({
|
||||
{"normal", 0x00},
|
||||
{"inormal", 0x0F},
|
||||
{"ipc", 0x10},
|
||||
{"iball", 0x11},
|
||||
{"cave1", 0xD4},
|
||||
{"cave2u", 0x67},
|
||||
{"dragon", 0xFC},
|
||||
{"derolle", 0xFD},
|
||||
{"volopt", 0xFE},
|
||||
{"darkfalz", 0xFF},
|
||||
{"planet", 0xE9},
|
||||
{"clouds", 0xEA},
|
||||
{"cave", 0xED},
|
||||
{"jungle", 0xEE},
|
||||
{"forest2-2", 0xEF},
|
||||
{"forest2-1", 0xF0},
|
||||
{"windpower", 0xF1},
|
||||
{"overview", 0xF2},
|
||||
{"seaside", 0xF3},
|
||||
{"some?", 0xF4},
|
||||
{"dmorgue", 0xF5},
|
||||
{"caelum", 0xF6},
|
||||
{"digital", 0xF8},
|
||||
{"boss1", 0xF9},
|
||||
{"boss2", 0xFA},
|
||||
{"boss3", 0xFB},
|
||||
{"knight", 0xFC},
|
||||
{"sky", 0xFE},
|
||||
{"morgue", 0xFF},
|
||||
});
|
||||
|
||||
const vector<string> tech_id_to_name({
|
||||
"foie", "gifoie", "rafoie",
|
||||
"barta", "gibarta", "rabarta",
|
||||
"zonde", "gizonde", "razonde",
|
||||
"grants", "deband", "jellen", "zalure", "shifta",
|
||||
"ryuker", "resta", "anti", "reverser", "megid"});
|
||||
|
||||
const unordered_map<string, uint8_t> name_to_tech_id({
|
||||
{"foie", 0},
|
||||
{"gifoie", 1},
|
||||
{"rafoie", 2},
|
||||
{"barta", 3},
|
||||
{"gibarta", 4},
|
||||
{"rabarta", 5},
|
||||
{"zonde", 6},
|
||||
{"gizonde", 7},
|
||||
{"razonde", 8},
|
||||
{"grants", 9},
|
||||
{"deband", 10},
|
||||
{"jellen", 11},
|
||||
{"zalure", 12},
|
||||
{"shifta", 13},
|
||||
{"ryuker", 14},
|
||||
{"resta", 15},
|
||||
{"anti", 16},
|
||||
{"reverser", 17},
|
||||
{"megid", 18},
|
||||
});
|
||||
|
||||
const vector<string> npc_id_to_name({
|
||||
"ninja", "rico", "sonic", "knuckles", "tails", "flowen", "elly"});
|
||||
|
||||
const unordered_map<string, uint8_t> name_to_npc_id({
|
||||
{"ninja", 0},
|
||||
{"rico", 1},
|
||||
{"sonic", 2},
|
||||
{"knuckles", 3},
|
||||
{"tails", 4},
|
||||
{"flowen", 5},
|
||||
{"elly", 6}});
|
||||
|
||||
const string& name_for_section_id(uint8_t section_id) {
|
||||
if (section_id < section_id_to_name.size()) {
|
||||
return section_id_to_name[section_id];
|
||||
} else {
|
||||
static const string ret = "<Unknown section id>";
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
u16string u16name_for_section_id(uint8_t section_id) {
|
||||
return decode_sjis(name_for_section_id(section_id));
|
||||
}
|
||||
|
||||
uint8_t section_id_for_name(const string& name) {
|
||||
try {
|
||||
return name_to_section_id.at(name);
|
||||
} catch (const out_of_range&) { }
|
||||
try {
|
||||
uint64_t x = stoul(name);
|
||||
if (x < section_id_to_name.size()) {
|
||||
return x;
|
||||
}
|
||||
} catch (const invalid_argument&) {
|
||||
} catch (const out_of_range&) { }
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
uint8_t section_id_for_name(const u16string& name) {
|
||||
return section_id_for_name(encode_sjis(name));
|
||||
}
|
||||
|
||||
const string& name_for_event(uint8_t event) {
|
||||
if (event < lobby_event_to_name.size()) {
|
||||
return lobby_event_to_name[event];
|
||||
} else {
|
||||
static const string ret = "<Unknown lobby event>";
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
u16string u16name_for_event(uint8_t event) {
|
||||
return decode_sjis(name_for_event(event));
|
||||
}
|
||||
|
||||
uint8_t event_for_name(const string& name) {
|
||||
try {
|
||||
return name_to_lobby_event.at(name);
|
||||
} catch (const out_of_range&) { }
|
||||
try {
|
||||
uint64_t x = stoul(name);
|
||||
if (x < lobby_event_to_name.size()) {
|
||||
return x;
|
||||
}
|
||||
} catch (const invalid_argument&) {
|
||||
} catch (const out_of_range&) { }
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
uint8_t event_for_name(const u16string& name) {
|
||||
return event_for_name(encode_sjis(name));
|
||||
}
|
||||
|
||||
const string& name_for_lobby_type(uint8_t type) {
|
||||
try {
|
||||
return lobby_type_to_name.at(type);
|
||||
} catch (const out_of_range&) {
|
||||
static const string ret = "<Unknown lobby type>";
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
u16string u16name_for_lobby_type(uint8_t type) {
|
||||
return decode_sjis(name_for_lobby_type(type));
|
||||
}
|
||||
|
||||
uint8_t lobby_type_for_name(const string& name) {
|
||||
try {
|
||||
return name_to_lobby_type.at(name);
|
||||
} catch (const out_of_range&) { }
|
||||
try {
|
||||
uint64_t x = stoul(name);
|
||||
if (x < lobby_type_to_name.size()) {
|
||||
return x;
|
||||
}
|
||||
} catch (const invalid_argument&) {
|
||||
} catch (const out_of_range&) { }
|
||||
return 0x80;
|
||||
}
|
||||
|
||||
uint8_t lobby_type_for_name(const u16string& name) {
|
||||
return lobby_type_for_name(encode_sjis(name));
|
||||
}
|
||||
|
||||
const string& name_for_technique(uint8_t tech) {
|
||||
try {
|
||||
return tech_id_to_name.at(tech);
|
||||
} catch (const out_of_range&) {
|
||||
static const string ret = "<Unknown technique>";
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
u16string u16name_for_technique(uint8_t tech) {
|
||||
return decode_sjis(name_for_technique(tech));
|
||||
}
|
||||
|
||||
uint8_t technique_for_name(const string& name) {
|
||||
try {
|
||||
return name_to_tech_id.at(name);
|
||||
} catch (const out_of_range&) { }
|
||||
try {
|
||||
uint64_t x = stoul(name);
|
||||
if (x < tech_id_to_name.size()) {
|
||||
return x;
|
||||
}
|
||||
} catch (const invalid_argument&) {
|
||||
} catch (const out_of_range&) { }
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
uint8_t technique_for_name(const u16string& name) {
|
||||
return technique_for_name(encode_sjis(name));
|
||||
}
|
||||
|
||||
const string& name_for_npc(uint8_t npc) {
|
||||
try {
|
||||
return npc_id_to_name.at(npc);
|
||||
} catch (const out_of_range&) {
|
||||
static const string ret = "<Unknown NPC>";
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
u16string u16name_for_npc(uint8_t npc) {
|
||||
return decode_sjis(name_for_npc(npc));
|
||||
}
|
||||
|
||||
uint8_t npc_for_name(const string& name) {
|
||||
try {
|
||||
return name_to_npc_id.at(name);
|
||||
} catch (const out_of_range&) { }
|
||||
try {
|
||||
uint64_t x = stoul(name);
|
||||
if (x < npc_id_to_name.size()) {
|
||||
return x;
|
||||
}
|
||||
} catch (const invalid_argument&) {
|
||||
} catch (const out_of_range&) { }
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
uint8_t npc_for_name(const u16string& name) {
|
||||
return npc_for_name(encode_sjis(name));
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Checks
|
||||
|
||||
|
||||
|
||||
class precondition_failed {
|
||||
public:
|
||||
precondition_failed(const char16_t* user_msg) : user_msg(user_msg) { }
|
||||
~precondition_failed() = default;
|
||||
|
||||
const char16_t* what() const {
|
||||
return this->user_msg;
|
||||
}
|
||||
|
||||
private:
|
||||
const char16_t* user_msg;
|
||||
};
|
||||
|
||||
static void check_privileges(shared_ptr<Client> c, uint64_t mask) {
|
||||
if (!c->license) {
|
||||
throw precondition_failed(u"$C6You are not\nlogged in.");
|
||||
}
|
||||
if ((c->license->privileges & mask) != mask) {
|
||||
throw precondition_failed(u"$C6You do not have\npermission to\nrun this command.");
|
||||
}
|
||||
}
|
||||
|
||||
static void check_version(shared_ptr<Client> c, GameVersion version) {
|
||||
if (c->version != version) {
|
||||
throw precondition_failed(u"$C6This command cannot\nbe used for your\nversion of PSO.");
|
||||
}
|
||||
}
|
||||
|
||||
static void check_not_version(shared_ptr<Client> c, GameVersion version) {
|
||||
if (c->version == version) {
|
||||
throw precondition_failed(u"$C6This command cannot\nbe used for your\nversion of PSO.");
|
||||
}
|
||||
}
|
||||
|
||||
static void check_is_game(shared_ptr<Lobby> l, bool is_game) {
|
||||
if (l->is_game() != is_game) {
|
||||
throw precondition_failed(is_game ?
|
||||
u"$C6This command cannot\nbe used in lobbies." :
|
||||
u"$C6This command cannot\nbe used in games.");
|
||||
}
|
||||
}
|
||||
|
||||
static void check_cheats_enabled(shared_ptr<Lobby> l) {
|
||||
if (!(l->flags & LobbyFlag::CheatsEnabled)) {
|
||||
throw precondition_failed(u"$C6This command can\nonly be used in\ncheat mode.");
|
||||
}
|
||||
}
|
||||
|
||||
static void check_is_leader(shared_ptr<Lobby> l, shared_ptr<Client> c) {
|
||||
if (l->leader_id != c->lobby_client_id) {
|
||||
throw precondition_failed(u"$C6This command can\nonly be used by\nthe game leader.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Message commands
|
||||
|
||||
static void command_lobby_info(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t*) {
|
||||
// no preconditions - everyone can use this command
|
||||
|
||||
if (!l) {
|
||||
send_text_message(c, u"$C6No lobby information");
|
||||
|
||||
} else if (l->is_game()) {
|
||||
string level_string;
|
||||
if (l->max_level == 0xFFFFFFFF) {
|
||||
level_string = string_printf("Levels: %d+", l->min_level + 1);
|
||||
} else {
|
||||
level_string = string_printf("Levels: %d-%d", l->min_level + 1, l->max_level + 1);
|
||||
}
|
||||
|
||||
send_text_message_printf(c, "$C6Game ID: %08X\n%s\nSection ID: %s\nCheat mode: %s",
|
||||
l->lobby_id, level_string.c_str(),
|
||||
name_for_section_id(l->section_id).c_str(),
|
||||
(l->flags & LobbyFlag::CheatsEnabled) ? "on" : "off");
|
||||
|
||||
} else {
|
||||
size_t num_clients = l->count_clients();
|
||||
size_t max_clients = l->max_clients;
|
||||
send_text_message_printf(c, "$C6Lobby ID: %08X\nPlayers: %zu/%zu",
|
||||
l->lobby_id, num_clients, max_clients);
|
||||
}
|
||||
}
|
||||
|
||||
static void command_ax(shared_ptr<ServerState>, shared_ptr<Lobby>,
|
||||
shared_ptr<Client> c, const char16_t* args) {
|
||||
check_privileges(c, Privilege::Announce);
|
||||
log(INFO, "[$ax from %010u] %S\n", c->license->serial_number, args);
|
||||
}
|
||||
|
||||
static void command_announce(shared_ptr<ServerState> s, shared_ptr<Lobby>,
|
||||
shared_ptr<Client> c, const char16_t* args) {
|
||||
check_privileges(c, Privilege::Announce);
|
||||
send_text_message(s, args);
|
||||
}
|
||||
|
||||
static void command_arrow(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t* args) {
|
||||
// no preconditions
|
||||
c->lobby_arrow_color = stoull(encode_sjis(args), NULL, 0);
|
||||
if (!l->is_game()) {
|
||||
send_arrow_update(l);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Lobby commands
|
||||
|
||||
static void command_cheat(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t*) {
|
||||
check_is_game(l, true);
|
||||
check_is_leader(l, c);
|
||||
|
||||
l->flags ^= LobbyFlag::CheatsEnabled;
|
||||
send_text_message_printf(l, "Cheat mode %s",
|
||||
(l->flags & LobbyFlag::CheatsEnabled) ? "enabled" : "disabled");
|
||||
|
||||
// if cheat mode was disabled, turn off all the cheat features that were on
|
||||
if (!(l->flags & LobbyFlag::CheatsEnabled)) {
|
||||
for (size_t x = 0; x < l->max_clients; x++) {
|
||||
auto c = l->clients[x];
|
||||
if (!c) {
|
||||
continue;
|
||||
}
|
||||
c->infinite_hp = false;
|
||||
c->infinite_tp = false;
|
||||
}
|
||||
memset(&l->next_drop_item, 0, sizeof(l->next_drop_item));
|
||||
}
|
||||
}
|
||||
|
||||
static void command_lobby_event(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t* args) {
|
||||
check_is_game(l, false);
|
||||
check_privileges(c, Privilege::ChangeEvent);
|
||||
|
||||
uint8_t new_event = event_for_name(args);
|
||||
if (new_event == 0xFF) {
|
||||
send_text_message(c, u"$C6No such lobby event.");
|
||||
return;
|
||||
}
|
||||
|
||||
l->event = new_event;
|
||||
send_change_event(l, l->event);
|
||||
}
|
||||
|
||||
static void command_lobby_event_all(shared_ptr<ServerState> s, shared_ptr<Lobby>,
|
||||
shared_ptr<Client> c, const char16_t* args) {
|
||||
check_privileges(c, Privilege::ChangeEvent);
|
||||
|
||||
uint8_t new_event = event_for_name(args);
|
||||
if (new_event == 0xFF) {
|
||||
send_text_message(c, u"$C6No such lobby event.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto l : s->all_lobbies()) {
|
||||
if (l->is_game() || !(l->flags & LobbyFlag::Default)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
l->event = new_event;
|
||||
send_change_event(l, l->event);
|
||||
}
|
||||
}
|
||||
|
||||
static void command_lobby_type(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t* args) {
|
||||
check_is_game(l, false);
|
||||
check_privileges(c, Privilege::ChangeEvent);
|
||||
|
||||
uint8_t new_type = lobby_type_for_name(args);
|
||||
if (new_type == 0x80) {
|
||||
send_text_message(c, u"$C6No such lobby type.");
|
||||
return;
|
||||
}
|
||||
|
||||
l->type = new_type;
|
||||
if (l->type < ((l->flags & LobbyFlag::Episode3) ? 20 : 15)) {
|
||||
l->type = l->block - 1;
|
||||
}
|
||||
|
||||
for (size_t x = 0; x < l->max_clients; x++) {
|
||||
if (l->clients[x]) {
|
||||
send_join_lobby(l->clients[x], l);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Game commands
|
||||
|
||||
static void command_password(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t* args) {
|
||||
check_is_game(l, true);
|
||||
check_is_leader(l, c);
|
||||
|
||||
if (!args[0]) {
|
||||
l->password[0] = 0;
|
||||
send_text_message(l, u"$C6Game unlocked");
|
||||
|
||||
} else {
|
||||
char16cpy(l->password, args, 0x10);
|
||||
auto encoded = encode_sjis(l->password);
|
||||
send_text_message_printf(l, "$C6Game password:\n%s",
|
||||
encoded.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static void command_min_level(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t* args) {
|
||||
check_is_game(l, true);
|
||||
check_is_leader(l, c);
|
||||
|
||||
u16string buffer;
|
||||
l->min_level = stoull(encode_sjis(args)) - 1;
|
||||
send_text_message_printf(l, "$C6Minimum level set to %" PRIu32,
|
||||
l->min_level + 1);
|
||||
}
|
||||
|
||||
static void command_max_level(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t* args) {
|
||||
check_is_game(l, true);
|
||||
check_is_leader(l, c);
|
||||
|
||||
l->max_level = stoull(encode_sjis(args)) - 1;
|
||||
if (l->max_level >= 200) {
|
||||
l->max_level = 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
if (l->max_level == 0xFFFFFFFF) {
|
||||
send_text_message(l, u"$C6Maximum level set to unlimited");
|
||||
} else {
|
||||
send_text_message_printf(l, "$C6Maximum level set to %" PRIu32, l->max_level + 1);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Character commands
|
||||
|
||||
static void command_edit(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t* args) {
|
||||
check_is_game(l, false);
|
||||
check_version(c, GameVersion::BB);
|
||||
|
||||
string encoded_args = encode_sjis(args);
|
||||
vector<string> tokens = split(encoded_args, L' ');
|
||||
|
||||
if (tokens.size() < 3) {
|
||||
send_text_message(c, u"$C6Not enough arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
if (tokens[0] == "atp") {
|
||||
c->player.disp.stats.atp = stoul(tokens[1]);
|
||||
} else if (tokens[0] == "mst") {
|
||||
c->player.disp.stats.mst = stoul(tokens[1]);
|
||||
} else if (tokens[0] == "evp") {
|
||||
c->player.disp.stats.evp = stoul(tokens[1]);
|
||||
} else if (tokens[0] == "hp") {
|
||||
c->player.disp.stats.hp = stoul(tokens[1]);
|
||||
} else if (tokens[0] == "dfp") {
|
||||
c->player.disp.stats.dfp = stoul(tokens[1]);
|
||||
} else if (tokens[0] == "ata") {
|
||||
c->player.disp.stats.ata = stoul(tokens[1]);
|
||||
} else if (tokens[0] == "lck") {
|
||||
c->player.disp.stats.lck = stoul(tokens[1]);
|
||||
} else if (tokens[0] == "meseta") {
|
||||
c->player.disp.meseta = stoul(tokens[1]);
|
||||
} else if (tokens[0] == "exp") {
|
||||
c->player.disp.experience = stoul(tokens[1]);
|
||||
} else if (tokens[0] == "level") {
|
||||
c->player.disp.level = stoul(tokens[1]) - 1;
|
||||
} else if (tokens[0] == "namecolor") {
|
||||
sscanf(tokens[1].c_str(), "%8X", &c->player.disp.name_color);
|
||||
} else if (tokens[0] == "secid") {
|
||||
uint8_t secid = section_id_for_name(decode_sjis(tokens[1]));
|
||||
if (secid == 0xFF) {
|
||||
send_text_message(c, u"$C6No such section ID.");
|
||||
return;
|
||||
} else {
|
||||
c->player.disp.section_id = secid;
|
||||
}
|
||||
} else if (tokens[0] == "name") {
|
||||
decode_sjis(c->player.disp.name, tokens[1].c_str(), 0x10);
|
||||
add_language_marker_inplace(c->player.disp.name, u'J', 0x10);
|
||||
} else if (tokens[0] == "npc") {
|
||||
if (tokens[1] == "none") {
|
||||
c->player.disp.extra_model = 0;
|
||||
c->player.disp.v2_flags &= 0xFD;
|
||||
} else {
|
||||
uint8_t npc = npc_for_name(decode_sjis(tokens[1]));
|
||||
if (npc == 0xFF) {
|
||||
send_text_message(c, u"$C6No such NPC.");
|
||||
return;
|
||||
}
|
||||
c->player.disp.extra_model = npc;
|
||||
c->player.disp.v2_flags |= 0x02;
|
||||
}
|
||||
} else if ((tokens[0] == "tech") && (tokens.size() > 2)) {
|
||||
uint8_t level = stoul(tokens[2]) - 1;
|
||||
if (tokens[1] == "all") {
|
||||
for (size_t x = 0; x < 0x14; x++) {
|
||||
c->player.disp.technique_levels[x] = level;
|
||||
}
|
||||
} else {
|
||||
uint8_t tech_id = technique_for_name(decode_sjis(tokens[1]));
|
||||
if (tech_id == 0xFF) {
|
||||
send_text_message(c, u"$C6No such technique.");
|
||||
return;
|
||||
}
|
||||
c->player.disp.technique_levels[tech_id] = level;
|
||||
}
|
||||
} else {
|
||||
send_text_message(c, u"$C6Unknown field.");
|
||||
return;
|
||||
}
|
||||
|
||||
// reload the client in the lobby/game
|
||||
send_player_leave_notification(l, c->lobby_client_id);
|
||||
send_complete_player_bb(c);
|
||||
s->send_lobby_join_notifications(l, c);
|
||||
}
|
||||
|
||||
static void command_change_bank(shared_ptr<ServerState>, shared_ptr<Lobby>,
|
||||
shared_ptr<Client> c, const char16_t*) {
|
||||
check_version(c, GameVersion::BB);
|
||||
|
||||
// TODO: implement this
|
||||
// TODO: make sure the bank name is filesystem-safe
|
||||
}
|
||||
|
||||
static void command_convert_char_to_bb(shared_ptr<ServerState> s,
|
||||
shared_ptr<Lobby> l, shared_ptr<Client> c, const char16_t* args) {
|
||||
check_is_game(l, false);
|
||||
check_not_version(c, GameVersion::BB);
|
||||
|
||||
vector<string> tokens = split(encode_sjis(args), L' ');
|
||||
if (tokens.size() != 3) {
|
||||
send_text_message(c, u"$C6Incorrect argument count");
|
||||
return;
|
||||
}
|
||||
|
||||
// username/password are tokens[0] and [1]
|
||||
c->pending_bb_save_player_index = stoul(tokens[2]) - 1;
|
||||
if (c->pending_bb_save_player_index > 3) {
|
||||
send_text_message(c, u"$C6Player index must be 1-4");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
s->license_manager->verify_bb(tokens[0].c_str(), tokens[1].c_str());
|
||||
} catch (const exception& e) {
|
||||
send_text_message_printf(c, "$C6Login failed: %s", e.what());
|
||||
return;
|
||||
}
|
||||
|
||||
c->pending_bb_save_username = tokens[0];
|
||||
|
||||
// request the player data. the client will respond with a 61, and the handler
|
||||
// for that command will execute the conversion
|
||||
send_command(c, 0x95, 0x00);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Administration commands
|
||||
|
||||
static void command_silence(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t* args) {
|
||||
check_privileges(c, Privilege::SilenceUser);
|
||||
|
||||
auto target = s->find_client(args);
|
||||
if (!target->license) {
|
||||
// this should be impossible, but I'll bet it's not actually
|
||||
send_text_message(c, u"$C6Client not logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
if (target->license->privileges & Privilege::Moderator) {
|
||||
send_text_message(c, u"$C6You do not have\nsufficient privileges.");
|
||||
return;
|
||||
}
|
||||
|
||||
target->can_chat = !target->can_chat;
|
||||
send_text_message_printf(l, "$C6%s %ssilenced", target->player.disp.name,
|
||||
target->can_chat ? "un" : "");
|
||||
}
|
||||
|
||||
static void command_kick(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t* args) {
|
||||
check_privileges(c, Privilege::KickUser);
|
||||
|
||||
auto target = s->find_client(args);
|
||||
if (!target->license) {
|
||||
// this should be impossible, but I'll bet it's not actually
|
||||
send_text_message(c, u"$C6Client not logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
if (target->license->privileges & Privilege::Moderator) {
|
||||
send_text_message(c, u"$C6You do not have\nsufficient privileges.");
|
||||
return;
|
||||
}
|
||||
|
||||
send_message_box(target, u"$C6You were kicked off by a moderator.");
|
||||
target->should_disconnect = true;
|
||||
send_text_message_printf(l, "$C6%s kicked off", target->player.disp.name);
|
||||
}
|
||||
|
||||
static void command_ban(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t* args) {
|
||||
check_privileges(c, Privilege::BanUser);
|
||||
|
||||
u16string args_str(args);
|
||||
size_t space_pos = args_str.find(L' ');
|
||||
if (space_pos == string::npos) {
|
||||
send_text_message(c, u"$C6Incorrect argument count");
|
||||
return;
|
||||
}
|
||||
|
||||
auto target = s->find_client(args_str.data() + space_pos + 1);
|
||||
if (!target->license) {
|
||||
// this should be impossible, but I'll bet it's not actually
|
||||
send_text_message(c, u"$C6Client not logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
if (target->license->privileges & Privilege::BanUser) {
|
||||
send_text_message(c, u"$C6You do not have\nsufficient privileges.");
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t usecs = stoull(encode_sjis(args), NULL, 0) * 1000000;
|
||||
|
||||
size_t unit_offset = 0;
|
||||
for (; isdigit(args[unit_offset]); unit_offset++);
|
||||
if (args[unit_offset] == 'm') {
|
||||
usecs *= 60;
|
||||
} else if (args[unit_offset] == 'h') {
|
||||
usecs *= 60 * 60;
|
||||
} else if (args[unit_offset] == 'd') {
|
||||
usecs *= 60 * 60 * 24;
|
||||
} else if (args[unit_offset] == 'w') {
|
||||
usecs *= 60 * 60 * 24 * 7;
|
||||
} else if (args[unit_offset] == 'M') {
|
||||
usecs *= 60 * 60 * 24 * 30;
|
||||
} else if (args[unit_offset] == 'y') {
|
||||
usecs *= 60 * 60 * 24 * 365;
|
||||
}
|
||||
|
||||
// TODO: put the length of time in this message. or don't; presumably the
|
||||
// person deserved it
|
||||
s->license_manager->ban_until(target->license->serial_number, now() + usecs);
|
||||
send_message_box(target, u"$C6You were banned by a moderator.");
|
||||
target->should_disconnect = true;
|
||||
auto encoded_name = encode_sjis(target->player.disp.name);
|
||||
send_text_message_printf(l, "$C6%s banned", encoded_name.c_str());
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Cheat commands
|
||||
|
||||
static void command_warp(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t* args) {
|
||||
check_is_game(l, true);
|
||||
check_cheats_enabled(l);
|
||||
|
||||
uint32_t area = stoul(encode_sjis(args), NULL, 0);
|
||||
if (!l->episode || (l->episode > 3)) {
|
||||
return;
|
||||
}
|
||||
if (c->area == area) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((l->episode == 1) && (area > 17)) {
|
||||
send_text_message(c, u"$C6Area numbers must be\n17 or less.");
|
||||
return;
|
||||
}
|
||||
if ((l->episode == 2) && (area > 17)) {
|
||||
send_text_message(c, u"$C6Area numbers must be\n17 or less.");
|
||||
return;
|
||||
}
|
||||
if ((l->episode == 3) && (area > 10)) {
|
||||
send_text_message(c, u"$C6Area numbers must be\n10 or less.");
|
||||
return;
|
||||
}
|
||||
|
||||
send_warp(c, area);
|
||||
}
|
||||
|
||||
static void command_infinite_hp(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t*) {
|
||||
check_is_game(l, true);
|
||||
check_cheats_enabled(l);
|
||||
|
||||
c->infinite_hp = !c->infinite_hp;
|
||||
send_text_message_printf(c, "$C6Infinite HP %s", c->infinite_hp ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
static void command_infinite_tp(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t*) {
|
||||
check_is_game(l, true);
|
||||
check_cheats_enabled(l);
|
||||
|
||||
c->infinite_tp = !c->infinite_tp;
|
||||
send_text_message_printf(c, "$C6Infinite TP %s", c->infinite_tp ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
static void command_item(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t* args) {
|
||||
check_is_game(l, true);
|
||||
check_cheats_enabled(l);
|
||||
|
||||
string data = parse_data_string(encode_sjis(args));
|
||||
if (data.size() < 2) {
|
||||
send_text_message(c, u"$C6Item codes must be\n2 bytes or more.");
|
||||
return;
|
||||
}
|
||||
if (data.size() > 16) {
|
||||
send_text_message(c, u"$C6Item codes must be\n16 bytes or fewer.");
|
||||
return;
|
||||
}
|
||||
|
||||
ItemData item_data;
|
||||
memset(&item_data, 0, sizeof(item_data));
|
||||
if (data.size() < 12) {
|
||||
memcpy(&l->next_drop_item.data.item_data1, data.data(), data.size());
|
||||
} else {
|
||||
memcpy(&l->next_drop_item.data.item_data1, data.data(), 12);
|
||||
memcpy(&l->next_drop_item.data.item_data2, data.data() + 12, 12 - data.size());
|
||||
}
|
||||
|
||||
send_text_message(c, u"$C6Next drop chosen.");
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
typedef void (*handler_t)(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const char16_t* args);
|
||||
struct ChatCommandDefinition {
|
||||
handler_t handler;
|
||||
const char16_t* usage;
|
||||
};
|
||||
|
||||
static const unordered_map<u16string, ChatCommandDefinition> chat_commands({
|
||||
{u"allevent" , {command_lobby_event_all , u"usage:\nallevent <name/ID>"}},
|
||||
{u"ann" , {command_announce , u"usage:\nann <message>"}},
|
||||
{u"arrow" , {command_arrow , u"usage:\narrow <color>"}},
|
||||
{u"ax" , {command_ax , u"usage:\nax <message>"}},
|
||||
{u"ban" , {command_ban , u"usage:\nban <name-or-number>"}},
|
||||
{u"bbchar" , {command_convert_char_to_bb, u"usage:\nbbchar <user> <pass> <1-4>"}},
|
||||
{u"changebank", {command_change_bank , u"usage:\nchangebank <bank name>"}},
|
||||
{u"cheat" , {command_cheat , u"usage:\nduh"}},
|
||||
{u"edit" , {command_edit , u"usage:\nedit <stat> <value>"}},
|
||||
{u"event" , {command_lobby_event , u"usage:\nevent <name>"}},
|
||||
{u"infhp" , {command_infinite_hp , u"usage:\nduh"}},
|
||||
{u"inftp" , {command_infinite_tp , u"usage:\nduh"}},
|
||||
{u"item" , {command_item , u"usage:\nitem <item-code>"}},
|
||||
{u"kick" , {command_kick , u"usage:\nkick <name-or-number>"}},
|
||||
{u"li" , {command_lobby_info , u"usage:\nli"}},
|
||||
{u"password" , {command_password , u"usage:\nlock [password]\nomit password to\nunlock game"}},
|
||||
{u"maxlevel" , {command_max_level , u"usage:\nmax_level <level>"}},
|
||||
{u"minlevel" , {command_min_level , u"usage:\nmin_level <level>"}},
|
||||
{u"silence" , {command_silence , u"usage:\nsilence <name-or-number>"}},
|
||||
{u"type" , {command_lobby_type , u"usage:\ntype <name>"}},
|
||||
{u"warp" , {command_warp , u"usage:\nwarp <area-number>"}},
|
||||
});
|
||||
|
||||
// this function is called every time any player sends a chat beginning with a dollar sign.
|
||||
// It is this function's responsibility to see if the chat is a command, and to
|
||||
// execute the command and block the chat if it is.
|
||||
void process_chat_command(std::shared_ptr<ServerState> s, std::shared_ptr<Lobby> l,
|
||||
std::shared_ptr<Client> c, const char16_t* text) {
|
||||
|
||||
// remove the chat command marker
|
||||
if (text[0] == u'$') {
|
||||
text++;
|
||||
}
|
||||
|
||||
u16string command_name;
|
||||
u16string text_str(text);
|
||||
size_t space_pos = text_str.find(u' ');
|
||||
if (space_pos != string::npos) {
|
||||
command_name = text_str.substr(0, space_pos);
|
||||
text_str = text_str.substr(space_pos + 1);
|
||||
} else {
|
||||
command_name = text_str;
|
||||
text_str.clear();
|
||||
}
|
||||
|
||||
const ChatCommandDefinition* def = NULL;
|
||||
try {
|
||||
def = &chat_commands.at(command_name);
|
||||
} catch (const out_of_range&) {
|
||||
send_text_message(c, u"$C6Unknown command.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
def->handler(s, l, c, text_str.c_str());
|
||||
} catch (const precondition_failed& e) {
|
||||
send_text_message(c, e.what());
|
||||
} catch (const exception& e) {
|
||||
send_text_message_printf(c, "$C6Failed:\n%s", e.what());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "ServerState.hh"
|
||||
#include "Lobby.hh"
|
||||
#include "Client.hh"
|
||||
|
||||
const std::string& name_for_section_id(uint8_t section_id);
|
||||
std::u16string u16name_for_section_id(uint8_t section_id);
|
||||
uint8_t section_id_for_name(const std::string& name);
|
||||
uint8_t section_id_for_name(const std::u16string& name);
|
||||
|
||||
const std::string& name_for_event(uint8_t event);
|
||||
std::u16string u16name_for_event(uint8_t event);
|
||||
uint8_t event_for_name(const std::string& name);
|
||||
uint8_t event_for_name(const std::u16string& name);
|
||||
|
||||
const std::string& name_for_lobby_type(uint8_t type);
|
||||
std::u16string u16name_for_lobby_type(uint8_t type);
|
||||
uint8_t lobby_type_for_name(const std::string& name);
|
||||
uint8_t lobby_type_for_name(const std::u16string& name);
|
||||
|
||||
const std::string& name_for_technique(uint8_t tech);
|
||||
std::u16string u16name_for_technique(uint8_t tech);
|
||||
uint8_t technique_for_name(const std::string& name);
|
||||
uint8_t technique_for_name(const std::u16string& name);
|
||||
|
||||
const std::string& name_for_npc(uint8_t npc);
|
||||
std::u16string u16name_for_npc(uint8_t npc);
|
||||
uint8_t npc_for_name(const std::string& name);
|
||||
uint8_t npc_for_name(const std::u16string& name);
|
||||
|
||||
void process_chat_command(std::shared_ptr<ServerState> s, std::shared_ptr<Lobby> l,
|
||||
std::shared_ptr<Client> c, const char16_t* text);
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "Client.hh"
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/bufferevent.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <phosg/Network.hh>
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "Version.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
Client::Client(struct bufferevent* bev, GameVersion version,
|
||||
ServerBehavior server_behavior) : version(version),
|
||||
flags(flags_for_version(version, 0)), bev(bev),
|
||||
server_behavior(server_behavior), should_disconnect(false),
|
||||
play_time_begin(now()), last_recv_time(this->play_time_begin),
|
||||
last_send_time(0), area(0), lobby_id(0), lobby_client_id(0),
|
||||
lobby_arrow_color(0), next_exp_value(0), infinite_hp(false),
|
||||
infinite_tp(false), can_chat(true) {
|
||||
|
||||
int fd = bufferevent_getfd(this->bev);
|
||||
get_socket_addresses(fd, &this->local_addr, &this->remote_addr);
|
||||
memset(this->name, 0, sizeof(this->name));
|
||||
memset(&this->next_connection_addr, 0, sizeof(this->next_connection_addr));
|
||||
}
|
||||
|
||||
bool Client::send(string&& data) {
|
||||
if (!this->bev) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->crypt_out.get()) {
|
||||
this->crypt_out->encrypt(const_cast<char*>(data.data()), data.size());
|
||||
}
|
||||
|
||||
struct evbuffer* buf = bufferevent_get_output(this->bev);
|
||||
evbuffer_add(buf, data.data(), data.size());
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include <netinet/in.h>
|
||||
|
||||
#include <memory>
|
||||
#include <phosg/Concurrency.hh>
|
||||
|
||||
#include "License.hh"
|
||||
#include "Player.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
|
||||
|
||||
|
||||
enum class ServerBehavior {
|
||||
SplitReconnect = 0,
|
||||
LoginServer,
|
||||
LobbyServer,
|
||||
DataServerBB,
|
||||
PatchServer,
|
||||
};
|
||||
|
||||
struct ClientConfig {
|
||||
uint32_t magic; // must be set to 0x48615467
|
||||
uint8_t bb_game_state; // status of client connecting on BB
|
||||
uint8_t bb_player_index; // selected char
|
||||
uint16_t flags; // just in case we lose them somehow between connections
|
||||
uint16_t ports[4]; // used by shipgate clients
|
||||
uint32_t unused[4];
|
||||
};
|
||||
|
||||
struct ClientConfigBB {
|
||||
ClientConfig cfg;
|
||||
uint32_t unused[2];
|
||||
};
|
||||
|
||||
struct Client {
|
||||
// license & account
|
||||
std::shared_ptr<const License> license;
|
||||
char16_t name[0x20];
|
||||
ClientConfigBB config;
|
||||
GameVersion version;
|
||||
uint16_t flags;
|
||||
|
||||
// encryption
|
||||
std::unique_ptr<PSOEncryption> crypt_in;
|
||||
std::unique_ptr<PSOEncryption> crypt_out;
|
||||
|
||||
// network
|
||||
struct sockaddr_storage local_addr;
|
||||
struct sockaddr_storage remote_addr;
|
||||
struct bufferevent* bev;
|
||||
struct sockaddr_storage next_connection_addr;
|
||||
ServerBehavior server_behavior;
|
||||
bool should_disconnect;
|
||||
std::string recv_buffer;
|
||||
|
||||
// timing & menus
|
||||
uint64_t play_time_begin; // time of connection (used for incrementing play time on BB)
|
||||
uint64_t last_recv_time; // time of last data received
|
||||
uint64_t last_send_time; // time of last data sent
|
||||
|
||||
// lobby/positioning
|
||||
uint32_t area; // which area is the client in?
|
||||
uint32_t lobby_id; // which lobby is this person in?
|
||||
uint8_t lobby_client_id; // which client number is this person?
|
||||
uint8_t lobby_arrow_color; // lobby arrow color ID
|
||||
Player player;
|
||||
|
||||
// miscellaneous (used by chat commands)
|
||||
uint32_t next_exp_value; // next EXP value to give
|
||||
bool infinite_hp; // cheats enabled
|
||||
bool infinite_tp; // cheats enabled
|
||||
bool can_chat;
|
||||
std::string pending_bb_save_username;
|
||||
uint8_t pending_bb_save_player_index;
|
||||
|
||||
Client(struct bufferevent* bev, GameVersion version,
|
||||
ServerBehavior server_behavior);
|
||||
|
||||
// adds data to the client's output buffer, encrypting it first
|
||||
bool send(std::string&& data);
|
||||
};
|
||||
@@ -0,0 +1,365 @@
|
||||
#include "Compression.hh"
|
||||
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <phosg/Strings.hh>
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
struct prs_compress_ctx {
|
||||
unsigned char bitpos;
|
||||
std::string forward_log;
|
||||
std::string output;
|
||||
|
||||
prs_compress_ctx() : bitpos(0) { }
|
||||
|
||||
string finish() {
|
||||
this->put_control_bit(0);
|
||||
this->put_control_bit(1);
|
||||
if (this->bitpos != 0) {
|
||||
this->forward_log[0] = ((this->forward_log[0] << this->bitpos) >> 8);
|
||||
}
|
||||
this->put_static_data(0);
|
||||
this->put_static_data(0);
|
||||
this->output += this->forward_log;
|
||||
this->forward_log.clear();
|
||||
return this->output;
|
||||
}
|
||||
|
||||
void put_control_bit_nosave(bool bit) {
|
||||
this->forward_log[0] = this->forward_log[0] >> 1;
|
||||
this->forward_log[0] |= ((!!bit) << 7);
|
||||
this->bitpos++;
|
||||
}
|
||||
|
||||
void put_control_save() {
|
||||
if (this->bitpos >= 8) {
|
||||
this->bitpos = 0;
|
||||
this->output += this->forward_log;
|
||||
this->forward_log.resize(1);
|
||||
this->forward_log[0] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void put_control_bit(bool bit) {
|
||||
this->put_control_bit_nosave(bit);
|
||||
this->put_control_save();
|
||||
}
|
||||
|
||||
void put_static_data(uint8_t data) {
|
||||
this->forward_log += static_cast<char>(data);
|
||||
}
|
||||
|
||||
void raw_byte(uint8_t value) {
|
||||
this->put_control_bit_nosave(1);
|
||||
this->put_static_data(value);
|
||||
this->put_control_save();
|
||||
}
|
||||
|
||||
void short_copy(ssize_t offset, uint8_t size) {
|
||||
size -= 2;
|
||||
this->put_control_bit(0);
|
||||
this->put_control_bit(0);
|
||||
this->put_control_bit((size >> 1) & 1);
|
||||
this->put_control_bit_nosave(size & 1);
|
||||
this->put_static_data(offset & 0xFF);
|
||||
this->put_control_save();
|
||||
}
|
||||
|
||||
void long_copy(ssize_t offset, uint8_t size) {
|
||||
if (size <= 9) {
|
||||
this->put_control_bit(0);
|
||||
this->put_control_bit_nosave(1);
|
||||
this->put_static_data(((offset << 3) & 0xF8) | ((size - 2) & 0x07));
|
||||
this->put_static_data((offset >> 5) & 0xFF);
|
||||
this->put_control_save();
|
||||
} else {
|
||||
this->put_control_bit(0);
|
||||
this->put_control_bit_nosave(1);
|
||||
this->put_static_data((offset << 3) & 0xF8);
|
||||
this->put_static_data((offset >> 5) & 0xFF);
|
||||
this->put_static_data(size - 1);
|
||||
this->put_control_save();
|
||||
}
|
||||
}
|
||||
|
||||
void copy(ssize_t offset, uint8_t size) {
|
||||
if ((offset > -0x100) && (size <= 5)) {
|
||||
this->short_copy(offset, size);
|
||||
} else {
|
||||
this->long_copy(offset, size);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
string prs_compress(const string& data) {
|
||||
prs_compress_ctx pc;
|
||||
|
||||
ssize_t data_ssize = static_cast<ssize_t>(data.size());
|
||||
ssize_t read_offset = 0;
|
||||
while (read_offset < data_ssize) {
|
||||
|
||||
// look for a chunk of data in history matching what's at the current offset
|
||||
ssize_t best_offset = 0;
|
||||
ssize_t best_size = 0;
|
||||
for (ssize_t this_offset = -3;
|
||||
(this_offset + data_ssize >= 0) &&
|
||||
(this_offset > -0x1FF0) &&
|
||||
(best_size < 255);
|
||||
this_offset--) {
|
||||
|
||||
// for this offset, expand the match as much as possible
|
||||
ssize_t this_size = 1;
|
||||
while ((this_size < 0x100) && // max copy size is 255 bytes
|
||||
((this_offset + this_size) < 0) && // don't copy past the read offset
|
||||
(this_size <= data_ssize - read_offset) && // don't copy past the end
|
||||
!memcmp(data.data() + read_offset + this_offset,
|
||||
data.data() + read_offset, this_size)) {
|
||||
this_size++;
|
||||
}
|
||||
this_size--;
|
||||
|
||||
if (this_size > best_size) {
|
||||
best_offset = this_offset;
|
||||
best_size = this_size;
|
||||
}
|
||||
}
|
||||
|
||||
// if there are no good matches, write the byte directly
|
||||
if (best_size < 3) {
|
||||
pc.raw_byte(data[read_offset]);
|
||||
read_offset++;
|
||||
|
||||
} else {
|
||||
pc.copy(best_offset, best_size);
|
||||
read_offset += best_size;
|
||||
}
|
||||
}
|
||||
|
||||
return pc.finish();
|
||||
}
|
||||
|
||||
|
||||
|
||||
static int16_t get_u8_or_eof(StringReader& r) {
|
||||
return r.eof() ? -1 : r.get_u8();
|
||||
}
|
||||
|
||||
string prs_decompress(const string& data, size_t max_size) {
|
||||
string output;
|
||||
StringReader r(data.data(), data.size());
|
||||
|
||||
int32_t r3, r5;
|
||||
int bitpos = 9;
|
||||
int16_t currentbyte; // int16_t because it can be -1 when EOF occurs
|
||||
int flag;
|
||||
int offset;
|
||||
unsigned long x, t;
|
||||
|
||||
currentbyte = get_u8_or_eof(r);
|
||||
if (currentbyte == EOF) {
|
||||
return output;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
bitpos--;
|
||||
if (bitpos == 0) {
|
||||
currentbyte = get_u8_or_eof(r);
|
||||
if (currentbyte == EOF) {
|
||||
return output;
|
||||
}
|
||||
bitpos = 8;
|
||||
}
|
||||
flag = currentbyte & 1;
|
||||
currentbyte = currentbyte >> 1;
|
||||
if (flag) {
|
||||
int ch = get_u8_or_eof(r);
|
||||
if (ch == EOF) {
|
||||
return output;
|
||||
}
|
||||
output += static_cast<char>(ch);
|
||||
if (max_size && (output.size() > max_size)) {
|
||||
throw runtime_error("maximum output size exceeded");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
bitpos--;
|
||||
if (bitpos == 0) {
|
||||
currentbyte = get_u8_or_eof(r);
|
||||
if (currentbyte == EOF) {
|
||||
return output;
|
||||
}
|
||||
bitpos = 8;
|
||||
}
|
||||
flag = currentbyte & 1;
|
||||
currentbyte = currentbyte >> 1;
|
||||
if (flag) {
|
||||
r3 = get_u8_or_eof(r);
|
||||
if (r3 == EOF) {
|
||||
return output;
|
||||
}
|
||||
int high_byte = get_u8_or_eof(r);
|
||||
if (high_byte == EOF) {
|
||||
return output;
|
||||
}
|
||||
offset = ((high_byte & 0xFF) << 8) | (r3 & 0xFF);
|
||||
if (offset == 0) {
|
||||
return output;
|
||||
}
|
||||
r3 = r3 & 0x00000007;
|
||||
r5 = (offset >> 3) | 0xFFFFE000;
|
||||
if (r3 == 0) {
|
||||
flag = 0;
|
||||
r3 = get_u8_or_eof(r);
|
||||
if (r3 == EOF) {
|
||||
return output;
|
||||
}
|
||||
r3 = (r3 & 0xFF) + 1;
|
||||
} else {
|
||||
r3 += 2;
|
||||
}
|
||||
} else {
|
||||
r3 = 0;
|
||||
for (x = 0; x < 2; x++) {
|
||||
bitpos--;
|
||||
if (bitpos == 0) {
|
||||
currentbyte = get_u8_or_eof(r);
|
||||
if (currentbyte == EOF) {
|
||||
return output;
|
||||
}
|
||||
bitpos = 8;
|
||||
}
|
||||
flag = currentbyte & 1;
|
||||
currentbyte = currentbyte >> 1;
|
||||
offset = r3 << 1;
|
||||
r3 = offset | flag;
|
||||
}
|
||||
offset = get_u8_or_eof(r);
|
||||
if (offset == EOF) {
|
||||
return output;
|
||||
}
|
||||
r3 += 2;
|
||||
r5 = offset | 0xFFFFFF00;
|
||||
}
|
||||
if (r3 == 0) {
|
||||
continue;
|
||||
}
|
||||
t = r3;
|
||||
for (x = 0; x < t; x++) {
|
||||
output += output.at(output.size() + r5);
|
||||
if (max_size && (output.size() > max_size)) {
|
||||
throw runtime_error("maximum output size exceeded");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t prs_decompress_size(const string& data, size_t max_size) {
|
||||
size_t output_size = 0;
|
||||
StringReader r(data.data(), data.size());
|
||||
|
||||
int32_t r3;
|
||||
int bitpos = 9;
|
||||
int16_t currentbyte; // int16_t because it can be -1 when EOF occurs
|
||||
int flag;
|
||||
int offset;
|
||||
unsigned long x;
|
||||
|
||||
currentbyte = get_u8_or_eof(r);
|
||||
if (currentbyte == EOF) {
|
||||
return output_size;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
bitpos--;
|
||||
if (bitpos == 0) {
|
||||
currentbyte = get_u8_or_eof(r);
|
||||
if (currentbyte == EOF) {
|
||||
return output_size;
|
||||
}
|
||||
bitpos = 8;
|
||||
}
|
||||
flag = currentbyte & 1;
|
||||
currentbyte = currentbyte >> 1;
|
||||
if (flag) {
|
||||
int ch = get_u8_or_eof(r);
|
||||
if (ch == EOF) {
|
||||
return output_size;
|
||||
}
|
||||
output_size++;
|
||||
if (max_size && (output_size > max_size)) {
|
||||
throw runtime_error("maximum output size exceeded");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
bitpos--;
|
||||
if (bitpos == 0) {
|
||||
currentbyte = get_u8_or_eof(r);
|
||||
if (currentbyte == EOF) {
|
||||
return output_size;
|
||||
}
|
||||
bitpos = 8;
|
||||
}
|
||||
flag = currentbyte & 1;
|
||||
currentbyte = currentbyte >> 1;
|
||||
if (flag) {
|
||||
r3 = get_u8_or_eof(r);
|
||||
if (r3 == EOF) {
|
||||
return output_size;
|
||||
}
|
||||
int high_byte = get_u8_or_eof(r);
|
||||
if (high_byte == EOF) {
|
||||
return output_size;
|
||||
}
|
||||
offset = ((high_byte & 0xFF) << 8) | (r3 & 0xFF);
|
||||
if (offset == 0) {
|
||||
return output_size;
|
||||
}
|
||||
r3 = r3 & 0x00000007;
|
||||
if (r3 == 0) {
|
||||
flag = 0;
|
||||
r3 = get_u8_or_eof(r);
|
||||
if (r3 == EOF) {
|
||||
return output_size;
|
||||
}
|
||||
r3 = (r3 & 0xFF) + 1;
|
||||
} else {
|
||||
r3 += 2;
|
||||
}
|
||||
} else {
|
||||
r3 = 0;
|
||||
for (x = 0; x < 2; x++) {
|
||||
bitpos--;
|
||||
if (bitpos == 0) {
|
||||
currentbyte = get_u8_or_eof(r);
|
||||
if (currentbyte == EOF) {
|
||||
return output_size;
|
||||
}
|
||||
bitpos = 8;
|
||||
}
|
||||
flag = currentbyte & 1;
|
||||
currentbyte = currentbyte >> 1;
|
||||
offset = r3 << 1;
|
||||
r3 = offset | flag;
|
||||
}
|
||||
offset = get_u8_or_eof(r);
|
||||
if (offset == EOF) {
|
||||
return output_size;
|
||||
}
|
||||
r3 += 2;
|
||||
}
|
||||
if (r3 == 0) {
|
||||
continue;
|
||||
}
|
||||
output_size += r3;
|
||||
if (max_size && (output_size > max_size)) {
|
||||
throw runtime_error("maximum output size exceeded");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
|
||||
|
||||
std::string prs_compress(const std::string& data);
|
||||
std::string prs_decompress(const std::string& data, size_t max_size = 0);
|
||||
size_t prs_decompress_size(const std::string& data, size_t max_size = 0);
|
||||
@@ -0,0 +1,107 @@
|
||||
#include "DNSServer.hh"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <poll.h>
|
||||
#include <netinet/in.h>
|
||||
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/Network.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include "NetworkAddresses.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
DNSServer::DNSServer(shared_ptr<struct event_base> base,
|
||||
uint32_t local_connect_address, uint32_t external_connect_address) :
|
||||
base(base), local_connect_address(local_connect_address),
|
||||
external_connect_address(external_connect_address) { }
|
||||
|
||||
DNSServer::~DNSServer() {
|
||||
for (const auto& it : this->fd_to_receive_event) {
|
||||
close(it.first);
|
||||
}
|
||||
}
|
||||
|
||||
void DNSServer::listen(const std::string& socket_path) {
|
||||
this->add_socket(::listen(socket_path, 0, 0));
|
||||
}
|
||||
|
||||
void DNSServer::listen(const std::string& addr, int port) {
|
||||
this->add_socket(::listen(addr, port, 0));
|
||||
}
|
||||
|
||||
void DNSServer::listen(int port) {
|
||||
this->add_socket(::listen("", port, 0));
|
||||
}
|
||||
|
||||
void DNSServer::add_socket(int fd) {
|
||||
unique_ptr<struct event, void(*)(struct event*)> e(event_new(this->base.get(),
|
||||
fd, EV_READ | EV_PERSIST, &DNSServer::dispatch_on_receive_message,
|
||||
this), event_free);
|
||||
event_add(e.get(), NULL);
|
||||
this->fd_to_receive_event.emplace(fd, move(e));
|
||||
}
|
||||
|
||||
void DNSServer::dispatch_on_receive_message(evutil_socket_t fd,
|
||||
short events, void* ctx) {
|
||||
reinterpret_cast<DNSServer*>(ctx)->on_receive_message(fd, events);
|
||||
}
|
||||
|
||||
void DNSServer::on_receive_message(int fd, short) {
|
||||
for (;;) {
|
||||
sockaddr_in remote;
|
||||
socklen_t remote_size = sizeof(sockaddr_in);
|
||||
memset(&remote, 0, remote_size);
|
||||
|
||||
string input(2048, 0);
|
||||
ssize_t bytes = recvfrom(fd, const_cast<char*>(input.data()), input.size(),
|
||||
0, reinterpret_cast<sockaddr*>(&remote), &remote_size);
|
||||
|
||||
if (bytes < 0) {
|
||||
if (errno != EAGAIN) {
|
||||
log(INFO, "[DNSServer] input error %d", errno);
|
||||
throw runtime_error("cannot read from udp socket");
|
||||
}
|
||||
break;
|
||||
|
||||
} else if (bytes == 0) {
|
||||
break;
|
||||
|
||||
} else { // bytes > 0
|
||||
input.resize(bytes);
|
||||
|
||||
uint32_t remote_address = bswap32(remote.sin_addr.s_addr);
|
||||
uint32_t connect_address;
|
||||
if (is_local_address(remote_address)) {
|
||||
connect_address = this->local_connect_address;
|
||||
} else {
|
||||
connect_address = this->external_connect_address;
|
||||
}
|
||||
|
||||
if (input.size() >= 0x0C) {
|
||||
string response;
|
||||
size_t name_len = strlen(input.data() + 0x0C) + 1;
|
||||
|
||||
uint32_t connect_address_be = bswap32(connect_address);
|
||||
response.append(input.substr(0, 2));
|
||||
response.append("\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00", 10);
|
||||
response.append(input.substr(12, name_len));
|
||||
response.append("\x00\x01\x00\x01\xC0\x0C\x00\x01\x00\x01\x00\x00\x00\x3C\x00\x04", 16);
|
||||
response.append(reinterpret_cast<const char*>(&connect_address_be), 4);
|
||||
|
||||
sendto(fd, response.data(), response.size(), 0,
|
||||
reinterpret_cast<const sockaddr*>(&remote), remote_size);
|
||||
} else {
|
||||
log(WARNING, "[DNSServer] input query too small");
|
||||
print_data(stderr, input);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <set>
|
||||
|
||||
|
||||
class DNSServer {
|
||||
public:
|
||||
DNSServer(std::shared_ptr<struct event_base> base,
|
||||
uint32_t local_connect_address, uint32_t external_connect_address);
|
||||
DNSServer(const DNSServer&) = delete;
|
||||
DNSServer(DNSServer&&) = delete;
|
||||
virtual ~DNSServer();
|
||||
|
||||
void listen(const std::string& socket_path);
|
||||
void listen(const std::string& addr, int port);
|
||||
void listen(int port);
|
||||
void add_socket(int fd);
|
||||
|
||||
private:
|
||||
std::shared_ptr<struct event_base> base;
|
||||
std::unordered_map<int, std::unique_ptr<struct event, void(*)(struct event*)>> fd_to_receive_event;
|
||||
uint32_t local_connect_address;
|
||||
uint32_t external_connect_address;
|
||||
|
||||
static void dispatch_on_receive_message(evutil_socket_t fd, short events,
|
||||
void* ctx);
|
||||
void on_receive_message(int fd, short event);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
#include "FileContentsCache.hh"
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
FileContentsCache::File::File(const string& name, shared_ptr<const string> contents,
|
||||
uint64_t load_time) : name(name), contents(contents), load_time(load_time) { }
|
||||
|
||||
shared_ptr<const string> FileContentsCache::get(const std::string& name) {
|
||||
|
||||
uint64_t t = now();
|
||||
try {
|
||||
auto& entry = this->name_to_file.at(name);
|
||||
if (t - entry.load_time < 300000000) { // not 5 minutes old? return it
|
||||
return entry.contents;
|
||||
}
|
||||
} catch (const out_of_range& e) { }
|
||||
|
||||
shared_ptr<const string> contents(new string(load_file(name)));
|
||||
this->name_to_file.erase(name);
|
||||
this->name_to_file.emplace(piecewise_construct, forward_as_tuple(name),
|
||||
forward_as_tuple(name, contents, t));
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
shared_ptr<const string> FileContentsCache::get(const char* name) {
|
||||
return this->get(string(name));
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
class FileContentsCache {
|
||||
private:
|
||||
struct File {
|
||||
std::string name;
|
||||
std::shared_ptr<const std::string> contents;
|
||||
uint64_t load_time;
|
||||
|
||||
File() = delete;
|
||||
File(const std::string& name, std::shared_ptr<const std::string> contents,
|
||||
uint64_t load_time);
|
||||
File(const File&) = delete;
|
||||
File(File&&) = delete;
|
||||
File& operator=(const File&) = delete;
|
||||
File& operator=(File&&) = delete;
|
||||
~File() = default;
|
||||
};
|
||||
|
||||
public:
|
||||
FileContentsCache() = default;
|
||||
FileContentsCache(const FileContentsCache&) = delete;
|
||||
FileContentsCache(FileContentsCache&&) = delete;
|
||||
FileContentsCache& operator=(const FileContentsCache&) = delete;
|
||||
FileContentsCache& operator=(FileContentsCache&&) = delete;
|
||||
~FileContentsCache() = default;
|
||||
|
||||
std::shared_ptr<const std::string> get(const std::string& name);
|
||||
std::shared_ptr<const std::string> get(const char* name);
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, File> name_to_file;
|
||||
};
|
||||
+588
@@ -0,0 +1,588 @@
|
||||
#include "Items.hh"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <phosg/Random.hh>
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/* these items all need some kind of special handling that hasn't been implemented yet.
|
||||
|
||||
030B04 = TP Material (?)
|
||||
030C00 = Cell Of MAG 502
|
||||
030C01 = Cell Of MAG 213
|
||||
030C02 = Parts Of RoboChao
|
||||
030C03 = Heart Of Opa Opa
|
||||
030C04 = Heart Of Pian
|
||||
030C05 = Heart Of Chao
|
||||
|
||||
030D00 = Sorcerer's Right Arm
|
||||
030D01 = S-beat's Arms
|
||||
030D02 = P-arm's Arms
|
||||
030D03 = Delsabre's Right Arm
|
||||
030D04 = C-bringer's Right Arm
|
||||
030D05 = Delsabre's Left Arm
|
||||
030D06 = S-red's Arms
|
||||
030D07 = Dragon's Claw
|
||||
030D08 = Hildebear's Head
|
||||
030D09 = Hildeblue's Head
|
||||
030D0A = Parts of Baranz
|
||||
030D0B = Belra's Right Arms
|
||||
030D0C = GIGUE'S ARMS
|
||||
030D0D = S-BERILL'S ARMS
|
||||
030D0E = G-ASSASIN'S ARMS
|
||||
030D0F = BOOMA'S RIGHT ARMS
|
||||
030D10 = GOBOOMA'S RIGHT ARMS
|
||||
030D11 = GIGOBOOMA'S RIGHT ARMS
|
||||
030D12 = GAL WIND
|
||||
030D13 = RAPPY'S WING
|
||||
|
||||
030E00 = BERILL PHOTON
|
||||
030E01 = PARASITIC GENE FLOW
|
||||
030E02 = MAGICSTONE IRITISTA
|
||||
030E03 = BLUE BLACK STONE
|
||||
030E04 = SYNCESTA
|
||||
030E05 = MAGIC WATER
|
||||
030E06 = PARASITIC CELL TYPE D
|
||||
030E07 = MAGIC ROCK HEART KEY
|
||||
030E08 = MAGIC ROCK MOOLA
|
||||
030E09 = STAR AMPLIFIER
|
||||
030E0A = BOOK OF HITOGATA
|
||||
030E0B = HEART OF CHU CHU
|
||||
030E0C = PART OF EGG BLASTER
|
||||
030E0D = HEART OF ANGLE
|
||||
030E0E = HEART OF DEVIL
|
||||
030E0F = KIT OF HAMBERGER
|
||||
030E10 = PANTHER'S SPIRIT
|
||||
030E11 = KIT OF MARK3
|
||||
030E12 = KIT OF MASTER SYSTEM
|
||||
030E13 = KIT OF GENESIS
|
||||
030E14 = KIT OF SEGA SATURN
|
||||
030E15 = KIT OF DREAMCAST
|
||||
030E16 = AMP. RESTA
|
||||
030E17 = AMP. ANTI
|
||||
030E18 = AMP. SHIFTA
|
||||
030E19 = AMP. DEBAND
|
||||
030E1A = AMP.
|
||||
030E1B = AMP.
|
||||
030E1C = AMP.
|
||||
030E1D = AMP.
|
||||
030E1E = AMP.
|
||||
030E1F = AMP.
|
||||
030E20 = AMP.
|
||||
030E21 = AMP.
|
||||
030E22 = AMP.
|
||||
030E23 = AMP.
|
||||
030E24 = AMP.
|
||||
030E25 = AMP.
|
||||
030E26 = HEART OF KAPUKAPU
|
||||
030E27 = PROTON BOOSTER
|
||||
030F00 = ADD SLOT
|
||||
031000 = PHOTON DROP
|
||||
031001 = PHOTON SPHERE
|
||||
031002 = PHOTON CRYSTAL
|
||||
031100 = BOOK OF KATANA 1
|
||||
031101 = BOOK OF KATANA 2
|
||||
031102 = BOOK OF KATANA 3
|
||||
031200 = WEAPONS BRONZE BADGE
|
||||
031201 = WEAPONS SILVER BADGE
|
||||
031202 = WEAPONS GOLD BADGE
|
||||
031203 = WEAPONS CRYSTAL BADGE
|
||||
031204 = WEAPONS STEEL BADGE
|
||||
031205 = WEAPONS ALUMINUM BADGE
|
||||
031206 = WEAPONS LEATHER BADGE
|
||||
031207 = WEAPONS BONE BADGE
|
||||
031208 = LETTER OF APPRECATION
|
||||
031209 = AUTOGRAPH ALBUM
|
||||
03120A = VALENTINE'S CHOCOLATE
|
||||
03120B = NEWYEAR'S CARD
|
||||
03120C = CRISMAS CARD
|
||||
03120D = BIRTHDAY CARD
|
||||
03120E = PROOF OF SONIC TEAM
|
||||
03120F = SPECIAL EVENT TICKET
|
||||
031300 = PRESENT
|
||||
031400 = CHOCOLATE
|
||||
031401 = CANDY
|
||||
031402 = CAKE
|
||||
031403 = SILVER BADGE
|
||||
031404 = GOLD BADGE
|
||||
031405 = CRYSTAL BADGE
|
||||
031406 = IRON BADGE
|
||||
031407 = ALUMINUM BADGE
|
||||
031408 = LEATHER BADGE
|
||||
031409 = BONE BADGE
|
||||
03140A = BONQUET
|
||||
03140B = DECOCTION
|
||||
031500 = CRISMAS PRESENT
|
||||
031501 = EASTER EGG
|
||||
031502 = JACK-O'S-LANTERN
|
||||
031700 = HUNTERS REPORT
|
||||
031701 = HUNTERS REPORT RANK A
|
||||
031702 = HUNTERS REPORT RANK B
|
||||
031703 = HUNTERS REPORT RANK C
|
||||
031704 = HUNTERS REPORT RANK F
|
||||
031705 = HUNTERS REPORT
|
||||
031705 = HUNTERS REPORT
|
||||
031705 = HUNTERS REPORT
|
||||
031705 = HUNTERS REPORT
|
||||
031705 = HUNTERS REPORT
|
||||
031705 = HUNTERS REPORT
|
||||
031705 = HUNTERS REPORT
|
||||
031705 = HUNTERS REPORT
|
||||
031705 = HUNTERS REPORT
|
||||
031802 = Dragon Scale
|
||||
031803 = Heaven Striker Coat
|
||||
031807 = Rappys Beak
|
||||
031802 = Dragon Scale */
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
|
||||
ssize_t equipped_weapon = -1;
|
||||
// ssize_t equipped_armor = -1;
|
||||
// ssize_t equipped_shield = -1;
|
||||
// ssize_t equipped_mag = -1;
|
||||
for (size_t y = 0; y < c->player.inventory.num_items; y++) {
|
||||
if (c->player.inventory.items[y].equip_flags & 0x0008) {
|
||||
if (c->player.inventory.items[y].data.item_data1[0] == 0) {
|
||||
equipped_weapon = y;
|
||||
}
|
||||
// else if ((c->player.inventory.items[y].data.item_data1[0] == 1) &&
|
||||
// (c->player.inventory.items[y].data.item_data1[1] == 1)) {
|
||||
// equipped_armor = y;
|
||||
// } else if ((c->player.inventory.items[y].data.item_data1[0] == 1) &&
|
||||
// (c->player.inventory.items[y].data.item_data1[1] == 2)) {
|
||||
// equipped_shield = y;
|
||||
// } else if (c->player.inventory.items[y].data.item_data1[0] == 2) {
|
||||
// equipped_mag = y;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
bool should_delete_item = true;
|
||||
|
||||
auto& item = c->player.inventory.items[item_index];
|
||||
if (item.data.item_data1w[0] == 0x0203) { // technique disk
|
||||
c->player.disp.technique_levels[item.data.item_data1[4]] = item.data.item_data1[2];
|
||||
|
||||
} else if (item.data.item_data1w[0] == 0x0A03) { // grinder
|
||||
if (equipped_weapon < 0) {
|
||||
throw invalid_argument("grinder used with no weapon equipped");
|
||||
}
|
||||
if (item.data.item_data1[2] > 2) {
|
||||
throw invalid_argument("incorrect grinder value");
|
||||
}
|
||||
c->player.inventory.items[equipped_weapon].data.item_data1[3] += (item.data.item_data1[2] + 1);
|
||||
// TODO: we should check for max grind here
|
||||
|
||||
} else if (item.data.item_data1w[0] == 0x0B03) { // material
|
||||
switch (item.data.item_data1[2]) {
|
||||
case 0: // Power Material
|
||||
c->player.disp.stats.atp += 2;
|
||||
break;
|
||||
case 1: // Mind Material
|
||||
c->player.disp.stats.mst += 2;
|
||||
break;
|
||||
case 2: // Evade Material
|
||||
c->player.disp.stats.evp += 2;
|
||||
break;
|
||||
case 3: // HP Material
|
||||
c->player.inventory.hp_materials_used += 2;
|
||||
break;
|
||||
case 4: // TP Material
|
||||
c->player.inventory.tp_materials_used += 2;
|
||||
break;
|
||||
case 5: // Def Material
|
||||
c->player.disp.stats.dfp += 2;
|
||||
break;
|
||||
case 6: // Luck Material
|
||||
c->player.disp.stats.lck += 2;
|
||||
break;
|
||||
default:
|
||||
throw invalid_argument("unknown material used");
|
||||
}
|
||||
|
||||
} else {
|
||||
// default item action is to unwrap the item if it's a present
|
||||
if ((item.data.item_data1[0] == 2) && (item.data.item_data2[2] & 0x40)) {
|
||||
item.data.item_data2[2] &= 0xBF;
|
||||
should_delete_item = false;
|
||||
} else if ((item.data.item_data1[0] != 2) && (item.data.item_data1[4] & 0x40)) {
|
||||
item.data.item_data1[4] &= 0xBF;
|
||||
should_delete_item = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (should_delete_item) {
|
||||
c->player.remove_item(item.data.item_id, 1, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// reads the non-rare item preferences from the config file.
|
||||
CommonItemCreator::CommonItemCreator(
|
||||
const vector<uint32_t>& enemy_item_categories,
|
||||
const vector<uint32_t>& box_item_categories,
|
||||
const vector<vector<uint8_t>>& unit_types) :
|
||||
enemy_item_categories(enemy_item_categories),
|
||||
box_item_categories(box_item_categories),
|
||||
unit_types(unit_types) {
|
||||
|
||||
// sanity check the values
|
||||
if (this->enemy_item_categories.size() != 8) {
|
||||
throw invalid_argument("enemy item categories is incorrect length");
|
||||
}
|
||||
if (this->box_item_categories.size() != 8) {
|
||||
throw invalid_argument("box item categories is incorrect length");
|
||||
}
|
||||
if (this->unit_types.size() != 4) {
|
||||
throw invalid_argument("unit types is incorrect length");
|
||||
}
|
||||
|
||||
{
|
||||
uint64_t sum = 0;
|
||||
for (uint32_t v : this->enemy_item_categories) {
|
||||
sum += v;
|
||||
}
|
||||
if (sum > 0xFFFFFFFF) {
|
||||
throw invalid_argument("enemy item category sum is too large");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
uint64_t sum = 0;
|
||||
for (uint32_t v : this->box_item_categories) {
|
||||
sum += v;
|
||||
}
|
||||
if (sum > 0xFFFFFFFF) {
|
||||
throw invalid_argument("box item category sum is too large");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int32_t CommonItemCreator::decide_item_type(bool is_box) const {
|
||||
uint32_t determinant = random_object<uint32_t>();
|
||||
|
||||
const auto* v = is_box ? &this->box_item_categories : &this->enemy_item_categories;
|
||||
for (size_t x = 0; x < v->size(); x++) {
|
||||
uint32_t probability = v->at(x);
|
||||
if (probability > determinant) {
|
||||
return x;
|
||||
}
|
||||
determinant -= probability;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
ItemData CommonItemCreator::create_drop_item(bool is_box, uint8_t episode,
|
||||
uint8_t difficulty, uint8_t area, uint8_t) const {
|
||||
// TODO: use the section ID (last argument) to vary drop frequencies appropriately
|
||||
// change the area if it's invalid (data for the bosses are actually in other areas)
|
||||
if (area > 10) {
|
||||
if (episode == 1) {
|
||||
if (area == 11) {
|
||||
area = 3; // dragon
|
||||
} else if (area == 12) {
|
||||
area = 6; // de rol le
|
||||
} else if (area == 13) {
|
||||
area = 8; // vol opt
|
||||
} else if (area == 14) {
|
||||
area = 10; // dark falz
|
||||
} else {
|
||||
area = 1; // unknown area -> forest 1
|
||||
}
|
||||
} else if (episode == 2) {
|
||||
if (area == 12) {
|
||||
area = 9; // gal gryphon
|
||||
} else if (area == 13) {
|
||||
area = 10; // olga flow
|
||||
} else if (area == 14) {
|
||||
area = 3; // barba ray
|
||||
} else if (area == 15) {
|
||||
area = 6; // gol dragon
|
||||
} else {
|
||||
area = 10; // tower
|
||||
}
|
||||
} else if (episode == 3) {
|
||||
area = 1;
|
||||
}
|
||||
}
|
||||
|
||||
ItemData item;
|
||||
memset(&item, 0, sizeof(item));
|
||||
|
||||
// picks a random non-rare item type, then gives it appropriate random stats
|
||||
// modify some of the constants in this section to change the system's
|
||||
// parameters
|
||||
int32_t type = this->decide_item_type(is_box);
|
||||
switch (type) {
|
||||
case 0x00: // material
|
||||
item.item_data1[0] = 0x03;
|
||||
item.item_data1[1] = 0x0B;
|
||||
item.item_data1[2] = random_int(0, 6);
|
||||
break;
|
||||
|
||||
case 0x01: // equipment
|
||||
switch (random_int(0, 3)) {
|
||||
case 0x00: // weapon
|
||||
item.item_data1[1] = random_int(1, 12); // random normal class
|
||||
item.item_data1[2] = difficulty + random_int(0, 2); // special type
|
||||
if ((item.item_data1[1] > 0x09) && (item.item_data1[2] > 0x04)) {
|
||||
item.item_data1[2] = 0x04; // no special classes above 4
|
||||
}
|
||||
item.item_data1[4] = 0x80; // untekked
|
||||
if (item.item_data1[2] < 0x04) {
|
||||
item.item_data1[4] |= random_int(0, 40); // give a special
|
||||
}
|
||||
for (size_t x = 0, y = 0; (x < 5) && (y < 3); x++) { // percentages
|
||||
if (random_int(0, 10) == 1) { // 1/11 chance of getting each type of percentage
|
||||
item.item_data1[6 + (y * 2)] = x + 1;
|
||||
item.item_data1[7 + (y * 2)] = random_int(0, 10) * 5;
|
||||
y++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x01: // armor
|
||||
item.item_data1[0] = 0x01;
|
||||
item.item_data1[1] = 0x01;
|
||||
item.item_data1[2] = (6 * difficulty) + random_int(0, ((area / 2) + 2) - 1); // standard type based on difficulty and area
|
||||
if (item.item_data1[2] > 0x17) {
|
||||
item.item_data1[2] = 0x17; // no standard types above 0x17
|
||||
}
|
||||
if (random_int(0, 10) == 0) { // +/-
|
||||
item.item_data1[4] = random_int(0, 5);
|
||||
item.item_data1[6] = random_int(0, 2);
|
||||
}
|
||||
item.item_data1[5] = random_int(0, 4); // slots
|
||||
break;
|
||||
|
||||
case 0x02: // shield
|
||||
item.item_data1[0] = 0x01;
|
||||
item.item_data1[1] = 0x02;
|
||||
item.item_data1[2] = (5 * difficulty) + random_int(0, ((area / 2) + 2) - 1); // standard type based on difficulty and area
|
||||
if (item.item_data1[2] > 0x14) {
|
||||
item.item_data1[2] = 0x14; // no standard types above 0x14
|
||||
}
|
||||
if (random_int(0, 10) == 0) { // +/-
|
||||
item.item_data1[4] = random_int(0, 5);
|
||||
item.item_data1[6] = random_int(0, 5);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x03: { // unit
|
||||
const auto& type_table = this->unit_types.at(difficulty);
|
||||
uint8_t type = type_table[random_int(0, type_table.size() - 1)];
|
||||
if (type == 0xFF) {
|
||||
throw out_of_range("no item dropped"); // 0xFF -> no item drops
|
||||
}
|
||||
item.item_data1[0] = 0x01;
|
||||
item.item_data1[1] = 0x03;
|
||||
item.item_data1[2] = type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x02: // technique
|
||||
item.item_data1[0] = 0x03;
|
||||
item.item_data1[1] = 0x02;
|
||||
item.item_data1[4] = random_int(0, 18); // tech type
|
||||
if ((item.item_data1[4] != 14) && (item.item_data1[4] != 17)) { // if not ryuker or reverser, give it a level
|
||||
if (item.item_data1[4] == 16) { // if not anti, give it a level between 1 and 30
|
||||
if (area > 3) {
|
||||
item.item_data1[2] = difficulty + random_int(0, ((area - 1) / 2) - 1);
|
||||
} else {
|
||||
item.item_data1[2] = difficulty;
|
||||
}
|
||||
if (item.item_data1[2] > 6) {
|
||||
item.item_data1[2] = 6;
|
||||
}
|
||||
} else {
|
||||
item.item_data1[2] = (5 * difficulty) + random_int(0, ((area * 3) / 2) - 1); // else between 1 and 7
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x03: // scape doll
|
||||
item.item_data1[0] = 0x03;
|
||||
item.item_data1[1] = 0x09;
|
||||
item.item_data1[2] = 0x00;
|
||||
break;
|
||||
|
||||
case 0x04: // grinder
|
||||
item.item_data1[0] = 0x03;
|
||||
item.item_data1[1] = 0x0A;
|
||||
item.item_data1[2] = random_int(0, 2); // mono, di, tri
|
||||
break;
|
||||
|
||||
case 0x05: // consumable
|
||||
item.item_data1[0] = 0x03;
|
||||
item.item_data1[5] = 0x01;
|
||||
switch (random_int(0, 2)) {
|
||||
case 0: // antidote / antiparalysis
|
||||
item.item_data1[1] = 6;
|
||||
item.item_data1[2] = random_int(0, 1);
|
||||
break;
|
||||
|
||||
case 1: // telepipe / trap vision
|
||||
item.item_data1[1] = 7 + random_int(0, 1);
|
||||
break;
|
||||
|
||||
case 2: // sol / moon / star atomizer
|
||||
item.item_data1[1] = 3 + random_int(0, 2);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x06: // consumable
|
||||
item.item_data1[0] = 0x03;
|
||||
item.item_data1[5] = 0x01;
|
||||
item.item_data1[1] = random_int(0, 1); // mate or fluid
|
||||
if (difficulty == 0) {
|
||||
item.item_data1[2] = random_int(0, 1); // only mono and di on normal
|
||||
} else if (difficulty == 3) {
|
||||
item.item_data1[2] = random_int(1, 2); // only di and tri on ultimate
|
||||
} else {
|
||||
item.item_data1[2] = random_int(0, 2); // else, any of the three
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x07: // meseta
|
||||
item.item_data1[0] = 0x04;
|
||||
item.item_data2d = (90 * difficulty) + (random_int(0, 20) * (area * 2)); // meseta amount
|
||||
break;
|
||||
|
||||
default:
|
||||
throw out_of_range("no item created");
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
ItemData CommonItemCreator::create_shop_item(uint8_t difficulty,
|
||||
uint8_t item_type) const {
|
||||
static const uint8_t max_percentages[4] = {20, 35, 45, 50};
|
||||
static const uint8_t max_quantity[4] = { 1, 1, 2, 2};
|
||||
static const uint8_t max_tech_level[4] = { 8, 15, 23, 30};
|
||||
static const uint8_t max_anti_level[4] = { 2, 4, 6, 7};
|
||||
|
||||
ItemData item;
|
||||
memset(&item, 0, sizeof(item));
|
||||
|
||||
item.item_data1[0] = item_type;
|
||||
while (item.item_data1[0] == 2) {
|
||||
item.item_data1[0] = rand() % 3;
|
||||
}
|
||||
switch (item.item_data1[0]) {
|
||||
case 0: { // weapon
|
||||
item.item_data1[1] = (rand() % 12) + 1;
|
||||
if (item.item_data1[1] > 9) {
|
||||
item.item_data1[2] = difficulty;
|
||||
} else {
|
||||
item.item_data1[2] = (rand() & 1) + difficulty;
|
||||
}
|
||||
|
||||
item.item_data1[3] = rand() % 11;
|
||||
item.item_data1[4] = rand() % 11;
|
||||
|
||||
size_t num_percentages = 0;
|
||||
for (size_t x = 0; (x < 5) && (num_percentages < 3); x++) {
|
||||
if ((rand() % 4) == 1) {
|
||||
item.item_data1[(num_percentages * 2) + 6] = x;
|
||||
item.item_data1[(num_percentages * 2) + 7] = rand() % (max_percentages[difficulty] + 1);
|
||||
num_percentages++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 1: // armor
|
||||
item.item_data1[1] = 0;
|
||||
while (item.item_data1[1] == 0) {
|
||||
item.item_data1[1] = rand() & 3;
|
||||
}
|
||||
switch (item.item_data1[1]) {
|
||||
case 1:
|
||||
item.item_data1[2] = (rand() % 6) + (difficulty * 6);
|
||||
item.item_data1[5] = rand() % 5;
|
||||
break;
|
||||
case 2:
|
||||
item.item_data2[2] = (rand() % 6) + (difficulty * 5);
|
||||
*reinterpret_cast<short*>(&item.item_data1[6]) = (rand() % 9) - 4;
|
||||
*reinterpret_cast<short*>(&item.item_data1[9]) = (rand() % 9) - 4;
|
||||
break;
|
||||
case 3:
|
||||
item.item_data2[2] = rand() % 0x3B;
|
||||
*reinterpret_cast<short*>(&item.item_data1[7]) = (rand() % 5) - 4;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 3: // tool
|
||||
item.item_data1[1] = rand() % 12;
|
||||
switch (item.item_data1[1]) {
|
||||
case 0:
|
||||
case 1:
|
||||
if (difficulty == 0) {
|
||||
item.item_data1[2] = 0;
|
||||
} else if (difficulty == 1) {
|
||||
item.item_data1[2] = rand() % 2;
|
||||
} else if (difficulty == 2) {
|
||||
item.item_data1[2] = (rand() % 2) + 1;
|
||||
} else if (difficulty == 3) {
|
||||
item.item_data1[2] = 2;
|
||||
}
|
||||
break;
|
||||
|
||||
case 6:
|
||||
item.item_data1[2] = rand() % 2;
|
||||
break;
|
||||
|
||||
case 10:
|
||||
item.item_data1[2] = rand() % 3;
|
||||
break;
|
||||
|
||||
case 11:
|
||||
item.item_data1[2] = rand() % 7;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (item.item_data1[1]) {
|
||||
case 2:
|
||||
item.item_data1[4] = rand() % 19;
|
||||
switch (item.item_data1[4]) {
|
||||
case 14:
|
||||
case 17:
|
||||
item.item_data1[2] = 0; // reverser & ryuker always level 1
|
||||
break;
|
||||
case 16:
|
||||
item.item_data1[2] = rand() % max_anti_level[difficulty];
|
||||
break;
|
||||
default:
|
||||
item.item_data1[2] = rand() % max_tech_level[difficulty];
|
||||
}
|
||||
break;
|
||||
case 0:
|
||||
case 1:
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
case 6:
|
||||
case 7:
|
||||
case 8:
|
||||
case 16:
|
||||
item.item_data1[5] = rand() % (max_quantity[difficulty] + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Lobby.hh"
|
||||
#include "Client.hh"
|
||||
|
||||
void player_use_item(std::shared_ptr<Client> c, size_t item_index);
|
||||
|
||||
struct CommonItemCreator {
|
||||
std::vector<uint32_t> enemy_item_categories;
|
||||
std::vector<uint32_t> box_item_categories;
|
||||
std::vector<std::vector<uint8_t>> unit_types;
|
||||
|
||||
CommonItemCreator(const std::vector<uint32_t>& enemy_item_categories,
|
||||
const std::vector<uint32_t>& box_item_categories,
|
||||
const std::vector<std::vector<uint8_t>>& unit_types);
|
||||
|
||||
int32_t decide_item_type(bool is_box) const;
|
||||
ItemData create_drop_item(bool is_box, uint8_t episode, uint8_t difficulty,
|
||||
uint8_t area, uint8_t section_id) const;
|
||||
ItemData create_shop_item(uint8_t difficulty, uint8_t shop_type) const;
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "LevelTable.hh"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <phosg/Filesystem.hh>
|
||||
|
||||
#include "Compression.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
LevelTable::LevelTable(const char* filename, bool compressed) {
|
||||
|
||||
string data = load_file(filename);
|
||||
if (compressed) {
|
||||
data = prs_decompress(data);
|
||||
}
|
||||
|
||||
if (data.size() < sizeof(*this)) {
|
||||
throw invalid_argument("level table size is incorrect");
|
||||
}
|
||||
|
||||
memcpy(this, data.data(), sizeof(*this));
|
||||
}
|
||||
|
||||
const PlayerStats& LevelTable::base_stats_for_class(uint8_t char_class) const {
|
||||
if (char_class >= 12) {
|
||||
throw out_of_range("invalid character class");
|
||||
}
|
||||
return this->base_stats[char_class];
|
||||
}
|
||||
|
||||
const LevelStats& LevelTable::stats_for_level(uint8_t char_class,
|
||||
uint8_t level) const {
|
||||
if (char_class >= 12) {
|
||||
throw invalid_argument("invalid character class");
|
||||
}
|
||||
if (level >= 200) {
|
||||
throw invalid_argument("invalid character level");
|
||||
}
|
||||
return this->levels[char_class][level];
|
||||
}
|
||||
|
||||
// Levels up a character by adding the level-up bonuses to the player's stats.
|
||||
void LevelStats::apply(PlayerStats& ps) const {
|
||||
ps.ata += this->ata;
|
||||
ps.atp += this->atp;
|
||||
ps.dfp += this->dfp;
|
||||
ps.evp += this->evp;
|
||||
ps.hp += this->hp;
|
||||
ps.mst += this->mst;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "Player.hh"
|
||||
|
||||
// information on a single level for a single class
|
||||
struct LevelStats {
|
||||
uint8_t atp; // atp to add on level up
|
||||
uint8_t mst; // mst to add on level up
|
||||
uint8_t evp; // evp to add on level up
|
||||
uint8_t hp; // hp to add on level up
|
||||
uint8_t dfp; // dfp to add on level up
|
||||
uint8_t ata; // ata to add on level up
|
||||
uint8_t unknown[2];
|
||||
uint32_t experience; // EXP value of this level
|
||||
|
||||
void apply(PlayerStats& ps) const;
|
||||
};
|
||||
|
||||
// level table format (PlyLevelTbl.prs)
|
||||
struct LevelTable {
|
||||
PlayerStats base_stats[12];
|
||||
uint32_t unknown[12];
|
||||
LevelStats levels[12][200];
|
||||
|
||||
LevelTable(const char* filename, bool compressed);
|
||||
|
||||
const PlayerStats& base_stats_for_class(uint8_t char_class) const;
|
||||
const LevelStats& stats_for_level(uint8_t char_class, uint8_t level) const;
|
||||
};
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "License.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
string License::str() const {
|
||||
string ret = string_printf("License(serial_number=%" PRIu32, this->serial_number);
|
||||
if (this->username[0]) {
|
||||
ret += ", username=";
|
||||
ret += this->username;
|
||||
}
|
||||
if (this->bb_password[0]) {
|
||||
ret += ", bb-password=";
|
||||
ret += this->bb_password;
|
||||
}
|
||||
if (this->access_key[0]) {
|
||||
ret += ", access-key=";
|
||||
ret += this->access_key;
|
||||
}
|
||||
if (this->gc_password[0]) {
|
||||
ret += ", gc-password=";
|
||||
ret += this->gc_password;
|
||||
}
|
||||
ret += string_printf(", privileges=%" PRIu32, this->privileges);
|
||||
if (this->ban_end_time) {
|
||||
ret += string_printf(", banned-until=%" PRIu64, this->ban_end_time);
|
||||
}
|
||||
return ret + ")";
|
||||
}
|
||||
|
||||
|
||||
|
||||
LicenseManager::LicenseManager(const string& filename) : filename(filename) {
|
||||
try {
|
||||
auto licenses = load_vector_file<License>(this->filename);
|
||||
for (const auto& read_license : licenses) {
|
||||
shared_ptr<License> license(new License(read_license));
|
||||
uint32_t serial_number = license->serial_number;
|
||||
this->bb_username_to_license.emplace(license->username, license);
|
||||
this->serial_number_to_license.emplace(serial_number, license);
|
||||
}
|
||||
|
||||
} catch (const cannot_open_file&) {
|
||||
log(WARNING, "%s does not exist; no licenses are registered", this->filename.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void LicenseManager::save() const {
|
||||
auto f = fopen_unique(this->filename, "wb");
|
||||
for (const auto& it : this->serial_number_to_license) {
|
||||
fwritex(f.get(), it.second.get(), sizeof(License));
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<const License> LicenseManager::verify_pc(uint32_t serial_number,
|
||||
const char* access_key, const char* password) const {
|
||||
auto& license = this->serial_number_to_license.at(serial_number);
|
||||
if (strncmp(license->access_key, access_key, 8)) {
|
||||
throw invalid_argument("incorrect access key");
|
||||
}
|
||||
if (password && (strcmp(license->gc_password, password))) {
|
||||
throw invalid_argument("incorrect password");
|
||||
}
|
||||
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
return license;
|
||||
}
|
||||
|
||||
shared_ptr<const License> LicenseManager::verify_gc(uint32_t serial_number,
|
||||
const char* access_key, const char* password) const {
|
||||
auto& license = this->serial_number_to_license.at(serial_number);
|
||||
if (strncmp(license->access_key, access_key, 12)) {
|
||||
throw invalid_argument("incorrect access key");
|
||||
}
|
||||
if (password && (strcmp(license->gc_password, password))) {
|
||||
throw invalid_argument("incorrect password");
|
||||
}
|
||||
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
return license;
|
||||
}
|
||||
|
||||
shared_ptr<const License> LicenseManager::verify_bb(const char* username,
|
||||
const char* password) const {
|
||||
auto& license = this->bb_username_to_license.at(username);
|
||||
if (password && strcmp(license->bb_password, password)) {
|
||||
throw invalid_argument("incorrect password");
|
||||
}
|
||||
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
return license;
|
||||
}
|
||||
|
||||
size_t LicenseManager::count() const {
|
||||
return this->serial_number_to_license.size();
|
||||
}
|
||||
|
||||
void LicenseManager::ban_until(uint32_t serial_number, uint64_t end_time) {
|
||||
this->serial_number_to_license.at(serial_number)->ban_end_time = end_time;
|
||||
this->save();
|
||||
}
|
||||
|
||||
void LicenseManager::add(shared_ptr<License> l) {
|
||||
uint32_t serial_number = l->serial_number;
|
||||
this->serial_number_to_license.emplace(serial_number, l);
|
||||
if (l->username[0]) {
|
||||
this->bb_username_to_license.emplace(l->username, l);
|
||||
}
|
||||
|
||||
this->save();
|
||||
}
|
||||
|
||||
void LicenseManager::remove(uint32_t serial_number) {
|
||||
auto l = this->serial_number_to_license.at(serial_number);
|
||||
this->serial_number_to_license.erase(l->serial_number);
|
||||
if (l->username[0]) {
|
||||
this->bb_username_to_license.erase(l->username);
|
||||
}
|
||||
|
||||
this->save();
|
||||
}
|
||||
|
||||
vector<License> LicenseManager::snapshot() const {
|
||||
vector<License> ret;
|
||||
for (auto it : this->serial_number_to_license) {
|
||||
ret.emplace_back(*it.second);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
shared_ptr<const License> LicenseManager::create_license_pc(
|
||||
uint32_t serial_number,const char* access_key, const char* password) {
|
||||
shared_ptr<License> l(new License());
|
||||
memset(l.get(), 0, sizeof(License));
|
||||
l->serial_number = serial_number;
|
||||
strncpy(l->access_key, access_key, 8);
|
||||
if (password) {
|
||||
strncpy(l->gc_password, password, 8);
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
shared_ptr<const License> LicenseManager::create_license_gc(
|
||||
uint32_t serial_number, const char* access_key, const char* password) {
|
||||
shared_ptr<License> l(new License());
|
||||
memset(l.get(), 0, sizeof(License));
|
||||
l->serial_number = serial_number;
|
||||
strncpy(l->access_key, access_key, 12);
|
||||
if (password) {
|
||||
strncpy(l->gc_password, password, 8);
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
shared_ptr<const License> LicenseManager::create_license_bb(
|
||||
uint32_t serial_number, const char* username, const char* password) {
|
||||
shared_ptr<License> l(new License());
|
||||
memset(l.get(), 0, sizeof(License));
|
||||
l->serial_number = serial_number;
|
||||
strncpy(l->username, username, 19);
|
||||
strncpy(l->bb_password, password, 19);
|
||||
return l;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <phosg/Concurrency.hh>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
|
||||
enum Privilege {
|
||||
KickUser = 0x00000001,
|
||||
BanUser = 0x00000002,
|
||||
SilenceUser = 0x00000004,
|
||||
ChangeLobbyInfo = 0x00000008,
|
||||
ChangeEvent = 0x00000010,
|
||||
Announce = 0x00000020,
|
||||
FreeJoinGames = 0x00000040,
|
||||
UnlockGames = 0x00000080,
|
||||
|
||||
Moderator = 0x00000007,
|
||||
Administrator = 0x0000003F,
|
||||
Root = 0xFFFFFFFF,
|
||||
};
|
||||
|
||||
enum LicenseVerifyAction {
|
||||
BB = 0x00,
|
||||
GC = 0x01,
|
||||
PC = 0x02,
|
||||
SerialNumber = 0x03,
|
||||
};
|
||||
|
||||
struct License {
|
||||
char username[20]; // BB username (max. 16 chars; should technically be Unicode)
|
||||
char bb_password[20]; // BB password (max. 16 chars)
|
||||
uint32_t serial_number; // PC/GC serial number. MUST BE PRESENT FOR BB LICENSES TOO; this is also the player's guild card number.
|
||||
char access_key[16]; // PC/GC access key. (to log in using PC on a GC license, just enter the first 8 characters of the GC access key)
|
||||
char gc_password[12]; // GC password
|
||||
uint32_t privileges; // privilege level
|
||||
uint64_t ban_end_time; // end time of ban (zero = not banned)
|
||||
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
class LicenseManager {
|
||||
public:
|
||||
LicenseManager(const std::string& filename);
|
||||
~LicenseManager() = default;
|
||||
|
||||
std::shared_ptr<const License> verify_pc(uint32_t serial_number,
|
||||
const char* access_key, const char* password) const;
|
||||
std::shared_ptr<const License> verify_gc(uint32_t serial_number,
|
||||
const char* access_key, const char* password) const;
|
||||
std::shared_ptr<const License> verify_bb(const char* username,
|
||||
const char* password) const;
|
||||
void ban_until(uint32_t serial_number, uint64_t seconds);
|
||||
|
||||
size_t count() const;
|
||||
|
||||
void add(std::shared_ptr<License> l);
|
||||
void remove(uint32_t serial_number);
|
||||
std::vector<License> snapshot() const;
|
||||
|
||||
static std::shared_ptr<const License> create_license_pc(
|
||||
uint32_t serial_number, const char* access_key, const char* password);
|
||||
static std::shared_ptr<const License> create_license_gc(
|
||||
uint32_t serial_number, const char* access_key, const char* password);
|
||||
static std::shared_ptr<const License> create_license_bb(
|
||||
uint32_t serial_number, const char* username, const char* password);
|
||||
|
||||
protected:
|
||||
void save() const;
|
||||
|
||||
std::string filename;
|
||||
std::unordered_map<std::string, std::shared_ptr<License>> bb_username_to_license;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<License>> serial_number_to_license;
|
||||
};
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
#include "Lobby.hh"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <phosg/Random.hh>
|
||||
|
||||
#include "SendCommands.hh"
|
||||
#include "Text.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
Lobby::Lobby() : lobby_id(0), min_level(0), max_level(0xFFFFFFFF),
|
||||
next_game_item_id(0), version(GameVersion::GC), section_id(0), episode(1),
|
||||
difficulty(0), mode(0), rare_seed(random_object<uint32_t>()), event(0),
|
||||
block(0), type(0), leader_id(0), max_clients(12), flags(0),
|
||||
loading_quest_id(0) {
|
||||
|
||||
for (size_t x = 0; x < 12; x++) {
|
||||
this->next_item_id[x] = 0;
|
||||
}
|
||||
memset(&this->next_drop_item, 0, sizeof(this->next_drop_item));
|
||||
memset(this->variations, 0, 0x20 * sizeof(this->variations[0]));
|
||||
memset(this->password, 0, 36 * sizeof(this->password[0]));
|
||||
memset(this->name, 0, 36 * sizeof(this->name[0]));
|
||||
}
|
||||
|
||||
bool Lobby::is_game() const {
|
||||
return this->flags & LobbyFlag::IsGame;
|
||||
}
|
||||
|
||||
void Lobby::reassign_leader_on_client_departure(size_t leaving_client_index) {
|
||||
for (size_t x = 0; x < this->max_clients; x++) {
|
||||
if (x == leaving_client_index) {
|
||||
continue;
|
||||
}
|
||||
if (this->clients[x].get()) {
|
||||
this->leader_id = x;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->leader_id = 0;
|
||||
}
|
||||
|
||||
bool Lobby::any_client_loading() const {
|
||||
for (size_t x = 0; x < this->max_clients; x++) {
|
||||
if (!this->clients[x].get()) {
|
||||
continue;
|
||||
}
|
||||
if (this->clients[x]->flags & ClientFlag::Loading) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t Lobby::count_clients() const {
|
||||
size_t ret = 0;
|
||||
for (size_t x = 0; x < this->max_clients; x++) {
|
||||
if (this->clients[x].get()) {
|
||||
ret++;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Lobby::add_client(shared_ptr<Client> c) {
|
||||
ssize_t index;
|
||||
for (index = 0; index < max_clients; index++) {
|
||||
if (!this->clients[index].get()) {
|
||||
this->clients[index] = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index >= max_clients) {
|
||||
throw out_of_range("no space left in lobby");
|
||||
}
|
||||
c->lobby_client_id = index;
|
||||
c->lobby_id = this->lobby_id;
|
||||
|
||||
// if there's no one else in the lobby, set the leader id as well
|
||||
if (index == 0) {
|
||||
for (index = 1; index < max_clients; index++) {
|
||||
if (this->clients[index].get()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index >= max_clients) {
|
||||
this->leader_id = c->lobby_client_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Lobby::remove_client(shared_ptr<Client> c) {
|
||||
if (this->clients[c->lobby_client_id] != c) {
|
||||
auto other_c = this->clients[c->lobby_client_id].get();
|
||||
throw logic_error(string_printf(
|
||||
"client\'s lobby client id (%hhu) does not match client list (%hhu)",
|
||||
c->lobby_client_id, other_c ? other_c->lobby_client_id : 0xFF));
|
||||
}
|
||||
|
||||
this->clients[c->lobby_client_id] = NULL;
|
||||
|
||||
// unassign the client's lobby if it matches the current lobby's id (it may
|
||||
// not match if the client was already added to another lobby - this can
|
||||
// happen during the lobby change procedure)
|
||||
if (c->lobby_id == this->lobby_id) {
|
||||
c->lobby_id = 0;
|
||||
}
|
||||
|
||||
this->reassign_leader_on_client_departure(c->lobby_client_id);
|
||||
}
|
||||
|
||||
void Lobby::move_client_to_lobby(shared_ptr<Lobby> dest_lobby,
|
||||
shared_ptr<Client> c) {
|
||||
if (dest_lobby.get() == this) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dest_lobby->count_clients() >= dest_lobby->max_clients) {
|
||||
throw out_of_range("no space left in lobby");
|
||||
}
|
||||
|
||||
this->remove_client(c);
|
||||
dest_lobby->add_client(c);
|
||||
}
|
||||
|
||||
|
||||
|
||||
shared_ptr<Client> Lobby::find_client(const char16_t* identifier,
|
||||
uint64_t serial_number) {
|
||||
for (size_t x = 0; x < this->max_clients; x++) {
|
||||
if (!this->clients[x]) {
|
||||
continue;
|
||||
}
|
||||
if (serial_number && this->clients[x]->license &&
|
||||
(this->clients[x]->license->serial_number == serial_number)) {
|
||||
return this->clients[x];
|
||||
}
|
||||
if (identifier && !char16cmp(this->clients[x]->player.disp.name, identifier, 0x10)) {
|
||||
return this->clients[x];
|
||||
}
|
||||
}
|
||||
|
||||
throw out_of_range("client not found");
|
||||
}
|
||||
|
||||
|
||||
|
||||
uint8_t Lobby::game_event_for_lobby_event(uint8_t lobby_event) {
|
||||
if (lobby_event > 7) {
|
||||
return 0;
|
||||
}
|
||||
if (lobby_event == 7) {
|
||||
return 2;
|
||||
}
|
||||
if (lobby_event == 2) {
|
||||
return 0;
|
||||
}
|
||||
return lobby_event;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Lobby::add_item(const PlayerInventoryItem& item) {
|
||||
this->item_id_to_floor_item.emplace(item.data.item_id, item);
|
||||
}
|
||||
|
||||
void Lobby::remove_item(uint32_t item_id, PlayerInventoryItem* item) {
|
||||
auto item_it = this->item_id_to_floor_item.find(item_id);
|
||||
if (item_it == this->item_id_to_floor_item.end()) {
|
||||
throw out_of_range("item not present");
|
||||
}
|
||||
*item = move(item_it->second);
|
||||
this->item_id_to_floor_item.erase(item_it);
|
||||
}
|
||||
|
||||
uint32_t Lobby::generate_item_id(uint8_t client_id) {
|
||||
if (client_id < this->max_clients) {
|
||||
return this->next_item_id[client_id]++;
|
||||
}
|
||||
return this->next_game_item_id++;
|
||||
}
|
||||
|
||||
void Lobby::assign_item_ids_for_player(uint32_t client_id, PlayerInventory& inv) {
|
||||
for (size_t x = 0; x < inv.num_items; x++) {
|
||||
inv.items[x].data.item_id = this->generate_item_id(client_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <vector>
|
||||
#include <phosg/Concurrency.hh>
|
||||
|
||||
#include "Client.hh"
|
||||
#include "Player.hh"
|
||||
#include "Map.hh"
|
||||
#include "RareItemSet.hh"
|
||||
|
||||
enum LobbyFlag {
|
||||
// games and lobbies are actually the same thing - games have negative IDs,
|
||||
// lobbies have IDs >= 0
|
||||
IsGame = 0x01,
|
||||
CheatsEnabled = 0x02, // game only
|
||||
Public = 0x04, // lobby only
|
||||
Episode3 = 0x08, // lobby only
|
||||
QuestInProgress = 0x10, // game only
|
||||
JoinableQuestInProgress = 0x20, // game only
|
||||
Default = 0x40, // lobby only; false for private lobbies
|
||||
Persistent = 0x80, // if not set, lobby is deleted when empty
|
||||
};
|
||||
|
||||
struct Lobby {
|
||||
uint32_t lobby_id;
|
||||
|
||||
uint32_t min_level;
|
||||
uint32_t max_level;
|
||||
|
||||
// item info
|
||||
std::vector<PSOEnemy> enemies;
|
||||
std::shared_ptr<const RareItemSet> rare_item_set;
|
||||
uint32_t next_item_id[12];
|
||||
uint32_t next_game_item_id;
|
||||
PlayerInventoryItem next_drop_item;
|
||||
std::unordered_map<uint32_t, PlayerInventoryItem> item_id_to_floor_item;
|
||||
uint32_t variations[0x20];
|
||||
|
||||
// game config
|
||||
GameVersion version;
|
||||
uint8_t section_id;
|
||||
uint8_t episode;
|
||||
uint8_t difficulty;
|
||||
uint8_t mode;
|
||||
char16_t password[0x24];
|
||||
char16_t name[0x24];
|
||||
uint32_t rare_seed;
|
||||
|
||||
//EP3_GAME_CONFIG* ep3; // only present if this is an Episode 3 game
|
||||
|
||||
// lobby stuff
|
||||
uint8_t event;
|
||||
uint8_t block;
|
||||
uint8_t type; // number to give to PSO for the lobby number
|
||||
uint8_t leader_id;
|
||||
uint8_t max_clients;
|
||||
uint32_t flags;
|
||||
uint32_t loading_quest_id; // for use with joinable quests
|
||||
std::shared_ptr<Client> clients[12];
|
||||
|
||||
Lobby();
|
||||
|
||||
bool is_game() const;
|
||||
|
||||
void reassign_leader_on_client_departure(size_t leaving_client_id);
|
||||
size_t count_clients() const;
|
||||
bool any_client_loading() const;
|
||||
|
||||
void add_client(std::shared_ptr<Client> c);
|
||||
void remove_client(std::shared_ptr<Client> c);
|
||||
|
||||
void move_client_to_lobby(std::shared_ptr<Lobby> dest_lobby,
|
||||
std::shared_ptr<Client> c);
|
||||
|
||||
std::shared_ptr<Client> find_client(const char16_t* identifier = NULL,
|
||||
uint64_t serial_number = 0);
|
||||
|
||||
void add_item(const PlayerInventoryItem& item);
|
||||
void remove_item(uint32_t item_id, PlayerInventoryItem* item);
|
||||
size_t find_item(uint32_t item_id);
|
||||
uint32_t generate_item_id(uint8_t client_id);
|
||||
|
||||
void assign_item_ids_for_player(uint32_t client_id, PlayerInventory& inv);
|
||||
|
||||
static uint8_t game_event_for_lobby_event(uint8_t lobby_event);
|
||||
};
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
#include <signal.h>
|
||||
#include <pwd.h>
|
||||
#include <event2/event.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unordered_map>
|
||||
#include <phosg/JSON.hh>
|
||||
#include <phosg/Network.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <set>
|
||||
|
||||
#include "NetworkAddresses.hh"
|
||||
#include "SendCommands.hh"
|
||||
#include "DNSServer.hh"
|
||||
#include "ProxyServer.hh"
|
||||
#include "ServerState.hh"
|
||||
#include "Server.hh"
|
||||
#include "FileContentsCache.hh"
|
||||
#include "Text.hh"
|
||||
#include "ServerShell.hh"
|
||||
#include "ProxyShell.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
FileContentsCache file_cache;
|
||||
|
||||
|
||||
|
||||
static const unordered_map<string, PortConfiguration> default_port_to_behavior({
|
||||
{"gc-jp10", {9000, GameVersion::GC, ServerBehavior::LoginServer}},
|
||||
{"gc-jp11", {9001, GameVersion::GC, ServerBehavior::LoginServer}},
|
||||
{"gc-jp3", {9003, GameVersion::GC, ServerBehavior::LoginServer}},
|
||||
{"gc-us10", {9100, GameVersion::PC, ServerBehavior::SplitReconnect}},
|
||||
{"gc-us3", {9103, GameVersion::GC, ServerBehavior::LoginServer}},
|
||||
{"gc-eu10", {9200, GameVersion::GC, ServerBehavior::LoginServer}},
|
||||
{"gc-eu11", {9201, GameVersion::GC, ServerBehavior::LoginServer}},
|
||||
{"gc-eu3", {9203, GameVersion::GC, ServerBehavior::LoginServer}},
|
||||
{"pc-login", {9300, GameVersion::PC, ServerBehavior::LoginServer}},
|
||||
{"pc-patch", {10000, GameVersion::Patch, ServerBehavior::PatchServer}},
|
||||
{"bb-patch", {11000, GameVersion::Patch, ServerBehavior::PatchServer}},
|
||||
{"bb-data", {12000, GameVersion::BB, ServerBehavior::DataServerBB}},
|
||||
|
||||
// these aren't hardcoded in any games; user can override them
|
||||
{"bb-data1", {12004, GameVersion::BB, ServerBehavior::DataServerBB}},
|
||||
{"bb-data2", {12005, GameVersion::BB, ServerBehavior::DataServerBB}},
|
||||
{"bb-login", {12008, GameVersion::BB, ServerBehavior::LoginServer}},
|
||||
{"pc-lobby", {9420, GameVersion::PC, ServerBehavior::LobbyServer}},
|
||||
{"gc-lobby", {9421, GameVersion::GC, ServerBehavior::LobbyServer}},
|
||||
{"bb-lobby", {9422, GameVersion::BB, ServerBehavior::LobbyServer}},
|
||||
});
|
||||
|
||||
|
||||
|
||||
template <typename T>
|
||||
vector<T> parse_int_vector(shared_ptr<const JSONObject> o) {
|
||||
vector<T> ret;
|
||||
for (const auto& x : o->as_list()) {
|
||||
ret.emplace_back(x->as_int());
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void populate_state_from_config(shared_ptr<ServerState> s,
|
||||
shared_ptr<JSONObject> config_json) {
|
||||
const auto& d = config_json->as_dict();
|
||||
|
||||
s->name = decode_sjis(d.at("ServerName")->as_string());
|
||||
|
||||
try {
|
||||
s->username = d.at("User")->as_string();
|
||||
if (s->username == "$SUDO_USER") {
|
||||
const char* user_from_env = getenv("SUDO_USER");
|
||||
if (!user_from_env) {
|
||||
throw runtime_error("configuration specifies $SUDO_USER, but variable is not defined");
|
||||
}
|
||||
s->username = user_from_env;
|
||||
}
|
||||
} catch (const out_of_range&) { }
|
||||
|
||||
// TODO: make this configurable
|
||||
s->port_configuration = default_port_to_behavior;
|
||||
|
||||
auto enemy_categories = parse_int_vector<uint32_t>(d.at("CommonItemDropRates-Enemy"));
|
||||
auto box_categories = parse_int_vector<uint32_t>(d.at("CommonItemDropRates-Box"));
|
||||
vector<vector<uint8_t>> unit_types;
|
||||
for (const auto& item : d.at("CommonUnitTypes")->as_list()) {
|
||||
unit_types.emplace_back(parse_int_vector<uint8_t>(item));
|
||||
}
|
||||
s->common_item_creator.reset(new CommonItemCreator(enemy_categories,
|
||||
box_categories, unit_types));
|
||||
|
||||
shared_ptr<vector<MenuItem>> information_menu(new vector<MenuItem>());
|
||||
shared_ptr<vector<u16string>> information_contents(new vector<u16string>());
|
||||
|
||||
information_menu->emplace_back(INFORMATION_MENU_GO_BACK, u"Go back",
|
||||
u"Return to the\nmain menu", 0);
|
||||
|
||||
uint32_t item_id = 0;
|
||||
for (const auto& item : d.at("InformationMenuContents")->as_list()) {
|
||||
auto& v = item->as_list();
|
||||
information_menu->emplace_back(item_id, decode_sjis(v.at(0)->as_string()),
|
||||
decode_sjis(v.at(1)->as_string()), MenuItemFlag::RequiresMessageBoxes);
|
||||
information_contents->emplace_back(decode_sjis(v.at(2)->as_string()));
|
||||
item_id++;
|
||||
}
|
||||
s->information_menu = information_menu;
|
||||
s->information_contents = information_contents;
|
||||
|
||||
try {
|
||||
s->welcome_message = decode_sjis(d.at("WelcomeMessage")->as_string());
|
||||
} catch (const out_of_range&) { }
|
||||
|
||||
auto local_address_str = d.at("LocalAddress")->as_string();
|
||||
try {
|
||||
s->local_address = s->all_addresses.at(local_address_str);
|
||||
string addr_str = string_for_address(s->local_address);
|
||||
log(INFO, "added local address: %s (%s)", addr_str.c_str(),
|
||||
local_address_str.c_str());
|
||||
} catch (const out_of_range&) {
|
||||
s->local_address = address_for_string(local_address_str.c_str());
|
||||
log(INFO, "added local address: %s", local_address_str.c_str());
|
||||
}
|
||||
s->all_addresses.emplace("<local>", s->local_address);
|
||||
|
||||
auto external_address_str = d.at("ExternalAddress")->as_string();
|
||||
try {
|
||||
s->external_address = s->all_addresses.at(external_address_str);
|
||||
string addr_str = string_for_address(s->external_address);
|
||||
log(INFO, "added external address: %s (%s)", addr_str.c_str(),
|
||||
external_address_str.c_str());
|
||||
} catch (const out_of_range&) {
|
||||
s->external_address = address_for_string(external_address_str.c_str());
|
||||
log(INFO, "added external address: %s", external_address_str.c_str());
|
||||
}
|
||||
s->all_addresses.emplace("<external>", s->external_address);
|
||||
|
||||
try {
|
||||
s->run_dns_server = d.at("RunDNSServer")->as_bool();
|
||||
} catch (const out_of_range&) {
|
||||
s->run_dns_server = true;
|
||||
}
|
||||
|
||||
try {
|
||||
s->allow_unregistered_users = d.at("AllowUnregisteredUsers")->as_bool();
|
||||
} catch (const out_of_range&) {
|
||||
s->allow_unregistered_users = true;
|
||||
}
|
||||
|
||||
{
|
||||
string key_file_name = d.at("BlueBurstKeyFile")->as_string();
|
||||
string key_file_contents = load_file("system/blueburst/keys/" + key_file_name + ".nsk");
|
||||
if (key_file_contents.size() != sizeof(PSOBBEncryption::KeyFile)) {
|
||||
log(WARNING, "key file is the wrong size (%zu bytes; should be %zu bytes)",
|
||||
key_file_contents.size(), sizeof(PSOBBEncryption::KeyFile));
|
||||
log(WARNING, "ignoring key file; Blue Burst clients will not be able to connect");
|
||||
} else {
|
||||
memcpy(&s->default_key_file, key_file_contents.data(), sizeof(PSOBBEncryption::KeyFile));
|
||||
log(INFO, "loaded key file: %s", key_file_name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
bool run_shell = d.at("RunInteractiveShell")->as_bool();
|
||||
s->run_shell_behavior = run_shell ?
|
||||
ServerState::RunShellBehavior::Always :
|
||||
ServerState::RunShellBehavior::Never;
|
||||
} catch (const out_of_range&) { }
|
||||
}
|
||||
|
||||
|
||||
|
||||
void drop_privileges(const string& username) {
|
||||
if ((getuid() != 0) || (getgid() != 0)) {
|
||||
throw runtime_error(string_printf(
|
||||
"newserv was not started as root; can\'t switch to user %s",
|
||||
username.c_str()));
|
||||
}
|
||||
|
||||
struct passwd* pw = getpwnam(username.c_str());
|
||||
if (!pw) {
|
||||
string error = string_for_error(errno);
|
||||
throw runtime_error(string_printf("user %s not found (%s)",
|
||||
username.c_str(), error.c_str()));
|
||||
}
|
||||
|
||||
if (setgid(pw->pw_gid) != 0) {
|
||||
string error = string_for_error(errno);
|
||||
throw runtime_error(string_printf("can\'t switch to group %d (%s)",
|
||||
pw->pw_gid, error.c_str()));
|
||||
}
|
||||
if (setuid(pw->pw_uid) != 0) {
|
||||
string error = string_for_error(errno);
|
||||
throw runtime_error(string_printf("can\'t switch to user %d (%s)",
|
||||
pw->pw_uid, error.c_str()));
|
||||
}
|
||||
log(INFO, "switched to user %s (%d:%d)", username.c_str(), pw->pw_uid,
|
||||
pw->pw_gid);
|
||||
}
|
||||
|
||||
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
log(INFO, "fuzziqer software newserv");
|
||||
|
||||
string proxy_hostname;
|
||||
int proxy_port = 0;
|
||||
GameVersion proxy_version = GameVersion::GC;
|
||||
for (int x = 1; x < argc; x++) {
|
||||
if (!strncmp(argv[x], "--proxy-destination=", 20)) {
|
||||
auto netloc = parse_netloc(&argv[x][20], 9100);
|
||||
proxy_hostname = netloc.first;
|
||||
proxy_port = netloc.second;
|
||||
|
||||
} else if (!strncmp(argv[x], "--proxy-version=", 16)) {
|
||||
proxy_version = version_for_name(&argv[x][16]);
|
||||
|
||||
} else {
|
||||
throw invalid_argument(string_printf("unknown option: %s", argv[x]));
|
||||
}
|
||||
}
|
||||
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
shared_ptr<ServerState> state(new ServerState());
|
||||
|
||||
log(INFO, "starting network subsystem");
|
||||
shared_ptr<struct event_base> base(event_base_new(), event_base_free);
|
||||
|
||||
log(INFO, "reading network addresses");
|
||||
state->all_addresses = get_local_addresses();
|
||||
for (const auto& it : state->all_addresses) {
|
||||
string addr_str = string_for_address(it.second);
|
||||
log(INFO, "found interface: %s = %s", it.first.c_str(), addr_str.c_str());
|
||||
}
|
||||
|
||||
log(INFO, "loading configuration");
|
||||
auto config_json = JSONObject::parse(load_file("system/config.json"));
|
||||
populate_state_from_config(state, config_json);
|
||||
|
||||
shared_ptr<DNSServer> dns_server;
|
||||
if (state->run_dns_server) {
|
||||
log(INFO, "starting dns server");
|
||||
dns_server.reset(new DNSServer(base, state->local_address,
|
||||
state->external_address));
|
||||
dns_server->listen("", 53);
|
||||
}
|
||||
|
||||
shared_ptr<ProxyServer> proxy_server;
|
||||
shared_ptr<Server> game_server;
|
||||
if (proxy_port) {
|
||||
log(INFO, "starting proxy");
|
||||
sockaddr_storage proxy_destination_ss = make_sockaddr_storage(
|
||||
proxy_hostname, proxy_port).first;
|
||||
proxy_server.reset(new ProxyServer(base, proxy_destination_ss,
|
||||
proxy_version));
|
||||
proxy_server->listen(proxy_port);
|
||||
if (proxy_version == GameVersion::BB) {
|
||||
proxy_server->listen(proxy_port + 1);
|
||||
}
|
||||
|
||||
} else {
|
||||
log(INFO, "starting game server");
|
||||
game_server.reset(new Server(base, state));
|
||||
for (const auto& it : state->port_configuration) {
|
||||
game_server->listen("", it.second.port, it.second.version, it.second.behavior);
|
||||
}
|
||||
|
||||
log(INFO, "loading license list");
|
||||
state->license_manager.reset(new LicenseManager("system/licenses.nsi"));
|
||||
|
||||
log(INFO, "loading battle parameters");
|
||||
state->battle_params.reset(new BattleParamTable("system/blueburst/BattleParamEntry"));
|
||||
|
||||
log(INFO, "loading level table");
|
||||
state->level_table.reset(new LevelTable("system/blueburst/PlyLevelTbl.prs", true));
|
||||
|
||||
log(INFO, "collecting quest metadata");
|
||||
state->quest_index.reset(new QuestIndex("system/quests"));
|
||||
}
|
||||
|
||||
if (!state->username.empty()) {
|
||||
log(INFO, "switching to user %s", state->username.c_str());
|
||||
drop_privileges(state->username);
|
||||
}
|
||||
|
||||
bool should_run_shell = (state->run_shell_behavior == ServerState::RunShellBehavior::Always);
|
||||
if (state->run_shell_behavior == ServerState::RunShellBehavior::Default) {
|
||||
should_run_shell = isatty(fileno(stdin));
|
||||
}
|
||||
|
||||
shared_ptr<Shell> shell;
|
||||
if (should_run_shell) {
|
||||
log(INFO, "starting interactive shell");
|
||||
if (proxy_port) {
|
||||
shell.reset(new ProxyShell(base, state, proxy_server));
|
||||
} else {
|
||||
shell.reset(new ServerShell(base, state));
|
||||
}
|
||||
}
|
||||
|
||||
log(INFO, "ready");
|
||||
event_base_dispatch(base.get());
|
||||
|
||||
log(INFO, "normal shutdown");
|
||||
return 0;
|
||||
}
|
||||
+443
@@ -0,0 +1,443 @@
|
||||
#include "Map.hh"
|
||||
|
||||
#include <phosg/Filesystem.hh>
|
||||
|
||||
#include "FileContentsCache.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
extern FileContentsCache file_cache;
|
||||
|
||||
|
||||
|
||||
static void load_battle_param_file(const string& filename, BattleParams* entries) {
|
||||
scoped_fd fd(filename, O_RDONLY);
|
||||
readx(fd, entries, 0x60 * sizeof(BattleParams));
|
||||
}
|
||||
|
||||
BattleParamTable::BattleParamTable(const char* prefix) {
|
||||
load_battle_param_file(string_printf("%s_on.dat", prefix),
|
||||
&this->entries[0][0][0][0]);
|
||||
load_battle_param_file(string_printf("%s_lab_on.dat", prefix),
|
||||
&this->entries[0][1][0][0]);
|
||||
load_battle_param_file(string_printf("%s_ep4_on.dat", prefix),
|
||||
&this->entries[0][2][0][0]);
|
||||
load_battle_param_file(string_printf("%s.dat", prefix),
|
||||
&this->entries[1][0][0][0]);
|
||||
load_battle_param_file(string_printf("%s_lab.dat", prefix),
|
||||
&this->entries[1][1][0][0]);
|
||||
load_battle_param_file(string_printf("%s_ep4.dat", prefix),
|
||||
&this->entries[1][2][0][0]);
|
||||
}
|
||||
|
||||
const BattleParams& BattleParamTable::get(bool solo, uint8_t episode,
|
||||
uint8_t difficulty, uint8_t monster_type) const {
|
||||
if (episode > 3) {
|
||||
throw invalid_argument("incorrect episode");
|
||||
}
|
||||
if (difficulty > 4) {
|
||||
throw invalid_argument("incorrect difficulty");
|
||||
}
|
||||
if (monster_type > 0x60) {
|
||||
throw invalid_argument("incorrect monster type");
|
||||
}
|
||||
return this->entries[!!solo][episode][difficulty][monster_type];
|
||||
}
|
||||
|
||||
const BattleParams* BattleParamTable::get_subtable(bool solo, uint8_t episode,
|
||||
uint8_t difficulty) const {
|
||||
if (episode > 3) {
|
||||
throw invalid_argument("incorrect episode");
|
||||
}
|
||||
if (difficulty > 4) {
|
||||
throw invalid_argument("incorrect difficulty");
|
||||
}
|
||||
return &this->entries[!!solo][episode][difficulty][0];
|
||||
}
|
||||
|
||||
|
||||
|
||||
PSOEnemy::PSOEnemy() : PSOEnemy(0, 0) { }
|
||||
|
||||
PSOEnemy::PSOEnemy(uint32_t experience, uint32_t rt_index) : unused(0),
|
||||
hit_flags(0), last_hit(0), experience(experience), rt_index(rt_index) { }
|
||||
|
||||
|
||||
|
||||
struct EnemyEntry {
|
||||
uint32_t base;
|
||||
uint16_t reserved0;
|
||||
uint16_t num_clones;
|
||||
uint32_t reserved[11];
|
||||
float reserved12;
|
||||
uint32_t reserved13;
|
||||
uint32_t reserved14;
|
||||
uint32_t skin;
|
||||
uint32_t reserved15;
|
||||
};
|
||||
|
||||
static vector<PSOEnemy> parse_map(uint8_t episode, uint8_t difficulty,
|
||||
const BattleParams* battle_params, const EnemyEntry* map,
|
||||
size_t entry_count, bool alt_enemies) {
|
||||
|
||||
vector<PSOEnemy> enemies;
|
||||
enemies.resize(0xB50);
|
||||
size_t num_enemies = 0;
|
||||
|
||||
// TODO: this is some of the nastiest code ever. de-nastify it at your leisure
|
||||
for (size_t y = 0; y < entry_count; y++) {
|
||||
if (enemies.size() >= 0xB50) {
|
||||
break;
|
||||
}
|
||||
|
||||
size_t num_clones = map[y].num_clones;
|
||||
|
||||
switch (map[y].base) {
|
||||
case 0x40: // Hildebear and Hildetorr
|
||||
enemies[num_enemies].rt_index = 0x01 + (map[y].skin & 0x01);
|
||||
enemies[num_enemies].experience = battle_params[0x49 + (map[y].skin & 0x01)].experience;
|
||||
break;
|
||||
case 0x41: // Rappies
|
||||
if (episode == 3) { // Del Rappy and Sand Rappy
|
||||
enemies[num_enemies].rt_index = 17 + (map[y].skin & 0x01);
|
||||
if (alt_enemies) {
|
||||
enemies[num_enemies].experience = battle_params[0x17 + (map[y].skin & 0x01)].experience;
|
||||
} else {
|
||||
enemies[num_enemies].experience = battle_params[0x05 + (map[y].skin & 0x01)].experience;
|
||||
}
|
||||
} else { // Rag Rappy and Al Rappy (Love for Episode II)
|
||||
if (map[y].skin & 0x01) {
|
||||
enemies[num_enemies].rt_index = 0xFF; // No clue what rappy it could be... yet.
|
||||
} else {
|
||||
enemies[num_enemies].rt_index = 5;
|
||||
}
|
||||
enemies[num_enemies].experience = battle_params[0x18 + (map[y].skin & 0x01)].experience;
|
||||
}
|
||||
break;
|
||||
case 0x42: // Monest + 30 Mothmants
|
||||
enemies[num_enemies].experience = battle_params[0x01].experience;
|
||||
enemies[num_enemies].rt_index = 4;
|
||||
for (size_t x = 0; x < 30; x++) {
|
||||
if (num_enemies >= 0xB50) {
|
||||
break;
|
||||
}
|
||||
num_enemies++;
|
||||
enemies[num_enemies].rt_index = 3;
|
||||
enemies[num_enemies].experience = battle_params[0x00].experience;
|
||||
}
|
||||
break;
|
||||
case 0x43: // Savage Wolf and Barbarous Wolf
|
||||
enemies[num_enemies].rt_index = 7 + ((map[y].reserved[10] & 0x800000) ? 1 : 0);
|
||||
enemies[num_enemies].experience = battle_params[0x02 + ((map[y].reserved[10] & 0x800000) ? 1 : 0)].experience;
|
||||
break;
|
||||
case 0x44: // Booma family
|
||||
enemies[num_enemies].rt_index = 9 + (map[y].skin % 3);
|
||||
enemies[num_enemies].experience = battle_params[0x4B + (map[y].skin % 3)].experience;
|
||||
break;
|
||||
case 0x60: // Grass Assassin
|
||||
enemies[num_enemies].rt_index = 12;
|
||||
enemies[num_enemies].experience = battle_params[0x4E].experience;
|
||||
break;
|
||||
case 0x61: // Del Lily, Poison Lily, Nar Lily
|
||||
if ((episode == 2) && (alt_enemies)) {
|
||||
enemies[num_enemies].rt_index = 83;
|
||||
enemies[num_enemies].experience = battle_params[0x25].experience;
|
||||
} else {
|
||||
enemies[num_enemies].rt_index = 13 + ((map[y].reserved[10] & 0x800000) ? 1 : 0);
|
||||
enemies[num_enemies].experience = battle_params[0x04 + ((map[y].reserved[10] & 0x800000) ? 1 : 0)].experience;
|
||||
}
|
||||
break;
|
||||
case 0x62: // Nano Dragon
|
||||
enemies[num_enemies].rt_index = 15;
|
||||
enemies[num_enemies].experience = battle_params[0x1A].experience;
|
||||
break;
|
||||
case 0x63: // Shark family
|
||||
enemies[num_enemies].rt_index = 16 + (map[y].skin % 3);
|
||||
enemies[num_enemies].experience = battle_params[0x4F + (map[y].skin % 3)].experience;
|
||||
break;
|
||||
case 0x64: // Slime + 4 clones
|
||||
enemies[num_enemies].rt_index = 19 + ((map[y].reserved[10] & 0x800000) ? 1 : 0);
|
||||
enemies[num_enemies].experience = battle_params[0x2F + ((map[y].reserved[10] & 0x800000) ? 0 : 1)].experience;
|
||||
for (size_t x = 0; x < 4; x++) {
|
||||
if (num_enemies >= 0xB50) {
|
||||
break;
|
||||
}
|
||||
num_enemies++;
|
||||
enemies[num_enemies].rt_index = 19;
|
||||
enemies[num_enemies].experience = battle_params[0x30].experience;
|
||||
}
|
||||
break;
|
||||
case 0x65: // Pan Arms, Migium, Hidoom
|
||||
for (size_t x = 0; x < 3; x++) {
|
||||
enemies[num_enemies + x].rt_index = 21 + x;
|
||||
enemies[num_enemies + x].experience = battle_params[0x31 + x].experience;
|
||||
}
|
||||
num_enemies += 2;
|
||||
break;
|
||||
case 0x80: // Dubchic and Gilchic
|
||||
enemies[num_enemies].experience = battle_params[0x1B + (map[y].skin & 0x01)].experience;
|
||||
if (map[y].skin & 0x01) {
|
||||
enemies[num_enemies].rt_index = 50;
|
||||
} else {
|
||||
enemies[num_enemies].rt_index = 24;
|
||||
}
|
||||
break;
|
||||
case 0x81: // Garanz
|
||||
enemies[num_enemies].rt_index = 25;
|
||||
enemies[num_enemies].experience = battle_params[0x1D].experience;
|
||||
break;
|
||||
case 0x82: // Sinow Beat and Gold
|
||||
enemies[num_enemies].rt_index = 26 + ((map[y].reserved[10] & 0x800000) ? 1 : 0);
|
||||
if (map[y].reserved[10] & 0x800000) {
|
||||
enemies[num_enemies].experience = battle_params[0x13].experience;
|
||||
} else {
|
||||
enemies[num_enemies].experience = battle_params[0x06].experience;
|
||||
}
|
||||
if (map[y].num_clones == 0) {
|
||||
num_clones = 4; // only if no clone # present
|
||||
}
|
||||
break;
|
||||
case 0x83: // Canadine
|
||||
enemies[num_enemies].rt_index = 28;
|
||||
enemies[num_enemies].experience = battle_params[0x07].experience;
|
||||
break;
|
||||
case 0x84: // Canadine Group
|
||||
enemies[num_enemies].rt_index = 29;
|
||||
enemies[num_enemies].experience = battle_params[0x09].experience;
|
||||
for (size_t x = 1; x < 9; x++) {
|
||||
enemies[num_enemies + x].rt_index = 28;
|
||||
enemies[num_enemies + x].experience = battle_params[0x08].experience;
|
||||
}
|
||||
num_enemies += 8;
|
||||
break;
|
||||
case 0x85: // Dubwitch
|
||||
break;
|
||||
case 0xA0: // Delsaber
|
||||
enemies[num_enemies].rt_index = 30;
|
||||
enemies[num_enemies].experience = battle_params[0x52].experience;
|
||||
break;
|
||||
case 0xA1: // Chaos Sorcerer + 2 Bits
|
||||
enemies[num_enemies].rt_index = 31;
|
||||
enemies[num_enemies].experience = battle_params[0x0A].experience;
|
||||
num_enemies += 2;
|
||||
break;
|
||||
case 0xA2: // Dark Gunner
|
||||
enemies[num_enemies].rt_index = 34;
|
||||
enemies[num_enemies].experience = battle_params[0x1E].experience;
|
||||
break;
|
||||
case 0xA4: // Chaos Bringer
|
||||
enemies[num_enemies].rt_index = 36;
|
||||
enemies[num_enemies].experience = battle_params[0x0D].experience;
|
||||
break;
|
||||
case 0xA5: // Dark Belra
|
||||
enemies[num_enemies].rt_index = 37;
|
||||
enemies[num_enemies].experience = battle_params[0x0E].experience;
|
||||
break;
|
||||
case 0xA6: // Dimenian family
|
||||
enemies[num_enemies].rt_index = 41 + (map[y].skin % 3);
|
||||
enemies[num_enemies].experience = battle_params[0x53 + (map[y].skin % 3)].experience;
|
||||
break;
|
||||
case 0xA7: // Bulclaw + 4 claws
|
||||
enemies[num_enemies].rt_index = 40;
|
||||
enemies[num_enemies].experience = battle_params[0x1F].experience;
|
||||
for (size_t x = 1; x < 5; x++) {
|
||||
enemies[num_enemies + x].rt_index = 38;
|
||||
enemies[num_enemies + x].experience = battle_params[0x20].experience;
|
||||
}
|
||||
num_enemies += 4;
|
||||
break;
|
||||
case 0xA8: // Claw
|
||||
enemies[num_enemies].rt_index = 38;
|
||||
enemies[num_enemies].experience = battle_params[0x20].experience;
|
||||
break;
|
||||
case 0xC0: // Dragon or Gal Gryphon
|
||||
if (episode == 1) {
|
||||
enemies[num_enemies].rt_index = 44;
|
||||
enemies[num_enemies].experience = battle_params[0x12].experience;
|
||||
} else if (episode == 0x02) {
|
||||
enemies[num_enemies].rt_index = 77;
|
||||
enemies[num_enemies].experience = battle_params[0x1E].experience;
|
||||
}
|
||||
break;
|
||||
case 0xC1: // De Rol Le
|
||||
enemies[num_enemies].rt_index = 45;
|
||||
enemies[num_enemies].experience = battle_params[0x0F].experience;
|
||||
break;
|
||||
case 0xC2: // Vol Opt form 1
|
||||
break;
|
||||
case 0xC5: // Vol Opt form 2
|
||||
enemies[num_enemies].rt_index = 46;
|
||||
enemies[num_enemies].experience = battle_params[0x25].experience;
|
||||
break;
|
||||
case 0xC8: // Dark Falz + 510 Helpers
|
||||
enemies[num_enemies].rt_index = 47;
|
||||
if (difficulty) {
|
||||
enemies[num_enemies].experience = battle_params[0x38].experience; // Form 2
|
||||
} else {
|
||||
enemies[num_enemies].experience = battle_params[0x37].experience;
|
||||
}
|
||||
for (size_t x = 1; x < 511; x++) {
|
||||
//enemies[num_enemies + x].base = 200;
|
||||
enemies[num_enemies + x].experience = battle_params[0x35].experience;
|
||||
}
|
||||
num_enemies += 510;
|
||||
break;
|
||||
case 0xCA: // Olga Flow
|
||||
enemies[num_enemies].rt_index = 78;
|
||||
enemies[num_enemies].experience = battle_params[0x2C].experience;
|
||||
num_enemies += 512;
|
||||
break;
|
||||
case 0xCB: // Barba Ray
|
||||
enemies[num_enemies].rt_index = 73;
|
||||
enemies[num_enemies].experience = battle_params[0x0F].experience;
|
||||
num_enemies += 47;
|
||||
break;
|
||||
case 0xCC: // Gol Dragon
|
||||
enemies[num_enemies].rt_index = 76;
|
||||
enemies[num_enemies].experience = battle_params[0x12].experience;
|
||||
num_enemies += 5;
|
||||
break;
|
||||
case 0xD4: // Sinow Berill & Spigell
|
||||
enemies[num_enemies].rt_index = 62 + ((map[y].reserved[10] & 0x800000) ? 1 : 0);
|
||||
enemies[num_enemies].experience = battle_params[(map[y].reserved[10] & 0x800000) ? 0x13 : 0x06].experience;
|
||||
num_enemies += 4; // Add 4 clones which are never used...
|
||||
break;
|
||||
case 0xD5: // Merillia & Meriltas
|
||||
enemies[num_enemies].rt_index = 52 + (map[y].skin & 0x01);
|
||||
enemies[num_enemies].experience = battle_params[0x4B + (map[y].skin & 0x01)].experience;
|
||||
break;
|
||||
case 0xD6: // Mericus, Merikle, & Mericarol
|
||||
enemies[num_enemies].rt_index = 56 + (map[y].skin % 3);
|
||||
if (map[y].skin) {
|
||||
enemies[num_enemies].experience = battle_params[0x44 + (map[y].skin % 3)].experience;
|
||||
} else {
|
||||
enemies[num_enemies].experience = battle_params[0x3A].experience;
|
||||
}
|
||||
break;
|
||||
case 0xD7: // Ul Gibbon and Zol Gibbon
|
||||
enemies[num_enemies].rt_index = 59 + (map[y].skin & 0x01);
|
||||
enemies[num_enemies].experience = battle_params[0x3B + (map[y].skin & 0x01)].experience;
|
||||
break;
|
||||
case 0xD8: // Gibbles
|
||||
enemies[num_enemies].rt_index = 61;
|
||||
enemies[num_enemies].experience = battle_params[0x3D].experience;
|
||||
break;
|
||||
case 0xD9: // Gee
|
||||
enemies[num_enemies].rt_index = 54;
|
||||
enemies[num_enemies].experience = battle_params[0x07].experience;
|
||||
break;
|
||||
case 0xDA: // Gi Gue
|
||||
enemies[num_enemies].rt_index = 55;
|
||||
enemies[num_enemies].experience = battle_params[0x1A].experience;
|
||||
break;
|
||||
case 0xDB: // Deldepth
|
||||
enemies[num_enemies].rt_index = 71;
|
||||
enemies[num_enemies].experience = battle_params[0x30].experience;
|
||||
break;
|
||||
case 0xDC: // Delbiter
|
||||
enemies[num_enemies].rt_index = 72;
|
||||
enemies[num_enemies].experience = battle_params[0x0D].experience;
|
||||
break;
|
||||
case 0xDD: // Dolmolm and Dolmdarl
|
||||
enemies[num_enemies].rt_index = 64 + (map[y].skin & 0x01);
|
||||
enemies[num_enemies].experience = battle_params[0x4F + (map[y].skin & 0x01)].experience;
|
||||
break;
|
||||
case 0xDE: // Morfos
|
||||
enemies[num_enemies].rt_index = 66;
|
||||
enemies[num_enemies].experience = battle_params[0x40].experience;
|
||||
break;
|
||||
case 0xDF: // Recobox & Recons
|
||||
enemies[num_enemies].rt_index = 67;
|
||||
enemies[num_enemies].experience = battle_params[0x41].experience;
|
||||
for (size_t x = 1; x <= map[y].num_clones; x++) {
|
||||
enemies[num_enemies + x].rt_index = 68;
|
||||
enemies[num_enemies + x].experience = battle_params[0x42].experience;
|
||||
}
|
||||
break;
|
||||
case 0xE0: // Epsilon, Sinow Zoa and Zele
|
||||
if ((episode == 0x02) && (alt_enemies)) {
|
||||
enemies[num_enemies].rt_index = 84;
|
||||
enemies[num_enemies].experience = battle_params[0x23].experience;
|
||||
num_enemies += 4;
|
||||
} else {
|
||||
enemies[num_enemies].rt_index = 69 + (map[y].skin & 0x01);
|
||||
enemies[num_enemies].experience = battle_params[0x43 + (map[y].skin & 0x01)].experience;
|
||||
}
|
||||
break;
|
||||
case 0xE1: // Ill Gill
|
||||
enemies[num_enemies].rt_index = 82;
|
||||
enemies[num_enemies].experience = battle_params[0x26].experience;
|
||||
break;
|
||||
case 0x0110: // Astark
|
||||
enemies[num_enemies].rt_index = 1;
|
||||
enemies[num_enemies].experience = battle_params[0x09].experience;
|
||||
break;
|
||||
case 0x0111: // Satellite Lizard and Yowie
|
||||
enemies[num_enemies].rt_index = 2 + ((map[y].reserved[10] & 0x800000) ? 0 : 1);
|
||||
enemies[num_enemies].experience = battle_params[0x0D + ((map[y].reserved[10] & 0x800000) ? 1 : 0) + (alt_enemies ? 0x10 : 0)].experience;
|
||||
break;
|
||||
case 0x0112: // Merissa A/AA
|
||||
enemies[num_enemies].rt_index = 4 + (map[y].skin & 0x01);
|
||||
enemies[num_enemies].experience = battle_params[0x19 + (map[y].skin & 0x01)].experience;
|
||||
break;
|
||||
case 0x0113: // Girtablulu
|
||||
enemies[num_enemies].rt_index = 6;
|
||||
enemies[num_enemies].experience = battle_params[0x1F].experience;
|
||||
break;
|
||||
case 0x0114: // Zu and Pazuzu
|
||||
enemies[num_enemies].rt_index = 7 + (map[y].skin & 0x01);
|
||||
enemies[num_enemies].experience = battle_params[0x0B + (map[y].skin & 0x01) + (alt_enemies ? 0x14: 0x00)].experience;
|
||||
break;
|
||||
case 0x0115: // Boota family
|
||||
enemies[num_enemies].rt_index = 9 + (map[y].skin % 3);
|
||||
if (map[y].skin & 2) {
|
||||
enemies[num_enemies].experience = battle_params[0x03].experience;
|
||||
} else {
|
||||
enemies[num_enemies].experience = battle_params[0x00 + (map[y].skin % 3)].experience;
|
||||
}
|
||||
break;
|
||||
case 0x0116: // Dorphon and Eclair
|
||||
enemies[num_enemies].rt_index = 12 + (map[y].skin & 0x01);
|
||||
enemies[num_enemies].experience = battle_params[0x0F + (map[y].skin & 0x01)].experience;
|
||||
break;
|
||||
case 0x0117: // Goran family
|
||||
if (map[y].skin & 0x02) {
|
||||
enemies[num_enemies].rt_index = 15;
|
||||
} else if (map[y].skin & 0x01) {
|
||||
enemies[num_enemies].rt_index = 16;
|
||||
} else {
|
||||
enemies[num_enemies].rt_index = 14;
|
||||
}
|
||||
enemies[num_enemies].experience = battle_params[0x11 + (map[y].skin % 3)].experience;
|
||||
break;
|
||||
case 0x0119: // Saint Million, Shambertin, and Kondrieu
|
||||
if (map[y].reserved[10] & 0x800000) {
|
||||
enemies[num_enemies].rt_index = 21;
|
||||
} else {
|
||||
enemies[num_enemies].rt_index = 19 + (map[y].skin & 0x01);
|
||||
}
|
||||
enemies[num_enemies].experience = battle_params[0x22].experience;
|
||||
break;
|
||||
default:
|
||||
enemies[num_enemies].experience = 0xFFFFFFFF;
|
||||
log(WARNING, "unknown enemy type %08" PRIX32 " %08" PRIX32, map[y].base,
|
||||
map[y].skin);
|
||||
break;
|
||||
}
|
||||
if (num_clones) {
|
||||
num_enemies += num_clones;
|
||||
}
|
||||
num_enemies++;
|
||||
}
|
||||
|
||||
return enemies;
|
||||
}
|
||||
|
||||
vector<PSOEnemy> load_map(const char* filename, uint8_t episode,
|
||||
uint8_t difficulty, const BattleParams* battle_params, bool alt_enemies) {
|
||||
shared_ptr<const string> data = file_cache.get(filename);
|
||||
const EnemyEntry* entries = reinterpret_cast<const EnemyEntry*>(data->data());
|
||||
size_t entry_count = data->size() / sizeof(EnemyEntry);
|
||||
return parse_map(episode, difficulty, battle_params, entries, entry_count,
|
||||
alt_enemies);
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
|
||||
|
||||
struct BattleParams {
|
||||
uint16_t atp; // attack power
|
||||
uint16_t psv; // perseverance (intelligence?)
|
||||
uint16_t evp; // evasion
|
||||
uint16_t hp; // hit points
|
||||
uint16_t dfp; // defense
|
||||
uint16_t ata; // accuracy
|
||||
uint16_t lck; // luck
|
||||
uint8_t unknown[14];
|
||||
uint32_t experience;
|
||||
uint32_t difficulty;
|
||||
};
|
||||
|
||||
struct BattleParamTable {
|
||||
BattleParams entries[2][3][4][0x60]; // online/offline, episode, difficulty, monster type
|
||||
|
||||
BattleParamTable(const char* filename_prefix);
|
||||
|
||||
const BattleParams& get(bool solo, uint8_t episode, uint8_t difficulty,
|
||||
uint8_t monster_type) const;
|
||||
const BattleParams* get_subtable(bool solo, uint8_t episode,
|
||||
uint8_t difficulty) const;
|
||||
};
|
||||
|
||||
|
||||
|
||||
struct BattleParamIndex {
|
||||
BattleParamTable table_for_episode[3];
|
||||
};
|
||||
|
||||
// an enemy entry as loaded by the game
|
||||
struct PSOEnemy {
|
||||
uint16_t unused;
|
||||
uint8_t hit_flags;
|
||||
uint8_t last_hit;
|
||||
uint32_t experience;
|
||||
uint32_t rt_index;
|
||||
|
||||
PSOEnemy();
|
||||
PSOEnemy(uint32_t experience, uint32_t rt_index);
|
||||
};
|
||||
|
||||
std::vector<PSOEnemy> load_map(const char* filename, uint8_t episode,
|
||||
uint8_t difficulty, const BattleParams* bp, bool alt_enemies);
|
||||
@@ -0,0 +1,9 @@
|
||||
#include "Menu.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
MenuItem::MenuItem(uint32_t item_id, const u16string& name,
|
||||
const u16string& description, uint32_t flags) : item_id(item_id), name(name),
|
||||
description(description), flags(flags) { }
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
|
||||
|
||||
enum MenuItemFlag {
|
||||
InvisibleOnDC = 0x01,
|
||||
InvisibleOnPC = 0x02,
|
||||
InvisibleOnGC = 0x04,
|
||||
InvisibleOnGCEpisode3 = 0x08,
|
||||
InvisibleOnBB = 0x10,
|
||||
RequiresMessageBoxes = 0x20,
|
||||
};
|
||||
|
||||
struct MenuItem {
|
||||
uint32_t item_id;
|
||||
std::u16string name;
|
||||
std::u16string description;
|
||||
uint32_t flags;
|
||||
|
||||
MenuItem(uint32_t item_id, const std::u16string& name,
|
||||
const std::u16string& description, uint32_t flags);
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "NetworkAddresses.hh"
|
||||
|
||||
#include <netinet/in.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netdb.h>
|
||||
#include <errno.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <ifaddrs.h>
|
||||
|
||||
#include <memory>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
uint32_t resolve_address(const char* address) {
|
||||
struct addrinfo *res0;
|
||||
if (getaddrinfo(address, NULL, NULL, &res0)) {
|
||||
auto e = string_for_error(errno);
|
||||
throw runtime_error(string_printf("can\'t resolve hostname %s: %s", address,
|
||||
e.c_str()));
|
||||
}
|
||||
|
||||
std::unique_ptr<struct addrinfo, void(*)(struct addrinfo*)> res0_unique(
|
||||
res0, freeaddrinfo);
|
||||
struct addrinfo *res4 = NULL;
|
||||
for (struct addrinfo* res = res0; res; res = res->ai_next) {
|
||||
if (res->ai_family == AF_INET) {
|
||||
res4 = res;
|
||||
}
|
||||
}
|
||||
if (!res4) {
|
||||
throw runtime_error(string_printf(
|
||||
"can\'t resolve hostname %s: no usable data", address));
|
||||
}
|
||||
|
||||
struct sockaddr_in* res_sin = (struct sockaddr_in*)res4->ai_addr;
|
||||
return bswap32(res_sin->sin_addr.s_addr);
|
||||
}
|
||||
|
||||
map<string, uint32_t> get_local_addresses() {
|
||||
struct ifaddrs* ifa_raw;
|
||||
if (getifaddrs(&ifa_raw)) {
|
||||
auto s = string_for_error(errno);
|
||||
throw runtime_error(string_printf("failed to get interface addresses: %s", s.c_str()));
|
||||
}
|
||||
|
||||
unique_ptr<struct ifaddrs, void(*)(struct ifaddrs*)> ifa(ifa_raw, freeifaddrs);
|
||||
|
||||
map<string, uint32_t> ret;
|
||||
for (struct ifaddrs* i = ifa.get(); i; i = i->ifa_next) {
|
||||
if (!i->ifa_addr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* sin = reinterpret_cast<sockaddr_in*>(i->ifa_addr);
|
||||
if (sin->sin_family != AF_INET) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ret.emplace(i->ifa_name, bswap32(sin->sin_addr.s_addr));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool is_local_address(uint32_t addr) {
|
||||
uint8_t net = (addr >> 24) & 0xFF;
|
||||
if ((net != 127) && (net != 172) && (net != 10) && (net != 192)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_local_address(const sockaddr_storage& daddr) {
|
||||
if (daddr.ss_family != AF_INET) {
|
||||
return false;
|
||||
}
|
||||
const sockaddr_in* sin = reinterpret_cast<const sockaddr_in*>(&daddr);
|
||||
return is_local_address(bswap32(sin->sin_addr.s_addr));
|
||||
}
|
||||
|
||||
string string_for_address(uint32_t address) {
|
||||
return string_printf("%hhu.%hhu.%hhu.%hhu",
|
||||
static_cast<uint8_t>(address >> 24), static_cast<uint8_t>(address >> 16),
|
||||
static_cast<uint8_t>(address >> 8), static_cast<uint8_t>(address));
|
||||
}
|
||||
|
||||
uint32_t address_for_string(const char* address) {
|
||||
return bswap32(inet_addr(address));
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <netinet/in.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
|
||||
|
||||
// PSO is IPv4-only, so we just treat addresses as uint32_t everywhere because
|
||||
// it's easier
|
||||
|
||||
uint32_t resolve_address(const char* address);
|
||||
std::map<std::string, uint32_t> get_local_addresses();
|
||||
uint32_t get_connected_address(int fd);
|
||||
bool is_local_address(uint32_t daddr);
|
||||
bool is_local_address(const sockaddr_storage& daddr);
|
||||
|
||||
std::string string_for_address(uint32_t address);
|
||||
uint32_t address_for_string(const char* address);
|
||||
@@ -0,0 +1,443 @@
|
||||
#include "PSOEncryption.hh"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
// TODO: fix style in this file, especially in psobb functions
|
||||
|
||||
|
||||
|
||||
// most ciphers used by pso are symmetric; alias decrypt to encrypt by default
|
||||
void PSOEncryption::decrypt(void* data, size_t size) {
|
||||
this->encrypt(data, size);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void PSOPCEncryption::update_stream() {
|
||||
uint32_t esi, edi, eax, ebp, edx;
|
||||
edi = 1;
|
||||
edx = 0x18;
|
||||
eax = edi;
|
||||
while (edx > 0) {
|
||||
esi = this->stream[eax + 0x1F];
|
||||
ebp = this->stream[eax];
|
||||
ebp = ebp - esi;
|
||||
this->stream[eax] = ebp;
|
||||
eax++;
|
||||
edx--;
|
||||
}
|
||||
edi = 0x19;
|
||||
edx = 0x1F;
|
||||
eax = edi;
|
||||
while (edx > 0) {
|
||||
esi = this->stream[eax - 0x18];
|
||||
ebp = this->stream[eax];
|
||||
ebp = ebp - esi;
|
||||
this->stream[eax] = ebp;
|
||||
eax++;
|
||||
edx--;
|
||||
}
|
||||
}
|
||||
|
||||
PSOPCEncryption::PSOPCEncryption(uint32_t seed) : offset(1) {
|
||||
uint32_t esi, ebx, edi, eax, edx, var1;
|
||||
esi = 1;
|
||||
ebx = seed;
|
||||
edi = 0x15;
|
||||
this->stream[56] = ebx;
|
||||
this->stream[55] = ebx;
|
||||
while (edi <= 0x46E) {
|
||||
eax = edi;
|
||||
var1 = eax / 55;
|
||||
edx = eax - (var1 * 55);
|
||||
ebx = ebx - esi;
|
||||
edi = edi + 0x15;
|
||||
this->stream[edx] = esi;
|
||||
esi = ebx;
|
||||
ebx = this->stream[edx];
|
||||
}
|
||||
for (size_t x = 0; x < 5; x++) {
|
||||
this->update_stream();
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t PSOPCEncryption::next() {
|
||||
if (this->offset == PC_STREAM_LENGTH) {
|
||||
this->update_stream();
|
||||
this->offset = 1;
|
||||
}
|
||||
return this->stream[this->offset++];
|
||||
}
|
||||
|
||||
void PSOPCEncryption::encrypt(void* vdata, size_t size) {
|
||||
if (size & 3) {
|
||||
throw invalid_argument("size must be a multiple of 4");
|
||||
}
|
||||
size >>= 2;
|
||||
|
||||
uint32_t* data = reinterpret_cast<uint32_t*>(vdata);
|
||||
for (size_t x = 0; x < size; x++) {
|
||||
data[x] ^= this->next();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void PSOGCEncryption::update_stream() {
|
||||
uint32_t r5, r6, r7;
|
||||
r5 = 0;
|
||||
r6 = 489;
|
||||
r7 = 0;
|
||||
|
||||
while (r6 != GC_STREAM_LENGTH) {
|
||||
this->stream[r5++] ^= this->stream[r6++];
|
||||
}
|
||||
|
||||
while (r5 != GC_STREAM_LENGTH) {
|
||||
this->stream[r5++] ^= this->stream[r7++];
|
||||
}
|
||||
|
||||
this->offset = 0;
|
||||
}
|
||||
|
||||
uint32_t PSOGCEncryption::next() {
|
||||
this->offset++;
|
||||
if (this->offset == GC_STREAM_LENGTH) {
|
||||
this->update_stream();
|
||||
}
|
||||
return this->stream[this->offset];
|
||||
}
|
||||
|
||||
PSOGCEncryption::PSOGCEncryption(uint32_t seed) : offset(0) {
|
||||
uint32_t x, y, basekey, source1, source2, source3;
|
||||
basekey = 0;
|
||||
|
||||
for (x = 0; x <= 16; x++) {
|
||||
for (y = 0; y < 32; y++) {
|
||||
seed = seed * 0x5D588B65;
|
||||
basekey = basekey >> 1;
|
||||
seed++;
|
||||
if (seed & 0x80000000) {
|
||||
basekey = basekey | 0x80000000;
|
||||
} else {
|
||||
basekey = basekey & 0x7FFFFFFF;
|
||||
}
|
||||
}
|
||||
this->stream[this->offset++] = basekey;
|
||||
}
|
||||
|
||||
this->stream[this->offset - 1] = (((this->stream[0] >> 9) ^ (this->stream[this->offset - 1] << 23)) ^ this->stream[15]);
|
||||
|
||||
source1 = 0;
|
||||
source2 = 1;
|
||||
source3 = this->offset - 1;
|
||||
while (this->offset != GC_STREAM_LENGTH) {
|
||||
this->stream[this->offset++] = (this->stream[source3++] ^ (((this->stream[source1++] << 23) & 0xFF800000) ^ ((this->stream[source2++] >> 9) & 0x007FFFFF)));
|
||||
}
|
||||
|
||||
for (size_t x = 0; x < 3; x++) {
|
||||
this->update_stream();
|
||||
}
|
||||
this->offset = GC_STREAM_LENGTH - 1;
|
||||
}
|
||||
|
||||
void PSOGCEncryption::encrypt(void* vdata, size_t size) {
|
||||
if (size & 3) {
|
||||
throw invalid_argument("size must be a multiple of 4");
|
||||
}
|
||||
size >>= 2;
|
||||
|
||||
uint32_t* data = reinterpret_cast<uint32_t*>(vdata);
|
||||
for (size_t x = 0; x < size; x++) {
|
||||
data[x] ^= this->next();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void PSOBBEncryption::decrypt(void* vdata, size_t size) {
|
||||
if (size & 7) {
|
||||
throw invalid_argument("size must be a multiple of 8");
|
||||
}
|
||||
size >>= 3;
|
||||
|
||||
uint32_t* data = reinterpret_cast<uint32_t*>(vdata);
|
||||
uint32_t edx, ebx, ebp, esi, edi;
|
||||
|
||||
edx = 0;
|
||||
while (edx < size) {
|
||||
ebx = data[edx];
|
||||
ebx = ebx ^ this->stream[5];
|
||||
ebp = ((this->stream[(ebx >> 0x18) + 0x12]+this->stream[((ebx >> 0x10)& 0xff) + 0x112])
|
||||
^ this->stream[((ebx >> 0x8)& 0xff) + 0x212]) + this->stream[(ebx & 0xff) + 0x312];
|
||||
ebp = ebp ^ this->stream[4];
|
||||
ebp ^= data[edx + 1];
|
||||
edi = ((this->stream[(ebp >> 0x18) + 0x12]+this->stream[((ebp >> 0x10)& 0xff) + 0x112])
|
||||
^ this->stream[((ebp >> 0x8)& 0xff) + 0x212]) + this->stream[(ebp & 0xff) + 0x312];
|
||||
edi = edi ^ this->stream[3];
|
||||
ebx = ebx ^ edi;
|
||||
esi = ((this->stream[(ebx >> 0x18) + 0x12]+this->stream[((ebx >> 0x10)& 0xff) + 0x112])
|
||||
^ this->stream[((ebx >> 0x8)& 0xff) + 0x212]) + this->stream[(ebx & 0xff) + 0x312];
|
||||
ebp = ebp ^ esi ^ this->stream[2];
|
||||
edi = ((this->stream[(ebp >> 0x18) + 0x12]+this->stream[((ebp >> 0x10)& 0xff) + 0x112])
|
||||
^ this->stream[((ebp >> 0x8)& 0xff) + 0x212]) + this->stream[(ebp & 0xff) + 0x312];
|
||||
edi = edi ^ this->stream[1];
|
||||
ebp = ebp ^ this->stream[0];
|
||||
ebx = ebx ^ edi;
|
||||
data[edx] = ebp;
|
||||
data[edx + 1] = ebx;
|
||||
edx += 2;
|
||||
}
|
||||
}
|
||||
|
||||
void PSOBBEncryption::encrypt(void* vdata, size_t size) {
|
||||
if (size & 7) {
|
||||
throw invalid_argument("size must be a multiple of 8");
|
||||
}
|
||||
size >>= 3;
|
||||
|
||||
uint8_t* data = reinterpret_cast<uint8_t*>(vdata);
|
||||
uint32_t edx, ebx, ebp, esi, edi;
|
||||
|
||||
edx = 0;
|
||||
while (edx < size) {
|
||||
ebx = data[edx];
|
||||
ebx = ebx ^ this->stream[0];
|
||||
ebp = ((this->stream[(ebx >> 0x18) + 0x12]+this->stream[((ebx >> 0x10)& 0xff) + 0x112])
|
||||
^ this->stream[((ebx >> 0x8)& 0xff) + 0x212]) + this->stream[(ebx & 0xff) + 0x312];
|
||||
ebp = ebp ^ this->stream[1];
|
||||
ebp ^= data[edx + 1];
|
||||
edi = ((this->stream[(ebp >> 0x18) + 0x12]+this->stream[((ebp >> 0x10)& 0xff) + 0x112])
|
||||
^ this->stream[((ebp >> 0x8)& 0xff) + 0x212]) + this->stream[(ebp & 0xff) + 0x312];
|
||||
edi = edi ^ this->stream[2];
|
||||
ebx = ebx ^ edi;
|
||||
esi = ((this->stream[(ebx >> 0x18) + 0x12]+this->stream[((ebx >> 0x10)& 0xff) + 0x112])
|
||||
^ this->stream[((ebx >> 0x8)& 0xff) + 0x212]) + this->stream[(ebx & 0xff) + 0x312];
|
||||
ebp = ebp ^ esi ^ this->stream[3];
|
||||
edi = ((this->stream[(ebp >> 0x18) + 0x12]+this->stream[((ebp >> 0x10)& 0xff) + 0x112])
|
||||
^ this->stream[((ebp >> 0x8)& 0xff) + 0x212]) + this->stream[(ebp & 0xff) + 0x312];
|
||||
edi = edi ^ this->stream[4];
|
||||
ebp = ebp ^ this->stream[5];
|
||||
ebx = ebx ^ edi;
|
||||
data[edx] = ebp;
|
||||
data[edx + 1] = ebx;
|
||||
edx += 2;
|
||||
}
|
||||
}
|
||||
|
||||
PSOBBEncryption::PSOBBEncryption(const KeyFile& key, const void* original_seed,
|
||||
size_t seed_size) : offset(0) {
|
||||
if (seed_size % 3) {
|
||||
throw invalid_argument("seed size must be divisible by 3");
|
||||
}
|
||||
|
||||
string seed;
|
||||
const uint8_t* original_seed_data = reinterpret_cast<const uint8_t*>(
|
||||
original_seed);
|
||||
for (size_t x = 0; x < seed_size; x += 3) {
|
||||
seed.push_back(original_seed_data[x] ^ 0x19);
|
||||
seed.push_back(original_seed_data[x + 1] ^ 0x16);
|
||||
seed.push_back(original_seed_data[x + 2] ^ 0x18);
|
||||
}
|
||||
|
||||
memcpy(this->stream, &key, sizeof(key));
|
||||
|
||||
this->postprocess_initial_stream(seed);
|
||||
}
|
||||
|
||||
void PSOBBEncryption::postprocess_initial_stream(const string& seed) {
|
||||
uint32_t eax, ecx, edx, ebx, ebp, esi, edi, ou, x;
|
||||
|
||||
ecx = 0;
|
||||
ebx = 0;
|
||||
|
||||
while (ebx < (seed.size() / 4)) {
|
||||
ebp = static_cast<uint32_t>(seed[ecx]) << 0x18;
|
||||
eax = ecx + 1;
|
||||
edx = eax % seed.size();
|
||||
eax = (static_cast<uint32_t>(seed[edx]) << 0x10) & 0xFF0000;
|
||||
ebp = (ebp | eax) & 0xffff00ff;
|
||||
eax = ecx + 2;
|
||||
edx = eax % seed.size();
|
||||
eax = (static_cast<uint32_t>(seed[edx]) << 0x08) & 0xFF00;
|
||||
ebp = (ebp | eax) & 0xffffff00;
|
||||
eax = ecx + 3;
|
||||
ecx = ecx + 4;
|
||||
edx = eax % seed.size();
|
||||
eax = static_cast<uint32_t>(seed[edx]);
|
||||
ebp = ebp | eax;
|
||||
eax = ecx;
|
||||
edx = eax % seed.size();
|
||||
this->stream[ebx] ^= ebp;
|
||||
ecx = edx;
|
||||
ebx++;
|
||||
}
|
||||
|
||||
ebp = 0;
|
||||
esi = 0;
|
||||
ecx = 0;
|
||||
edi = 0;
|
||||
ebx = 0;
|
||||
edx = 0x48;
|
||||
|
||||
while (edi < edx) {
|
||||
esi = esi ^ this->stream[0];
|
||||
eax = esi >> 0x18;
|
||||
ebx = (esi >> 0x10) & 0xff;
|
||||
eax = this->stream[eax + 0x12] + this->stream[ebx + 0x112];
|
||||
ebx = (esi >> 8) & 0xFF;
|
||||
eax = eax ^ this->stream[ebx + 0x212];
|
||||
ebx = esi & 0xFF;
|
||||
eax = eax + this->stream[ebx + 0x312];
|
||||
|
||||
eax = eax ^ this->stream[1];
|
||||
ecx = ecx ^ eax;
|
||||
ebx = ecx >> 0x18;
|
||||
eax = (ecx >> 0x10) & 0xFF;
|
||||
ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112];
|
||||
eax = (ecx >> 8) & 0xFF;
|
||||
ebx = ebx ^ this->stream[eax + 0x212];
|
||||
eax = ecx & 0xFF;
|
||||
ebx = ebx + this->stream[eax + 0x312];
|
||||
|
||||
for (x = 0; x <= 5; x++) {
|
||||
ebx = ebx ^ this->stream[(x * 2) + 2];
|
||||
esi = esi ^ ebx;
|
||||
ebx = esi >> 0x18;
|
||||
eax = (esi >> 0x10) & 0xFF;
|
||||
ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112];
|
||||
eax = (esi >> 8) & 0xFF;
|
||||
ebx = ebx ^ this->stream[eax + 0x212];
|
||||
eax = esi & 0xFF;
|
||||
ebx = ebx + this->stream[eax + 0x312];
|
||||
|
||||
ebx = ebx ^ this->stream[(x * 2) + 3];
|
||||
ecx = ecx ^ ebx;
|
||||
ebx = ecx >> 0x18;
|
||||
eax = (ecx >> 0x10) & 0xFF;
|
||||
ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112];
|
||||
eax = (ecx >> 8) & 0xFF;
|
||||
ebx = ebx ^ this->stream[eax + 0x212];
|
||||
eax = ecx & 0xff;
|
||||
ebx = ebx + this->stream[eax + 0x312];
|
||||
}
|
||||
|
||||
ebx = ebx ^ this->stream[14];
|
||||
esi = esi ^ ebx;
|
||||
eax = esi >> 0x18;
|
||||
ebx = (esi >> 0x10) & 0xFF;
|
||||
eax = this->stream[eax + 0x12] + this->stream[ebx + 0x112];
|
||||
ebx = (esi >> 8) & 0xff;
|
||||
eax = eax ^ this->stream[ebx + 0x212];
|
||||
ebx = esi & 0xff;
|
||||
eax = eax + this->stream[ebx + 0x312];
|
||||
|
||||
eax = eax ^ this->stream[15];
|
||||
eax = ecx ^ eax;
|
||||
ecx = eax >> 0x18;
|
||||
ebx = (eax >> 0x10) & 0xFF;
|
||||
ecx = this->stream[ecx + 0x12] + this->stream[ebx + 0x112];
|
||||
ebx = (eax >> 8) & 0xFF;
|
||||
ecx = ecx ^ this->stream[ebx + 0x212];
|
||||
ebx = eax & 0xFF;
|
||||
ecx = ecx + this->stream[ebx + 0x312];
|
||||
|
||||
ecx = ecx ^ this->stream[16];
|
||||
ecx = ecx ^ esi;
|
||||
esi = this->stream[17];
|
||||
esi = esi ^ eax;
|
||||
this->stream[(edi / 4)] = esi;
|
||||
this->stream[(edi / 4)+1] = ecx;
|
||||
edi = edi + 8;
|
||||
}
|
||||
|
||||
eax = 0;
|
||||
edx = 0;
|
||||
ou = 0;
|
||||
while (ou < 0x1000) {
|
||||
edi = 0x48;
|
||||
edx = 0x448;
|
||||
|
||||
while (edi < edx) {
|
||||
esi = esi ^ this->stream[0];
|
||||
eax = esi >> 0x18;
|
||||
ebx = (esi >> 0x10) & 0xff;
|
||||
eax = this->stream[eax + 0x12] + this->stream[ebx + 0x112];
|
||||
ebx = (esi >> 8) & 0xFF;
|
||||
eax = eax ^ this->stream[ebx + 0x212];
|
||||
ebx = esi & 0xFF;
|
||||
eax = eax + this->stream[ebx + 0x312];
|
||||
|
||||
eax = eax ^ this->stream[1];
|
||||
ecx = ecx ^ eax;
|
||||
ebx = ecx >> 0x18;
|
||||
eax = (ecx >> 0x10) & 0xFF;
|
||||
ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112];
|
||||
eax = (ecx >> 8) & 0xFF;
|
||||
ebx = ebx ^ this->stream[eax + 0x212];
|
||||
eax = ecx & 0xFF;
|
||||
ebx = ebx + this->stream[eax + 0x312];
|
||||
|
||||
for (x = 0; x <= 5; x++) {
|
||||
ebx = ebx ^ this->stream[(x * 2) + 2];
|
||||
esi = esi ^ ebx;
|
||||
ebx = esi >> 0x18;
|
||||
eax = (esi >> 0x10) & 0xFF;
|
||||
ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112];
|
||||
eax = (esi >> 8) & 0xFF;
|
||||
ebx = ebx ^ this->stream[eax + 0x212];
|
||||
eax = esi & 0xFF;
|
||||
ebx = ebx + this->stream[eax + 0x312];
|
||||
|
||||
ebx = ebx ^ this->stream[(x * 2) + 3];
|
||||
ecx = ecx ^ ebx;
|
||||
ebx = ecx >> 0x18;
|
||||
eax = (ecx >> 0x10) & 0xFF;
|
||||
ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112];
|
||||
eax = (ecx >> 8) & 0xFF;
|
||||
ebx = ebx ^ this->stream[eax + 0x212];
|
||||
eax = ecx & 0xFF;
|
||||
ebx = ebx + this->stream[eax + 0x312];
|
||||
}
|
||||
|
||||
ebx = ebx ^ this->stream[14];
|
||||
esi = esi ^ ebx;
|
||||
eax = esi >> 0x18;
|
||||
ebx = (esi >> 0x10) & 0xFF;
|
||||
eax = this->stream[eax + 0x12] + this->stream[ebx + 0x112];
|
||||
ebx = (esi >> 8) & 0xFF;
|
||||
eax = eax ^ this->stream[ebx + 0x212];
|
||||
ebx = esi & 0xFF;
|
||||
eax = eax + this->stream[ebx + 0x312];
|
||||
|
||||
eax = eax ^ this->stream[15];
|
||||
eax = ecx ^ eax;
|
||||
ecx = eax >> 0x18;
|
||||
ebx = (eax >> 0x10) & 0xFF;
|
||||
ecx = this->stream[ecx + 0x12] + this->stream[ebx + 0x112];
|
||||
ebx = (eax >> 8) & 0xFF;
|
||||
ecx = ecx ^ this->stream[ebx + 0x212];
|
||||
ebx = eax & 0xFF;
|
||||
ecx = ecx + this->stream[ebx + 0x312];
|
||||
|
||||
ecx = ecx ^ this->stream[16];
|
||||
ecx = ecx ^ esi;
|
||||
esi = this->stream[17];
|
||||
esi = esi ^ eax;
|
||||
this->stream[(ou / 4) + (edi / 4)] = esi;
|
||||
this->stream[(ou / 4) + (edi / 4) + 1] = ecx;
|
||||
edi = edi + 8;
|
||||
}
|
||||
ou = ou + 0x400;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
|
||||
|
||||
#define PC_STREAM_LENGTH 57
|
||||
#define GC_STREAM_LENGTH 521
|
||||
#define BB_STREAM_LENGTH 1042
|
||||
|
||||
class PSOEncryption {
|
||||
public:
|
||||
virtual ~PSOEncryption() = default;
|
||||
|
||||
virtual void encrypt(void* data, size_t size) = 0;
|
||||
virtual void decrypt(void* data, size_t size);
|
||||
|
||||
protected:
|
||||
PSOEncryption() = default;
|
||||
};
|
||||
|
||||
class PSOPCEncryption : public PSOEncryption {
|
||||
public:
|
||||
explicit PSOPCEncryption(uint32_t seed);
|
||||
|
||||
virtual void encrypt(void* data, size_t size);
|
||||
|
||||
protected:
|
||||
void update_stream();
|
||||
uint32_t next();
|
||||
|
||||
uint32_t stream[PC_STREAM_LENGTH];
|
||||
uint16_t offset;
|
||||
};
|
||||
|
||||
class PSOGCEncryption : public PSOEncryption {
|
||||
public:
|
||||
explicit PSOGCEncryption(uint32_t key);
|
||||
|
||||
virtual void encrypt(void* data, size_t size);
|
||||
|
||||
protected:
|
||||
void update_stream();
|
||||
uint32_t next();
|
||||
|
||||
uint32_t stream[GC_STREAM_LENGTH];
|
||||
uint16_t offset;
|
||||
};
|
||||
|
||||
class PSOBBEncryption : public PSOEncryption {
|
||||
public:
|
||||
struct KeyFile {
|
||||
uint32_t initial_keys[18];
|
||||
uint32_t private_keys[1024];
|
||||
};
|
||||
|
||||
PSOBBEncryption(const KeyFile& key, const void* seed, size_t seed_size);
|
||||
|
||||
virtual void encrypt(void* data, size_t size);
|
||||
virtual void decrypt(void* data, size_t size);
|
||||
|
||||
protected:
|
||||
PSOBBEncryption() = default;
|
||||
|
||||
void postprocess_initial_stream(const std::string& seed);
|
||||
|
||||
void update_stream();
|
||||
|
||||
uint32_t stream[BB_STREAM_LENGTH];
|
||||
uint16_t offset;
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "PSOProtocol.hh"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
uint16_t PSOCommandHeader::command(GameVersion version) const {
|
||||
switch (version) {
|
||||
case GameVersion::DC:
|
||||
case GameVersion::GC:
|
||||
return reinterpret_cast<const PSOCommandHeaderDCGC*>(this)->command;
|
||||
case GameVersion::PC:
|
||||
case GameVersion::Patch:
|
||||
return reinterpret_cast<const PSOCommandHeaderPC*>(this)->command;
|
||||
case GameVersion::BB:
|
||||
return reinterpret_cast<const PSOCommandHeaderBB*>(this)->command;
|
||||
}
|
||||
throw logic_error("unknown game version");
|
||||
}
|
||||
|
||||
uint16_t PSOCommandHeader::size(GameVersion version) const {
|
||||
switch (version) {
|
||||
case GameVersion::DC:
|
||||
case GameVersion::GC:
|
||||
return reinterpret_cast<const PSOCommandHeaderDCGC*>(this)->size;
|
||||
case GameVersion::PC:
|
||||
case GameVersion::Patch:
|
||||
return reinterpret_cast<const PSOCommandHeaderPC*>(this)->size;
|
||||
case GameVersion::BB:
|
||||
return reinterpret_cast<const PSOCommandHeaderBB*>(this)->size;
|
||||
}
|
||||
throw logic_error("unknown game version");
|
||||
}
|
||||
|
||||
uint32_t PSOCommandHeader::flag(GameVersion version) const {
|
||||
switch (version) {
|
||||
case GameVersion::DC:
|
||||
case GameVersion::GC:
|
||||
return reinterpret_cast<const PSOCommandHeaderDCGC*>(this)->flag;
|
||||
case GameVersion::PC:
|
||||
case GameVersion::Patch:
|
||||
return reinterpret_cast<const PSOCommandHeaderPC*>(this)->flag;
|
||||
case GameVersion::BB:
|
||||
return reinterpret_cast<const PSOCommandHeaderBB*>(this)->flag;
|
||||
}
|
||||
throw logic_error("unknown game version");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "Version.hh"
|
||||
|
||||
struct PSOCommandHeaderPC {
|
||||
uint16_t size;
|
||||
uint8_t command;
|
||||
uint8_t flag;
|
||||
};
|
||||
|
||||
struct PSOCommandHeaderDCGC {
|
||||
uint8_t command;
|
||||
uint8_t flag;
|
||||
uint16_t size;
|
||||
};
|
||||
|
||||
struct PSOCommandHeaderBB {
|
||||
uint16_t size;
|
||||
uint16_t command;
|
||||
uint32_t flag;
|
||||
};
|
||||
|
||||
union PSOCommandHeader {
|
||||
PSOCommandHeaderDCGC dc;
|
||||
PSOCommandHeaderPC pc;
|
||||
PSOCommandHeaderDCGC gc;
|
||||
PSOCommandHeaderBB bb;
|
||||
|
||||
uint16_t command(GameVersion version) const;
|
||||
uint16_t size(GameVersion version) const;
|
||||
uint32_t flag(GameVersion version) const;
|
||||
};
|
||||
|
||||
union PSOSubcommand {
|
||||
uint8_t byte[4];
|
||||
uint16_t word[2];
|
||||
uint32_t dword;
|
||||
};
|
||||
+674
@@ -0,0 +1,674 @@
|
||||
#include "Player.hh"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <wchar.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <phosg/Filesystem.hh>
|
||||
|
||||
#include "Text.hh"
|
||||
#include "Version.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
// originally there was going to be a language-based header, but then I decided against it.
|
||||
// these strings were already in use for that parser, so I didn't bother changing them.
|
||||
#define PLAYER_FILE_SIGNATURE "newserv player file format; 10 sections present; sequential;"
|
||||
#define ACCOUNT_FILE_SIGNATURE "newserv account file format; 7 sections present; sequential;"
|
||||
|
||||
|
||||
|
||||
// converts PC/GC player data to BB format
|
||||
PlayerDispDataBB PlayerDispDataPCGC::to_bb() const {
|
||||
PlayerDispDataBB bb;
|
||||
bb.stats.atp = this->stats.atp;
|
||||
bb.stats.mst = this->stats.mst;
|
||||
bb.stats.evp = this->stats.evp;
|
||||
bb.stats.hp = this->stats.hp;
|
||||
bb.stats.dfp = this->stats.dfp;
|
||||
bb.stats.ata = this->stats.ata;
|
||||
bb.stats.lck = this->stats.lck;
|
||||
bb.unknown1 = this->unknown1;
|
||||
bb.unknown2[0] = this->unknown2[0];
|
||||
bb.unknown2[1] = this->unknown2[1];
|
||||
bb.level = this->level;
|
||||
bb.experience = this->experience;
|
||||
bb.meseta = this->meseta;
|
||||
memset(bb.guild_card, 0, sizeof(bb.guild_card));
|
||||
strcpy(bb.guild_card, " 0");
|
||||
bb.unknown3[0] = this->unknown3[0];
|
||||
bb.unknown3[1] = this->unknown3[1];
|
||||
bb.name_color = this->name_color;
|
||||
bb.extra_model = this->extra_model;
|
||||
memcpy(&bb.unused, &this->unused, 15);
|
||||
bb.name_color_checksum = this->name_color_checksum;
|
||||
bb.section_id = this->section_id;
|
||||
bb.char_class = this->char_class;
|
||||
bb.v2_flags = this->v2_flags;
|
||||
bb.version = this->version;
|
||||
bb.v1_flags = this->v1_flags;
|
||||
bb.costume = this->costume;
|
||||
bb.skin = this->skin;
|
||||
bb.face = this->face;
|
||||
bb.head = this->head;
|
||||
bb.hair = this->hair;
|
||||
bb.hair_r = this->hair_r;
|
||||
bb.hair_g = this->hair_g;
|
||||
bb.hair_b = this->hair_b;
|
||||
bb.proportion_x = this->proportion_x;
|
||||
bb.proportion_y = this->proportion_y;
|
||||
memset(bb.name, 0, sizeof(bb.name));
|
||||
decode_sjis(bb.name, this->name, 0x10);
|
||||
add_language_marker_inplace(bb.name, 'J', 0x10);
|
||||
memcpy(&bb.config, &this->config, 0x48);
|
||||
memcpy(&bb.technique_levels, &this->technique_levels, 0x14);
|
||||
return bb;
|
||||
}
|
||||
|
||||
// converts BB player data to PC/GC format
|
||||
PlayerDispDataPCGC PlayerDispDataBB::to_pcgc() const {
|
||||
PlayerDispDataPCGC pcgc;
|
||||
pcgc.stats.atp = this->stats.atp;
|
||||
pcgc.stats.mst = this->stats.mst;
|
||||
pcgc.stats.evp = this->stats.evp;
|
||||
pcgc.stats.hp = this->stats.hp;
|
||||
pcgc.stats.dfp = this->stats.dfp;
|
||||
pcgc.stats.ata = this->stats.ata;
|
||||
pcgc.stats.lck = this->stats.lck;
|
||||
pcgc.unknown1 = this->unknown1;
|
||||
pcgc.unknown2[0] = this->unknown2[0];
|
||||
pcgc.unknown2[1] = this->unknown2[1];
|
||||
pcgc.level = this->level;
|
||||
pcgc.experience = this->experience;
|
||||
pcgc.meseta = this->meseta;
|
||||
pcgc.unknown3[0] = this->unknown3[0];
|
||||
pcgc.unknown3[1] = this->unknown3[1];
|
||||
pcgc.name_color = this->name_color;
|
||||
pcgc.extra_model = this->extra_model;
|
||||
memcpy(&pcgc.unused, &this->unused, 15);
|
||||
pcgc.name_color_checksum = this->name_color_checksum;
|
||||
pcgc.section_id = this->section_id;
|
||||
pcgc.char_class = this->char_class;
|
||||
pcgc.v2_flags = this->v2_flags;
|
||||
pcgc.version = this->version;
|
||||
pcgc.v1_flags = this->v1_flags;
|
||||
pcgc.costume = this->costume;
|
||||
pcgc.skin = this->skin;
|
||||
pcgc.face = this->face;
|
||||
pcgc.head = this->head;
|
||||
pcgc.hair = this->hair;
|
||||
pcgc.hair_r = this->hair_r;
|
||||
pcgc.hair_g = this->hair_g;
|
||||
pcgc.hair_b = this->hair_b;
|
||||
pcgc.proportion_x = this->proportion_x;
|
||||
pcgc.proportion_y = this->proportion_y;
|
||||
memset(pcgc.name, 0, sizeof(pcgc.name));
|
||||
encode_sjis(pcgc.name, this->name, 0x10);
|
||||
remove_language_marker_inplace(pcgc.name);
|
||||
memcpy(&pcgc.config, &this->config, 0x48);
|
||||
memcpy(&pcgc.technique_levels, &this->technique_levels, 0x14);
|
||||
return pcgc;
|
||||
}
|
||||
|
||||
// creates a player preview, which can then be sent to a BB client for character select
|
||||
PlayerDispDataBBPreview PlayerDispDataBB::to_preview() const {
|
||||
PlayerDispDataBBPreview pre;
|
||||
pre.level = this->level;
|
||||
pre.experience = this->experience;
|
||||
memset(pre.guild_card, 0, sizeof(pre.guild_card));
|
||||
strcpy(pre.guild_card, this->guild_card);
|
||||
pre.unknown3[0] = this->unknown3[0];
|
||||
pre.unknown3[1] = this->unknown3[1];
|
||||
pre.name_color = this->name_color;
|
||||
pre.extra_model = this->extra_model;
|
||||
memcpy(&pre.unused, &this->unused, 11);
|
||||
pre.name_color_checksum = this->name_color_checksum;
|
||||
pre.section_id = this->section_id;
|
||||
pre.char_class = this->char_class;
|
||||
pre.v2_flags = this->v2_flags;
|
||||
pre.version = this->version;
|
||||
pre.v1_flags = this->v1_flags;
|
||||
pre.costume = this->costume;
|
||||
pre.skin = this->skin;
|
||||
pre.face = this->face;
|
||||
pre.head = this->head;
|
||||
pre.hair = this->hair;
|
||||
pre.hair_r = this->hair_r;
|
||||
pre.hair_g = this->hair_g;
|
||||
pre.hair_b = this->hair_b;
|
||||
pre.proportion_x = this->proportion_x;
|
||||
pre.proportion_y = this->proportion_y;
|
||||
memset(pre.name, 0, sizeof(pre.name));
|
||||
char16cpy(pre.name, this->name, 16);
|
||||
pre.play_time = this->play_time;
|
||||
return pre;
|
||||
}
|
||||
|
||||
void PlayerDispDataBB::apply_preview(const PlayerDispDataBBPreview& pre) {
|
||||
this->level = pre.level;
|
||||
this->experience = pre.experience;
|
||||
memset(this->guild_card, 0, sizeof(this->guild_card));
|
||||
strcpy(this->guild_card, pre.guild_card);
|
||||
this->unknown3[0] = pre.unknown3[0];
|
||||
this->unknown3[1] = pre.unknown3[1];
|
||||
this->name_color = pre.name_color;
|
||||
this->extra_model = pre.extra_model;
|
||||
memcpy(&this->unused, &pre.unused, 11);
|
||||
this->name_color_checksum = pre.name_color_checksum;
|
||||
this->section_id = pre.section_id;
|
||||
this->char_class = pre.char_class;
|
||||
this->v2_flags = pre.v2_flags;
|
||||
this->version = pre.version;
|
||||
this->v1_flags = pre.v1_flags;
|
||||
this->costume = pre.costume;
|
||||
this->skin = pre.skin;
|
||||
this->face = pre.face;
|
||||
this->head = pre.head;
|
||||
this->hair = pre.hair;
|
||||
this->hair_r = pre.hair_r;
|
||||
this->hair_g = pre.hair_g;
|
||||
this->hair_b = pre.hair_b;
|
||||
this->proportion_x = pre.proportion_x;
|
||||
this->proportion_y = pre.proportion_y;
|
||||
memset(this->name, 0, sizeof(this->name));
|
||||
char16cpy(this->name, pre.name, 16);
|
||||
this->play_time = 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void PlayerBank::load(const string& filename) {
|
||||
*this = load_object_file<PlayerBank>(filename);
|
||||
for (uint32_t x = 0; x < this->num_items; x++) {
|
||||
this->items[x].data.item_id = 0x0F010000 + x;
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerBank::save(const string& filename) const {
|
||||
save_file(filename, this, sizeof(*this));
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Player::import(const PSOPlayerDataPC& pc) {
|
||||
this->inventory = pc.inventory;
|
||||
this->disp = pc.disp.to_bb();
|
||||
/* TODO: fix and re-enable this functionality
|
||||
memset(this->info_board, 0, sizeof(this->info_board));
|
||||
decode_sjis(this->info_board, pc->info_board);
|
||||
memcpy(&this->blocked, pc->blocked, sizeof(uint32_t) * 30);
|
||||
memset(this->auto_reply, 0, sizeof(this->auto_reply));
|
||||
if (pc->auto_reply_enabled) {
|
||||
decode_sjis(this->auto_reply, pc->auto_reply);
|
||||
} else {*/
|
||||
this->auto_reply[0] = 0;
|
||||
//}
|
||||
}
|
||||
|
||||
void Player::import(const PSOPlayerDataGC& gc) {
|
||||
this->inventory = gc.inventory;
|
||||
this->disp = gc.disp.to_bb();
|
||||
memset(this->info_board, 0, sizeof(this->info_board));
|
||||
decode_sjis(this->info_board, gc.info_board, 0xAC);
|
||||
memcpy(&this->blocked, gc.blocked, sizeof(uint32_t) * 30);
|
||||
memset(this->auto_reply, 0, sizeof(this->auto_reply));
|
||||
if (gc.auto_reply_enabled) {
|
||||
decode_sjis(this->auto_reply, gc.auto_reply, 0xAC);
|
||||
} else {
|
||||
this->auto_reply[0] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Player::import(const PSOPlayerDataBB& bb) {
|
||||
// note: we don't copy the inventory and disp here because we already have
|
||||
// it (we sent the player data to the client in the first place)
|
||||
memset(this->info_board, 0, sizeof(this->info_board));
|
||||
char16cpy(this->info_board, bb.info_board, 0xAC);
|
||||
memcpy(&this->blocked, bb.blocked, sizeof(uint32_t) * 30);
|
||||
memset(this->auto_reply, 0, sizeof(this->auto_reply));
|
||||
if (bb.auto_reply_enabled) {
|
||||
char16cpy(this->auto_reply, bb.auto_reply, 0xAC);
|
||||
} else {
|
||||
this->auto_reply[0] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// generates data for 65/67/68 commands (joining games/lobbies)
|
||||
PlayerLobbyJoinDataPCGC Player::export_lobby_data_pc() const {
|
||||
PlayerLobbyJoinDataPCGC pc;
|
||||
pc.inventory = this->inventory;
|
||||
pc.disp = this->disp.to_pcgc();
|
||||
|
||||
// PC has fewer classes, so we'll substitute some here
|
||||
if (pc.disp.char_class == 11) {
|
||||
pc.disp.char_class = 0; // fomar -> humar
|
||||
} else if (pc.disp.char_class == 10) {
|
||||
pc.disp.char_class = 1; // ramarl -> hunewearl
|
||||
} else if (pc.disp.char_class == 9) {
|
||||
pc.disp.char_class = 5; // hucaseal -> racaseal
|
||||
}
|
||||
|
||||
// if the player is still not a valid class, make them appear as the "ninja" NPC
|
||||
if (pc.disp.char_class > 8) {
|
||||
pc.disp.extra_model = 0;
|
||||
pc.disp.v2_flags |= 2;
|
||||
}
|
||||
pc.disp.version = 2;
|
||||
|
||||
return pc;
|
||||
}
|
||||
|
||||
PlayerLobbyJoinDataPCGC Player::export_lobby_data_gc() const {
|
||||
PlayerLobbyJoinDataPCGC gc;
|
||||
gc.inventory = this->inventory;
|
||||
gc.disp = this->disp.to_pcgc();
|
||||
return gc;
|
||||
}
|
||||
|
||||
PlayerLobbyJoinDataBB Player::export_lobby_data_bb() const {
|
||||
PlayerLobbyJoinDataBB bb;
|
||||
bb.inventory = this->inventory;
|
||||
bb.disp = this->disp;
|
||||
return bb;
|
||||
}
|
||||
|
||||
PlayerBB Player::export_bb_player_data() const {
|
||||
PlayerBB bb;
|
||||
bb.inventory = this->inventory;
|
||||
bb.disp = this->disp;
|
||||
memset(bb.unknown, 0, 0x10);
|
||||
bb.option_flags = this->option_flags;
|
||||
memcpy(bb.quest_data1, &this->quest_data1, 0x0208);
|
||||
bb.bank = this->bank;
|
||||
bb.serial_number = this->serial_number;
|
||||
memset(bb.name, 0, sizeof(bb.name));
|
||||
char16cpy(bb.name, this->disp.name, 24);
|
||||
memset(bb.team_name, 0, sizeof(bb.team_name));
|
||||
char16cpy(bb.team_name, this->team_name, 16);
|
||||
memset(bb.guild_card_desc, 0, sizeof(bb.guild_card_desc));
|
||||
char16cpy(bb.guild_card_desc, this->guild_card_desc, 0x58);
|
||||
bb.reserved1 = 0;
|
||||
bb.reserved2 = 0;
|
||||
bb.section_id = this->disp.section_id;
|
||||
bb.char_class = this->disp.char_class;
|
||||
bb.unknown3 = 0;
|
||||
memcpy(bb.symbol_chats, this->symbol_chats, 0x04E0);
|
||||
memcpy(bb.shortcuts, this->shortcuts, 0x0A40);
|
||||
memset(bb.auto_reply, 0, sizeof(bb.auto_reply));
|
||||
char16cpy(bb.auto_reply, this->auto_reply, 0xAC);
|
||||
memset(bb.info_board, 0, sizeof(bb.info_board));
|
||||
char16cpy(bb.info_board, this->info_board, 0xAC);
|
||||
memset(bb.unknown5, 0, 0x1C);
|
||||
memcpy(bb.challenge_data, this->challenge_data, 0x0140);
|
||||
memcpy(bb.tech_menu_config, this->tech_menu_config, 0x0028);
|
||||
memset(bb.unknown6, 0, 0x2C);
|
||||
memcpy(bb.quest_data2, &this->quest_data2, 0x0058);
|
||||
bb.key_config = this->key_config;
|
||||
return bb;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// checksums the guild card file for BB player account data
|
||||
uint32_t compute_guild_card_checksum(const void* vdata, size_t size) {
|
||||
const uint8_t* data = reinterpret_cast<const uint8_t*>(vdata);
|
||||
|
||||
uint32_t cs = 0xFFFFFFFF;
|
||||
for (size_t offset = 0; offset < size; offset++) {
|
||||
cs ^= data[offset];
|
||||
for (size_t y = 0; y < 8; y++) {
|
||||
if (!(cs & 1)) {
|
||||
cs = (cs >> 1) & 0x7FFFFFFF;
|
||||
} else {
|
||||
cs = ((cs >> 1) & 0x7FFFFFFF) ^ 0xEDB88320;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (cs ^ 0xFFFFFFFF);
|
||||
}
|
||||
|
||||
void Player::load_account_data(const string& filename) {
|
||||
SavedAccountBB account = load_object_file<SavedAccountBB>(filename);
|
||||
|
||||
if (strcmp(account.signature, ACCOUNT_FILE_SIGNATURE)) {
|
||||
throw runtime_error("account data header is incorrect");
|
||||
}
|
||||
|
||||
memcpy(&this->blocked, &account.blocked, sizeof(uint32_t) * 30);
|
||||
this->guild_cards = account.guild_cards;
|
||||
this->key_config = account.key_config;
|
||||
this->option_flags = account.option_flags;
|
||||
memcpy(&this->shortcuts, &account.shortcuts, 0x0A40);
|
||||
memcpy(&this->symbol_chats, &account.symbol_chats, 0x04E0);
|
||||
memset(this->team_name, 0, sizeof(this->team_name));
|
||||
char16cpy(this->team_name, account.team_name, 16);
|
||||
}
|
||||
|
||||
void Player::save_account_data(const string& filename) const {
|
||||
SavedAccountBB account;
|
||||
|
||||
strcpy(account.signature, ACCOUNT_FILE_SIGNATURE);
|
||||
memcpy(&account.blocked, &this->blocked, sizeof(uint32_t) * 30);
|
||||
account.guild_cards = this->guild_cards;
|
||||
account.key_config = this->key_config;
|
||||
account.option_flags = this->option_flags;
|
||||
memcpy(&account.shortcuts, &this->shortcuts, 0x0A40);
|
||||
memcpy(&account.symbol_chats, &this->symbol_chats, 0x04E0);
|
||||
memset(account.team_name, 0, sizeof(account.team_name));
|
||||
char16cpy(account.team_name, this->team_name, 16);
|
||||
|
||||
save_file(filename, &account, sizeof(account));
|
||||
}
|
||||
|
||||
void Player::load_player_data(const string& filename) {
|
||||
SavedPlayerBB player = load_object_file<SavedPlayerBB>(filename);
|
||||
|
||||
if (strcmp(player.signature, PLAYER_FILE_SIGNATURE)) {
|
||||
throw runtime_error("account data header is incorrect");
|
||||
}
|
||||
|
||||
memset(this->auto_reply, 0, sizeof(this->auto_reply));
|
||||
char16cpy(this->auto_reply, player.auto_reply, 0xAC);
|
||||
this->bank = player.bank;
|
||||
memcpy(&this->challenge_data, &player.challenge_data, 0x0140);
|
||||
this->disp = player.disp;
|
||||
memset(this->guild_card_desc, 0, sizeof(this->guild_card_desc));
|
||||
char16cpy(this->guild_card_desc, player.guild_card_desc, 0x58);
|
||||
memset(this->info_board, 0, sizeof(this->info_board));
|
||||
char16cpy(this->info_board, player.info_board, 0xAC);
|
||||
this->inventory = player.inventory;
|
||||
memcpy(&this->quest_data1, &player.quest_data1, 0x0208);
|
||||
memcpy(&this->quest_data2, &player.quest_data2, 0x0058);
|
||||
memcpy(&this->tech_menu_config, &player.tech_menu_config, 0x0028);
|
||||
}
|
||||
|
||||
void Player::save_player_data(const string& filename) const {
|
||||
SavedPlayerBB player;
|
||||
|
||||
strcpy(player.signature, PLAYER_FILE_SIGNATURE);
|
||||
player.preview = this->disp.to_preview();
|
||||
memset(player.auto_reply, 0, sizeof(player.auto_reply));
|
||||
char16cpy(player.auto_reply, this->auto_reply, 0xAC);
|
||||
player.bank = this->bank;
|
||||
memcpy(&player.challenge_data, &this->challenge_data, 0x0140);
|
||||
player.disp = this->disp;
|
||||
memset(player.guild_card_desc, 0, sizeof(player.guild_card_desc));
|
||||
char16cpy(player.guild_card_desc,this->guild_card_desc, 0x58);
|
||||
memset(player.info_board, 0, sizeof(player.info_board));
|
||||
char16cpy(player.info_board, this->info_board, 0xAC);
|
||||
player.inventory = this->inventory;
|
||||
memcpy(&player.quest_data1, &this->quest_data1, 0x0208);
|
||||
memcpy(&player.quest_data2, &this->quest_data2, 0x0058);
|
||||
memcpy(&player.tech_menu_config, &this->tech_menu_config, 0x0028);
|
||||
|
||||
save_file(filename, &player, sizeof(player));
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static const unordered_map<uint32_t, uint32_t> combine_item_to_max({
|
||||
{0x030000, 10},
|
||||
{0x030001, 10},
|
||||
{0x030002, 10},
|
||||
{0x030100, 10},
|
||||
{0x030101, 10},
|
||||
{0x030102, 10},
|
||||
{0x030300, 10},
|
||||
{0x030400, 10},
|
||||
{0x030500, 10},
|
||||
{0x030600, 10},
|
||||
{0x030601, 10},
|
||||
{0x030700, 10},
|
||||
{0x030800, 10},
|
||||
{0x031000, 99},
|
||||
{0x031001, 99},
|
||||
{0x031002, 99},
|
||||
});
|
||||
|
||||
const uint32_t meseta_identifier = 0x00000004;
|
||||
|
||||
uint32_t ItemData::primary_identifier() const {
|
||||
return (this->item_data1[0] << 16) | (this->item_data1[1] << 8) | this->item_data1[2];
|
||||
}
|
||||
|
||||
PlayerBankItem PlayerInventoryItem::to_bank_item() const {
|
||||
PlayerBankItem bank_item;
|
||||
bank_item.data = this->data;
|
||||
|
||||
if (combine_item_to_max.count(this->data.primary_identifier())) {
|
||||
bank_item.amount = this->data.item_data1[5];
|
||||
} else {
|
||||
bank_item.amount = 1;
|
||||
}
|
||||
bank_item.show_flags = 1;
|
||||
|
||||
return bank_item;
|
||||
}
|
||||
|
||||
PlayerInventoryItem PlayerBankItem::to_inventory_item() const {
|
||||
PlayerInventoryItem item;
|
||||
item.data = this->data;
|
||||
if (item.data.item_data1[0] > 2) {
|
||||
item.equip_flags = 0x0044;
|
||||
} else {
|
||||
item.equip_flags = 0x0050;
|
||||
}
|
||||
item.equip_flags = 0x0001; // TODO: is this a bug?
|
||||
item.tech_flag = 0x0001;
|
||||
return item;
|
||||
}
|
||||
|
||||
void Player::add_item(const PlayerInventoryItem& item) {
|
||||
uint32_t pid = item.data.primary_identifier();
|
||||
|
||||
// is it meseta? then just add to the meseta total
|
||||
if (pid == meseta_identifier) {
|
||||
this->disp.meseta += item.data.item_data2d;
|
||||
if (this->disp.meseta > 999999) {
|
||||
this->disp.meseta = 999999;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// is it a combine item?
|
||||
try {
|
||||
uint32_t combine_max = combine_item_to_max.at(pid);
|
||||
|
||||
// is there already a stack of it in the player's inventory?
|
||||
size_t y;
|
||||
for (y = 0; y < this->inventory.num_items; y++) {
|
||||
if (this->inventory.items[y].data.primary_identifier() == item.data.primary_identifier()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if there's already a stack, add to the stack and return
|
||||
if (y < this->inventory.num_items) {
|
||||
this->inventory.items[y].data.item_data1[5] += item.data.item_data1[5];
|
||||
if (this->inventory.items[y].data.item_data1[5] > combine_max) {
|
||||
this->inventory.items[y].data.item_data1[5] = combine_max;
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (const out_of_range&) { }
|
||||
|
||||
// else, just add the item if there's room
|
||||
if (this->inventory.num_items >= 30) {
|
||||
throw runtime_error("inventory is full");
|
||||
}
|
||||
this->inventory.items[this->inventory.num_items] = item;
|
||||
this->inventory.num_items++;
|
||||
}
|
||||
|
||||
// adds an item to a bank
|
||||
void PlayerBank::add_item(const PlayerBankItem& item) {
|
||||
uint32_t pid = item.data.primary_identifier();
|
||||
|
||||
// is it meseta? then just add to the meseta total
|
||||
if (pid == meseta_identifier) {
|
||||
this->meseta += item.data.item_data2d;
|
||||
if (this->meseta > 999999) {
|
||||
this->meseta = 999999;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// is it a combine item?
|
||||
try {
|
||||
uint32_t combine_max = combine_item_to_max.at(pid);
|
||||
|
||||
// is there already a stack of it in the player's inventory?
|
||||
size_t y;
|
||||
for (y = 0; y < this->num_items; y++) {
|
||||
if (this->items[y].data.primary_identifier() == item.data.primary_identifier()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if there's already a stack, add to the stack and return
|
||||
if (y < this->num_items) {
|
||||
this->items[y].data.item_data1[5] += item.data.item_data1[5];
|
||||
if (this->items[y].data.item_data1[5] > combine_max) {
|
||||
this->items[y].data.item_data1[5] = combine_max;
|
||||
}
|
||||
this->items[y].amount = this->items[y].data.item_data1[5];
|
||||
return;
|
||||
}
|
||||
} catch (const out_of_range&) { }
|
||||
|
||||
// else, just add the item if there's room
|
||||
if (this->num_items >= 200) {
|
||||
throw runtime_error("bank is full");
|
||||
}
|
||||
this->items[this->num_items] = item;
|
||||
this->num_items++;
|
||||
}
|
||||
|
||||
void Player::remove_item(uint32_t item_id, uint32_t amount,
|
||||
PlayerInventoryItem* item) {
|
||||
|
||||
// are we removing meseta? then create a meseta item
|
||||
if (item_id == 0xFFFFFFFF) {
|
||||
if (amount > this->disp.meseta) {
|
||||
throw out_of_range("player does not have enough meseta");
|
||||
}
|
||||
if (item) {
|
||||
memset(item, 0, sizeof(*item));
|
||||
item->data.item_data1[0] = 0x04;
|
||||
item->data.item_data2d = amount;
|
||||
}
|
||||
this->disp.meseta -= amount;
|
||||
return;
|
||||
}
|
||||
|
||||
// find this item
|
||||
size_t index = this->inventory.find_item(item_id);
|
||||
auto& inventory_item = this->inventory.items[index];
|
||||
|
||||
// is it a combine item, and are we removing less than we have of it?
|
||||
// (amount == 0 means remove all of it)
|
||||
if (amount && (amount < inventory_item.data.item_data1[5]) &&
|
||||
combine_item_to_max.count(inventory_item.data.primary_identifier())) {
|
||||
if (item) {
|
||||
*item = inventory_item;
|
||||
item->data.item_data1[5] = amount;
|
||||
item->data.item_id = 0xFFFFFFFF;
|
||||
}
|
||||
inventory_item.data.item_data1[5] -= amount;
|
||||
return;
|
||||
}
|
||||
|
||||
// not a combine item, or we're removing the whole stack? then just remove the item
|
||||
if (item) {
|
||||
*item = inventory_item;
|
||||
}
|
||||
this->inventory.num_items--;
|
||||
memcpy(&this->inventory.items[index], &this->inventory.items[index + 1],
|
||||
sizeof(PlayerInventoryItem) * (this->inventory.num_items - index));
|
||||
}
|
||||
|
||||
// removes an item from a bank. works just like RemoveItem for inventories; I won't comment it
|
||||
void PlayerBank::remove_item(uint32_t item_id, uint32_t amount,
|
||||
PlayerBankItem* item) {
|
||||
|
||||
// are we removing meseta? then create a meseta item
|
||||
if (item_id == 0xFFFFFFFF) {
|
||||
if (amount > this->meseta) {
|
||||
throw out_of_range("player does not have enough meseta");
|
||||
}
|
||||
if (item) {
|
||||
memset(item, 0, sizeof(*item));
|
||||
item->data.item_data1[0] = 0x04;
|
||||
item->data.item_data2d = amount;
|
||||
}
|
||||
this->meseta -= amount;
|
||||
return;
|
||||
}
|
||||
|
||||
// find this item
|
||||
size_t index = this->find_item(item_id);
|
||||
auto& bank_item = this->items[index];
|
||||
|
||||
// is it a combine item, and are we removing less than we have of it?
|
||||
// (amount == 0 means remove all of it)
|
||||
if (amount && (amount < bank_item.data.item_data1[5]) &&
|
||||
combine_item_to_max.count(bank_item.data.primary_identifier())) {
|
||||
if (item) {
|
||||
*item = bank_item;
|
||||
item->data.item_data1[5] = amount;
|
||||
item->amount = amount;
|
||||
}
|
||||
bank_item.data.item_data1[5] -= amount;
|
||||
bank_item.amount -= amount;
|
||||
return;
|
||||
}
|
||||
|
||||
// not a combine item, or we're removing the whole stack? then just remove the item
|
||||
if (item) {
|
||||
*item = bank_item;
|
||||
}
|
||||
this->num_items--;
|
||||
memcpy(&this->items[index], &this->items[index + 1],
|
||||
sizeof(PlayerBankItem) * (this->num_items - index));
|
||||
}
|
||||
|
||||
size_t PlayerInventory::find_item(uint32_t item_id) {
|
||||
for (size_t x = 0; x < this->num_items; x++) {
|
||||
if (this->items[x].data.item_id == item_id) {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
throw out_of_range("item not present");
|
||||
}
|
||||
|
||||
size_t PlayerBank::find_item(uint32_t item_id) {
|
||||
for (size_t x = 0; x < this->num_items; x++) {
|
||||
if (this->items[x].data.item_id == item_id) {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
throw out_of_range("item not present");
|
||||
}
|
||||
|
||||
string filename_for_player_bb(const string& username, uint8_t player_index) {
|
||||
return string_printf("system/players/player_%s_%ld.nsc", username.c_str(),
|
||||
player_index + 1);
|
||||
}
|
||||
|
||||
string filename_for_bank_bb(const string& username, const char* bank_name) {
|
||||
return string_printf("system/players/bank_%s_%s.nsb", username.c_str(),
|
||||
bank_name);
|
||||
}
|
||||
|
||||
string filename_for_class_template_bb(uint8_t char_class) {
|
||||
return string_printf("system/blueburst/player_class_%hhu.nsc", char_class);
|
||||
}
|
||||
|
||||
string filename_for_account_bb(const string& username) {
|
||||
return string_printf("system/players/account_%s.nsa", username.c_str());
|
||||
}
|
||||
+435
@@ -0,0 +1,435 @@
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Version.hh"
|
||||
|
||||
|
||||
|
||||
// raw item data
|
||||
struct ItemData {
|
||||
union {
|
||||
uint8_t item_data1[12];
|
||||
uint16_t item_data1w[6];
|
||||
uint32_t item_data1d[3];
|
||||
};
|
||||
uint32_t item_id;
|
||||
union {
|
||||
uint8_t item_data2[4];
|
||||
uint16_t item_data2w[2];
|
||||
uint32_t item_data2d;
|
||||
};
|
||||
|
||||
uint32_t primary_identifier() const;
|
||||
};
|
||||
|
||||
struct PlayerBankItem;
|
||||
|
||||
// an item in a player's inventory
|
||||
struct PlayerInventoryItem {
|
||||
uint16_t equip_flags;
|
||||
uint16_t tech_flag;
|
||||
uint32_t game_flags;
|
||||
ItemData data;
|
||||
|
||||
PlayerBankItem to_bank_item() const;
|
||||
};
|
||||
|
||||
// an item in a player's bank
|
||||
struct PlayerBankItem {
|
||||
ItemData data;
|
||||
uint16_t amount;
|
||||
uint16_t show_flags;
|
||||
|
||||
PlayerInventoryItem to_inventory_item() const;
|
||||
};
|
||||
|
||||
// a player's inventory (remarkably, the format is the same in all versions of PSO)
|
||||
struct PlayerInventory {
|
||||
uint8_t num_items;
|
||||
uint8_t hp_materials_used;
|
||||
uint8_t tp_materials_used;
|
||||
uint8_t language;
|
||||
PlayerInventoryItem items[30];
|
||||
|
||||
size_t find_item(uint32_t item_id);
|
||||
};
|
||||
|
||||
// a player's bank
|
||||
struct PlayerBank {
|
||||
uint32_t num_items;
|
||||
uint32_t meseta;
|
||||
PlayerBankItem items[200];
|
||||
|
||||
void load(const std::string& filename);
|
||||
void save(const std::string& filename) const;
|
||||
|
||||
bool switch_with_file(const char* save_filename, const char* load_filename);
|
||||
|
||||
void add_item(const PlayerBankItem& item);
|
||||
void remove_item(uint32_t item_id, uint32_t amount, PlayerBankItem* item);
|
||||
size_t find_item(uint32_t item_id);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// simple player stats
|
||||
struct PlayerStats {
|
||||
uint16_t atp;
|
||||
uint16_t mst;
|
||||
uint16_t evp;
|
||||
uint16_t hp;
|
||||
uint16_t dfp;
|
||||
uint16_t ata;
|
||||
uint16_t lck;
|
||||
};
|
||||
|
||||
struct PlayerDispDataBB;
|
||||
|
||||
// PC/GC player appearance and stats data
|
||||
struct PlayerDispDataPCGC { // 0xD0 in size
|
||||
PlayerStats stats;
|
||||
uint16_t unknown1;
|
||||
uint32_t unknown2[2];
|
||||
uint32_t level;
|
||||
uint32_t experience;
|
||||
uint32_t meseta;
|
||||
char name[16];
|
||||
uint32_t unknown3[2];
|
||||
uint32_t name_color;
|
||||
uint8_t extra_model;
|
||||
uint8_t unused[15];
|
||||
uint32_t name_color_checksum;
|
||||
uint8_t section_id;
|
||||
uint8_t char_class;
|
||||
uint8_t v2_flags;
|
||||
uint8_t version;
|
||||
uint32_t v1_flags;
|
||||
uint16_t costume;
|
||||
uint16_t skin;
|
||||
uint16_t face;
|
||||
uint16_t head;
|
||||
uint16_t hair;
|
||||
uint16_t hair_r;
|
||||
uint16_t hair_g;
|
||||
uint16_t hair_b;
|
||||
float proportion_x;
|
||||
float proportion_y;
|
||||
uint8_t config[0x48];
|
||||
uint8_t technique_levels[0x14];
|
||||
|
||||
PlayerDispDataBB to_bb() const;
|
||||
};
|
||||
|
||||
// BB player preview format
|
||||
struct PlayerDispDataBBPreview {
|
||||
uint32_t experience;
|
||||
uint32_t level;
|
||||
char guild_card[16];
|
||||
uint32_t unknown3[2];
|
||||
uint32_t name_color;
|
||||
uint8_t extra_model;
|
||||
uint8_t unused[15];
|
||||
uint32_t name_color_checksum;
|
||||
uint8_t section_id;
|
||||
uint8_t char_class;
|
||||
uint8_t v2_flags;
|
||||
uint8_t version;
|
||||
uint32_t v1_flags;
|
||||
uint16_t costume;
|
||||
uint16_t skin;
|
||||
uint16_t face;
|
||||
uint16_t head;
|
||||
uint16_t hair;
|
||||
uint16_t hair_r;
|
||||
uint16_t hair_g;
|
||||
uint16_t hair_b;
|
||||
float proportion_x;
|
||||
float proportion_y;
|
||||
char16_t name[16];
|
||||
uint32_t play_time;
|
||||
};
|
||||
|
||||
// BB player appearance and stats data
|
||||
struct PlayerDispDataBB {
|
||||
PlayerStats stats;
|
||||
uint16_t unknown1;
|
||||
uint32_t unknown2[2];
|
||||
uint32_t level;
|
||||
uint32_t experience;
|
||||
uint32_t meseta;
|
||||
char guild_card[16];
|
||||
uint32_t unknown3[2];
|
||||
uint32_t name_color;
|
||||
uint8_t extra_model;
|
||||
uint8_t unused[11];
|
||||
uint32_t play_time; // not actually a game field; used only by my server
|
||||
uint32_t name_color_checksum;
|
||||
uint8_t section_id;
|
||||
uint8_t char_class;
|
||||
uint8_t v2_flags;
|
||||
uint8_t version;
|
||||
uint32_t v1_flags;
|
||||
uint16_t costume;
|
||||
uint16_t skin;
|
||||
uint16_t face;
|
||||
uint16_t head;
|
||||
uint16_t hair;
|
||||
uint16_t hair_r;
|
||||
uint16_t hair_g;
|
||||
uint16_t hair_b;
|
||||
float proportion_x;
|
||||
float proportion_y;
|
||||
char16_t name[0x10];
|
||||
uint8_t config[0xE8];
|
||||
uint8_t technique_levels[0x14];
|
||||
|
||||
PlayerDispDataPCGC to_pcgc() const;
|
||||
PlayerDispDataBBPreview to_preview() const;
|
||||
void apply_preview(const PlayerDispDataBBPreview&);
|
||||
};
|
||||
|
||||
|
||||
|
||||
struct GuildCardGC {
|
||||
uint32_t player_tag;
|
||||
uint32_t serial_number;
|
||||
char name[0x18];
|
||||
char desc[0x6C];
|
||||
uint8_t reserved1; // should be 1
|
||||
uint8_t reserved2; // should be 1
|
||||
uint8_t section_id;
|
||||
uint8_t char_class;
|
||||
};
|
||||
|
||||
// BB guild card format
|
||||
struct GuildCardBB {
|
||||
uint32_t serial_number;
|
||||
char16_t name[0x18];
|
||||
char16_t teamname[0x10];
|
||||
char16_t desc[0x58];
|
||||
uint8_t reserved1; // should be 1
|
||||
uint8_t reserved2; // should be 1
|
||||
uint8_t section_id;
|
||||
uint8_t char_class;
|
||||
};
|
||||
|
||||
// an entry in the BB guild card file
|
||||
struct GuildCardEntryBB {
|
||||
GuildCardBB data;
|
||||
uint8_t unknown[0xB4];
|
||||
};
|
||||
|
||||
// the format of the BB guild card file
|
||||
struct GuildCardFileBB {
|
||||
uint8_t unknown[0x1F84];
|
||||
GuildCardEntryBB entry[0x0068]; // that's 104 of them in decimal
|
||||
uint8_t unknown2[0x01AC];
|
||||
};
|
||||
|
||||
// PSOBB key config and team info
|
||||
struct KeyAndTeamConfigBB {
|
||||
uint8_t unknown[0x0114]; // 0000
|
||||
uint8_t key_config[0x016C]; // 0114
|
||||
uint8_t joystick_config[0x0038]; // 0280
|
||||
uint32_t serial_number; // 02B8
|
||||
uint32_t team_id; // 02BC
|
||||
uint32_t team_info[2]; // 02C0
|
||||
uint16_t team_privilege_level; // 02C8
|
||||
uint16_t reserved; // 02CA
|
||||
char16_t team_name[0x0010]; // 02CC
|
||||
uint8_t team_flag[0x0800]; // 02EC
|
||||
uint32_t team_rewards[2]; // 0AEC
|
||||
};
|
||||
|
||||
// BB account data
|
||||
struct PlayerAccountDataBB {
|
||||
uint8_t symbol_chats[0x04E0];
|
||||
KeyAndTeamConfigBB key_config;
|
||||
GuildCardFileBB guild_cards;
|
||||
uint32_t options;
|
||||
uint8_t shortcuts[0x0A40]; // chat shortcuts (@1FB4 in E7 command)
|
||||
};
|
||||
|
||||
|
||||
|
||||
struct PlayerLobbyDataPC {
|
||||
uint32_t player_tag;
|
||||
uint32_t guild_card;
|
||||
uint32_t ip_address;
|
||||
uint32_t client_id;
|
||||
char16_t name[16];
|
||||
};
|
||||
|
||||
struct PlayerLobbyDataGC {
|
||||
uint32_t player_tag;
|
||||
uint32_t guild_card;
|
||||
uint32_t ip_address;
|
||||
uint32_t client_id;
|
||||
char name[16];
|
||||
};
|
||||
|
||||
struct PlayerLobbyDataBB {
|
||||
uint32_t player_tag;
|
||||
uint32_t guild_card;
|
||||
uint32_t unknown1[5];
|
||||
uint32_t client_id;
|
||||
char16_t name[16];
|
||||
uint32_t unknown2;
|
||||
};
|
||||
|
||||
|
||||
|
||||
struct PSOPlayerDataPC { // for command 0x61
|
||||
PlayerInventory inventory;
|
||||
PlayerDispDataPCGC disp;
|
||||
};
|
||||
|
||||
struct PSOPlayerDataGC { // for command 0x61
|
||||
PlayerInventory inventory;
|
||||
PlayerDispDataPCGC disp;
|
||||
char unknown[0x134];
|
||||
char info_board[0xAC];
|
||||
uint32_t blocked[0x1E];
|
||||
uint32_t auto_reply_enabled;
|
||||
char auto_reply[0];
|
||||
};
|
||||
|
||||
struct PSOPlayerDataBB { // for command 0x61
|
||||
PlayerInventory inventory;
|
||||
PlayerDispDataBB disp;
|
||||
char unused[0x174];
|
||||
char16_t info_board[0xAC];
|
||||
uint32_t blocked[0x1E];
|
||||
uint32_t auto_reply_enabled;
|
||||
char16_t auto_reply[0];
|
||||
};
|
||||
|
||||
// PC/GC lobby player data (used in lobby/game join commands)
|
||||
struct PlayerLobbyJoinDataPCGC {
|
||||
PlayerInventory inventory;
|
||||
PlayerDispDataPCGC disp;
|
||||
};
|
||||
|
||||
// BB lobby player data (used in lobby/game join commands)
|
||||
struct PlayerLobbyJoinDataBB {
|
||||
PlayerInventory inventory;
|
||||
PlayerDispDataBB disp;
|
||||
};
|
||||
|
||||
// complete BB player data format (used in E7 command)
|
||||
struct PlayerBB {
|
||||
PlayerInventory inventory; // 0000 // player
|
||||
PlayerDispDataBB disp; // 034C // player
|
||||
uint8_t unknown[0x0010]; // 04DC //
|
||||
uint32_t option_flags; // 04EC // account
|
||||
uint8_t quest_data1[0x0208]; // 04F0 // player
|
||||
PlayerBank bank; // 06F8 // player
|
||||
uint32_t serial_number; // 19C0 // player
|
||||
char16_t name[0x18]; // 19C4 // player
|
||||
char16_t team_name[0x10]; // 19C4 // player
|
||||
char16_t guild_card_desc[0x58]; // 1A14 // player
|
||||
uint8_t reserved1; // 1AC4 // player
|
||||
uint8_t reserved2; // 1AC5 // player
|
||||
uint8_t section_id; // 1AC6 // player
|
||||
uint8_t char_class; // 1AC7 // player
|
||||
uint32_t unknown3; // 1AC8 //
|
||||
uint8_t symbol_chats[0x04E0]; // 1ACC // account
|
||||
uint8_t shortcuts[0x0A40]; // 1FAC // account
|
||||
char16_t auto_reply[0x00AC]; // 29EC // player
|
||||
char16_t info_board[0x00AC]; // 2B44 // player
|
||||
uint8_t unknown5[0x001C]; // 2C9C //
|
||||
uint8_t challenge_data[0x0140]; // 2CB8 // player
|
||||
uint8_t tech_menu_config[0x0028]; // 2DF8 // player
|
||||
uint8_t unknown6[0x002C]; // 2E20 //
|
||||
uint8_t quest_data2[0x0058]; // 2E4C // player
|
||||
KeyAndTeamConfigBB key_config; // 2EA4 // account
|
||||
}; // total size: 39A0
|
||||
|
||||
|
||||
|
||||
struct SavedPlayerBB { // .nsc file format
|
||||
char signature[0x40];
|
||||
PlayerDispDataBBPreview preview;
|
||||
|
||||
char16_t auto_reply[0x00AC];
|
||||
PlayerBank bank;
|
||||
uint8_t challenge_data[0x0140];
|
||||
PlayerDispDataBB disp;
|
||||
char16_t guild_card_desc[0x58];
|
||||
char16_t info_board[0x00AC];
|
||||
PlayerInventory inventory;
|
||||
uint8_t quest_data1[0x0208];
|
||||
uint8_t quest_data2[0x0058];
|
||||
uint8_t tech_menu_config[0x0028];
|
||||
};
|
||||
|
||||
struct SavedAccountBB { // .nsa file format
|
||||
char signature[0x40];
|
||||
uint32_t blocked[0x001E];
|
||||
GuildCardFileBB guild_cards;
|
||||
KeyAndTeamConfigBB key_config;
|
||||
uint32_t option_flags;
|
||||
uint8_t shortcuts[0x0A40];
|
||||
uint8_t symbol_chats[0x04E0];
|
||||
char16_t team_name[0x0010];
|
||||
};
|
||||
|
||||
// complete player info stored by the server
|
||||
struct Player {
|
||||
uint32_t loaded_from_shipgate_time;
|
||||
|
||||
char16_t auto_reply[0x00AC]; // player
|
||||
PlayerBank bank; // player
|
||||
char bank_name[0x20];
|
||||
uint32_t blocked[0x001E]; // account
|
||||
uint8_t challenge_data[0x0140]; // player
|
||||
PlayerDispDataBB disp; // player
|
||||
uint8_t ep3_config[0x2408];
|
||||
char16_t guild_card_desc[0x58]; // player
|
||||
GuildCardFileBB guild_cards; // account
|
||||
PlayerInventoryItem identify_result;
|
||||
char16_t info_board[0x00AC]; // player
|
||||
PlayerInventory inventory; // player
|
||||
KeyAndTeamConfigBB key_config; // account
|
||||
uint32_t option_flags; // account
|
||||
uint8_t quest_data1[0x0208]; // player
|
||||
uint8_t quest_data2[0x0058]; // player
|
||||
uint32_t serial_number;
|
||||
std::vector<ItemData> current_shop_contents;
|
||||
uint8_t shortcuts[0x0A40]; // account
|
||||
uint8_t symbol_chats[0x04E0]; // account
|
||||
char16_t team_name[0x0010]; // account
|
||||
uint8_t tech_menu_config[0x0028]; // player
|
||||
|
||||
void load_player_data(const std::string& filename);
|
||||
void save_player_data(const std::string& filename) const;
|
||||
|
||||
void load_account_data(const std::string& filename);
|
||||
void save_account_data(const std::string& filename) const;
|
||||
|
||||
void import(const PSOPlayerDataPC& pd);
|
||||
void import(const PSOPlayerDataGC& pd);
|
||||
void import(const PSOPlayerDataBB& pd);
|
||||
PlayerLobbyJoinDataPCGC export_lobby_data_pc() const;
|
||||
PlayerLobbyJoinDataPCGC export_lobby_data_gc() const;
|
||||
PlayerLobbyJoinDataBB export_lobby_data_bb() const;
|
||||
PlayerBB export_bb_player_data() const;
|
||||
|
||||
void add_item(const PlayerInventoryItem& item);
|
||||
void remove_item(uint32_t item_id, uint32_t amount, PlayerInventoryItem* item);
|
||||
size_t find_item(uint32_t item_id);
|
||||
};
|
||||
|
||||
|
||||
|
||||
uint32_t compute_guild_card_checksum(const void* data, size_t size);
|
||||
|
||||
std::string filename_for_player_bb(const std::string& username, uint8_t player_index);
|
||||
std::string filename_for_bank_bb(const std::string& username, const char* bank_name);
|
||||
std::string filename_for_class_template_bb(uint8_t char_class);
|
||||
std::string filename_for_account_bb(const std::string& username);
|
||||
@@ -0,0 +1,394 @@
|
||||
#include "ProxyServer.hh"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/bufferevent.h>
|
||||
#include <event2/event.h>
|
||||
#include <event2/listener.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/Network.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "PSOProtocol.hh"
|
||||
#include "ReceiveCommands.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
ProxyServer::ProxyServer(shared_ptr<struct event_base> base,
|
||||
const struct sockaddr_storage& initial_destination, GameVersion version) :
|
||||
base(base), client_bev(nullptr, bufferevent_free),
|
||||
server_bev(nullptr, bufferevent_free),
|
||||
next_destination(initial_destination), version(version),
|
||||
header_size((version == GameVersion::BB) ? 8 : 4) {
|
||||
memset(&this->client_input_header, 0, sizeof(this->client_input_header));
|
||||
memset(&this->server_input_header, 0, sizeof(this->server_input_header));
|
||||
}
|
||||
|
||||
void ProxyServer::listen(int port) {
|
||||
unique_ptr<struct evconnlistener, void(*)(struct evconnlistener*)> listener(
|
||||
evconnlistener_new(this->base.get(),
|
||||
&ProxyServer::dispatch_on_listen_accept, this,
|
||||
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 0,
|
||||
::listen("", port, SOMAXCONN)), evconnlistener_free);
|
||||
this->listeners.emplace(port, move(listener));
|
||||
}
|
||||
|
||||
|
||||
|
||||
void ProxyServer::send_to_client(const std::string& data) {
|
||||
this->send_to_end(data, false);
|
||||
}
|
||||
|
||||
void ProxyServer::send_to_server(const std::string& data) {
|
||||
this->send_to_end(data, true);
|
||||
}
|
||||
|
||||
void ProxyServer::send_to_end(const std::string& data, bool to_server) {
|
||||
struct bufferevent* bev = to_server ? this->server_bev.get() : this->client_bev.get();
|
||||
if (!bev) {
|
||||
throw runtime_error("connection not open");
|
||||
}
|
||||
|
||||
struct evbuffer* buf = bufferevent_get_output(bev);
|
||||
|
||||
PSOEncryption* crypt = to_server ? this->server_output_crypt.get() : this->client_output_crypt.get();
|
||||
if (crypt) {
|
||||
string crypted_data = data;
|
||||
crypt->encrypt(const_cast<char*>(crypted_data.data()), crypted_data.size());
|
||||
evbuffer_add(buf, crypted_data.data(), crypted_data.size());
|
||||
} else {
|
||||
evbuffer_add(buf, data.data(), data.size());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void ProxyServer::dispatch_on_listen_accept(
|
||||
struct evconnlistener* listener, evutil_socket_t fd,
|
||||
struct sockaddr* address, int socklen, void* ctx) {
|
||||
reinterpret_cast<ProxyServer*>(ctx)->on_listen_accept(listener, fd, address,
|
||||
socklen);
|
||||
}
|
||||
|
||||
void ProxyServer::dispatch_on_listen_error(struct evconnlistener* listener,
|
||||
void* ctx) {
|
||||
reinterpret_cast<ProxyServer*>(ctx)->on_listen_error(listener);
|
||||
}
|
||||
|
||||
void ProxyServer::dispatch_on_client_input(struct bufferevent* bev, void* ctx) {
|
||||
reinterpret_cast<ProxyServer*>(ctx)->on_client_input(bev);
|
||||
}
|
||||
|
||||
void ProxyServer::dispatch_on_client_error(struct bufferevent* bev, short events,
|
||||
void* ctx) {
|
||||
reinterpret_cast<ProxyServer*>(ctx)->on_client_error(bev, events);
|
||||
}
|
||||
|
||||
void ProxyServer::dispatch_on_server_input(struct bufferevent* bev, void* ctx) {
|
||||
reinterpret_cast<ProxyServer*>(ctx)->on_server_input(bev);
|
||||
}
|
||||
|
||||
void ProxyServer::dispatch_on_server_error(struct bufferevent* bev, short events,
|
||||
void* ctx) {
|
||||
reinterpret_cast<ProxyServer*>(ctx)->on_server_error(bev, events);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void ProxyServer::on_listen_accept(struct evconnlistener*, evutil_socket_t fd,
|
||||
struct sockaddr*, int) {
|
||||
|
||||
if (this->client_bev.get()) {
|
||||
log(WARNING, "ignoring client connection because client already exists");
|
||||
close(fd);
|
||||
return;
|
||||
} else {
|
||||
log(INFO, "client connected");
|
||||
}
|
||||
|
||||
this->client_bev.reset(bufferevent_socket_new(this->base.get(), fd,
|
||||
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS));
|
||||
|
||||
bufferevent_setcb(this->client_bev.get(),
|
||||
&ProxyServer::dispatch_on_client_input, NULL,
|
||||
&ProxyServer::dispatch_on_client_error, this);
|
||||
bufferevent_enable(this->client_bev.get(), EV_READ | EV_WRITE);
|
||||
|
||||
// connect to the server, disconnecting first if needed
|
||||
this->server_bev.reset(bufferevent_socket_new(this->base.get(), -1,
|
||||
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS));
|
||||
|
||||
// TODO: figure out why this copy is necessary... shouldn't we just be able to
|
||||
// use the sockaddr_storage directly?
|
||||
const struct sockaddr_in* sin_ss = reinterpret_cast<const sockaddr_in*>(&this->next_destination);
|
||||
if (sin_ss->sin_family != AF_INET) {
|
||||
throw logic_error("ss not AF_INET");
|
||||
}
|
||||
struct sockaddr_in sin;
|
||||
memset(&sin, 0, sizeof(sin));
|
||||
sin.sin_family = AF_INET;
|
||||
sin.sin_port = sin_ss->sin_port;
|
||||
sin.sin_addr.s_addr = sin_ss->sin_addr.s_addr;
|
||||
|
||||
string netloc_str = render_sockaddr_storage(this->next_destination);
|
||||
log(INFO, "connecting to %s", netloc_str.c_str());
|
||||
if (bufferevent_socket_connect(this->server_bev.get(),
|
||||
reinterpret_cast<const sockaddr*>(&sin), sizeof(sin)) != 0) {
|
||||
throw runtime_error(string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR()));
|
||||
}
|
||||
bufferevent_setcb(this->server_bev.get(),
|
||||
&ProxyServer::dispatch_on_server_input, NULL,
|
||||
&ProxyServer::dispatch_on_server_error, this);
|
||||
bufferevent_enable(this->server_bev.get(), EV_READ | EV_WRITE);
|
||||
}
|
||||
|
||||
void ProxyServer::on_listen_error(struct evconnlistener* listener) {
|
||||
int err = EVUTIL_SOCKET_ERROR();
|
||||
log(ERROR, "failure on listening socket %d: %d (%s)",
|
||||
evconnlistener_get_fd(listener), err, evutil_socket_error_to_string(err));
|
||||
event_base_loopexit(this->base.get(), NULL);
|
||||
}
|
||||
|
||||
void ProxyServer::on_client_input(struct bufferevent*) {
|
||||
this->receive_and_process_commands(false);
|
||||
}
|
||||
|
||||
void ProxyServer::on_server_input(struct bufferevent*) {
|
||||
this->receive_and_process_commands(true);
|
||||
}
|
||||
|
||||
void ProxyServer::on_client_error(struct bufferevent*, short events) {
|
||||
if (events & BEV_EVENT_ERROR) {
|
||||
int err = EVUTIL_SOCKET_ERROR();
|
||||
log(WARNING, "error %d (%s) in client stream", err,
|
||||
evutil_socket_error_to_string(err));
|
||||
}
|
||||
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
|
||||
log(INFO, "client has disconnected");
|
||||
this->client_bev.reset();
|
||||
// "forward" the disconnection to the server
|
||||
this->server_bev.reset();
|
||||
|
||||
// disable encryption
|
||||
this->server_input_crypt.reset();
|
||||
this->server_output_crypt.reset();
|
||||
this->client_input_crypt.reset();
|
||||
this->client_output_crypt.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyServer::on_server_error(struct bufferevent*, short events) {
|
||||
if (events & BEV_EVENT_ERROR) {
|
||||
int err = EVUTIL_SOCKET_ERROR();
|
||||
log(WARNING, "error %d (%s) in server stream", err,
|
||||
evutil_socket_error_to_string(err));
|
||||
}
|
||||
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
|
||||
log(INFO, "server has disconnected");
|
||||
this->server_bev.reset();
|
||||
// "forward" the disconnection to the client
|
||||
this->client_bev.reset();
|
||||
|
||||
// disable encryption
|
||||
this->server_input_crypt.reset();
|
||||
this->server_output_crypt.reset();
|
||||
this->client_input_crypt.reset();
|
||||
this->client_output_crypt.reset();
|
||||
}
|
||||
}
|
||||
|
||||
size_t ProxyServer::get_size_field(const PSOCommandHeader* header) {
|
||||
if (this->version == GameVersion::DC) {
|
||||
return header->dc.size;
|
||||
} else if (this->version == GameVersion::PC) {
|
||||
return header->pc.size;
|
||||
} else if (this->version == GameVersion::GC) {
|
||||
return header->gc.size;
|
||||
} else if (this->version == GameVersion::BB) {
|
||||
return header->bb.size;
|
||||
} else {
|
||||
throw logic_error("version not supported in proxy mode");
|
||||
}
|
||||
}
|
||||
|
||||
size_t ProxyServer::get_command_field(const PSOCommandHeader* header) {
|
||||
if (this->version == GameVersion::DC) {
|
||||
return header->dc.command;
|
||||
} else if (this->version == GameVersion::PC) {
|
||||
return header->pc.command;
|
||||
} else if (this->version == GameVersion::GC) {
|
||||
return header->gc.command;
|
||||
} else if (this->version == GameVersion::BB) {
|
||||
return header->bb.command;
|
||||
} else {
|
||||
throw logic_error("version not supported in proxy mode");
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyServer::receive_and_process_commands(bool from_server) {
|
||||
struct bufferevent* source_bev = from_server ? this->server_bev.get() : this->client_bev.get();
|
||||
struct bufferevent* dest_bev = from_server ? this->client_bev.get() : this->server_bev.get();
|
||||
|
||||
struct evbuffer* source_buf = bufferevent_get_input(source_bev);
|
||||
struct evbuffer* dest_buf = dest_bev ? bufferevent_get_output(dest_bev) : NULL;
|
||||
|
||||
PSOEncryption* source_crypt = from_server ? this->server_input_crypt.get() : this->client_input_crypt.get();
|
||||
PSOEncryption* dest_crypt = from_server ? this->client_output_crypt.get() : this->server_output_crypt.get();
|
||||
|
||||
PSOCommandHeader* input_header = from_server ? &this->server_input_header : &this->client_input_header;
|
||||
|
||||
for (;;) {
|
||||
if (this->get_size_field(input_header) == 0) {
|
||||
ssize_t bytes = evbuffer_copyout(source_buf, input_header,
|
||||
this->header_size);
|
||||
//log(INFO, "[ProxyServer-debug] %zd bytes copied for header", bytes);
|
||||
if (bytes < static_cast<ssize_t>(this->header_size)) {
|
||||
break;
|
||||
}
|
||||
|
||||
//log(INFO, "[ProxyServer-debug] received encrypted header");
|
||||
//print_data(stderr, input_header, this->header_size);
|
||||
|
||||
if (source_crypt) {
|
||||
source_crypt->decrypt(input_header, this->header_size);
|
||||
}
|
||||
}
|
||||
|
||||
size_t command_size = this->get_size_field(input_header);
|
||||
if (evbuffer_get_length(source_buf) < command_size) {
|
||||
//log(INFO, "[ProxyServer-debug] insufficient data for command (%zX/%hX bytes)", evbuffer_get_length(source_buf), this->get_size_field(input_header));
|
||||
break;
|
||||
}
|
||||
|
||||
string command(command_size, '\0');
|
||||
ssize_t bytes = evbuffer_remove(source_buf,
|
||||
const_cast<char*>(command.data()), command_size);
|
||||
if (bytes < static_cast<ssize_t>(command_size)) {
|
||||
throw logic_error("enough bytes available, but could not remove them");
|
||||
}
|
||||
//log(INFO, "[ProxyServer-debug] read command (%zX bytes)", bytes);
|
||||
// overwrite the header with the already-decrypted header
|
||||
memcpy(const_cast<char*>(command.data()), input_header, this->header_size);
|
||||
|
||||
//log(INFO, "[ProxyServer-debug] received encrypted command with pre-decrypted header");
|
||||
//print_data(stderr, command);
|
||||
|
||||
if (source_crypt) {
|
||||
source_crypt->decrypt(
|
||||
const_cast<char*>(command.data() + this->header_size),
|
||||
command_size - this->header_size);
|
||||
}
|
||||
|
||||
log(INFO, "%s:", from_server ? "server" : "client");
|
||||
print_data(stderr, command);
|
||||
|
||||
// preprocess the command if needed
|
||||
if (from_server) {
|
||||
switch (this->get_command_field(input_header)) {
|
||||
case 0x02: // init encryption
|
||||
case 0x17: { // init encryption
|
||||
if (this->version == GameVersion::BB) {
|
||||
throw invalid_argument("console server init received on BB");
|
||||
}
|
||||
|
||||
struct InitEncryptionCommand {
|
||||
PSOCommandHeaderDCGC header;
|
||||
char copyright[0x40];
|
||||
uint32_t server_key;
|
||||
uint32_t client_key;
|
||||
};
|
||||
if (command.size() < sizeof(InitEncryptionCommand)) {
|
||||
throw std::runtime_error("init encryption command is too small");
|
||||
}
|
||||
|
||||
const InitEncryptionCommand* cmd = reinterpret_cast<const InitEncryptionCommand*>(
|
||||
command.data());
|
||||
if (this->version == GameVersion::PC) {
|
||||
this->server_input_crypt.reset(new PSOPCEncryption(cmd->server_key));
|
||||
this->server_output_crypt.reset(new PSOPCEncryption(cmd->client_key));
|
||||
this->client_input_crypt.reset(new PSOPCEncryption(cmd->client_key));
|
||||
this->client_output_crypt.reset(new PSOPCEncryption(cmd->server_key));
|
||||
|
||||
} else if (this->version == GameVersion::GC) {
|
||||
this->server_input_crypt.reset(new PSOGCEncryption(cmd->server_key));
|
||||
this->server_output_crypt.reset(new PSOGCEncryption(cmd->client_key));
|
||||
this->client_input_crypt.reset(new PSOGCEncryption(cmd->client_key));
|
||||
this->client_output_crypt.reset(new PSOGCEncryption(cmd->server_key));
|
||||
|
||||
} else {
|
||||
throw invalid_argument("unsupported version");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x19: { // reconnect
|
||||
struct ReconnectCommandArgs {
|
||||
uint32_t address;
|
||||
uint16_t port;
|
||||
uint16_t unused;
|
||||
};
|
||||
|
||||
if (command.size() < sizeof(ReconnectCommandArgs) + this->header_size) {
|
||||
throw std::runtime_error("reconnect command is too small");
|
||||
}
|
||||
|
||||
ReconnectCommandArgs* args = reinterpret_cast<ReconnectCommandArgs*>(
|
||||
const_cast<char*>(command.data() + this->header_size));
|
||||
memset(&this->next_destination, 0, sizeof(this->next_destination));
|
||||
struct sockaddr_in* sin = reinterpret_cast<struct sockaddr_in*>(
|
||||
&this->next_destination);
|
||||
sin->sin_family = AF_INET;
|
||||
sin->sin_port = htons(args->port);
|
||||
sin->sin_addr.s_addr = args->address; // already network byte order
|
||||
|
||||
if (!dest_bev) {
|
||||
log(WARNING, "received reconnect command with no destination present");
|
||||
} else {
|
||||
struct sockaddr_storage sockname_ss;
|
||||
socklen_t len = sizeof(sockname_ss);
|
||||
getsockname(bufferevent_getfd(dest_bev),
|
||||
reinterpret_cast<struct sockaddr*>(&sockname_ss), &len);
|
||||
if (sockname_ss.ss_family != AF_INET) {
|
||||
throw logic_error("existing connection is not ipv4");
|
||||
}
|
||||
|
||||
struct sockaddr_in* sockname_sin = reinterpret_cast<struct sockaddr_in*>(
|
||||
&sockname_ss);
|
||||
args->address = sockname_sin->sin_addr.s_addr; // already network byte order
|
||||
args->port = this->listeners.begin()->first;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reencrypt and forward the command
|
||||
if (dest_buf) {
|
||||
if (dest_crypt) {
|
||||
dest_crypt->encrypt(const_cast<char*>(command.data()), command.size());
|
||||
}
|
||||
//log(INFO, "[ProxyServer-debug] sending encrypted command");
|
||||
//print_data(stderr, command);
|
||||
|
||||
evbuffer_add(dest_buf, command.data(), command.size());
|
||||
} else {
|
||||
log(WARNING, "no destination present; dropping command");
|
||||
}
|
||||
|
||||
// clear the input header so we can read the next command
|
||||
memset(input_header, 0, this->header_size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
#include <map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
#include "PSOEncryption.hh"
|
||||
#include "PSOProtocol.hh"
|
||||
|
||||
|
||||
|
||||
class ProxyServer {
|
||||
public:
|
||||
ProxyServer() = delete;
|
||||
ProxyServer(const ProxyServer&) = delete;
|
||||
ProxyServer(ProxyServer&&) = delete;
|
||||
ProxyServer(std::shared_ptr<struct event_base> base,
|
||||
const struct sockaddr_storage& initial_destination, GameVersion version);
|
||||
virtual ~ProxyServer() = default;
|
||||
|
||||
void listen(int port);
|
||||
|
||||
void send_to_client(const std::string& data);
|
||||
void send_to_server(const std::string& data);
|
||||
|
||||
private:
|
||||
std::shared_ptr<struct event_base> base;
|
||||
std::map<int, std::unique_ptr<struct evconnlistener, void(*)(struct evconnlistener*)>> listeners;
|
||||
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)> client_bev;
|
||||
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)> server_bev;
|
||||
struct sockaddr_storage next_destination;
|
||||
int listen_port;
|
||||
GameVersion version;
|
||||
|
||||
size_t header_size;
|
||||
PSOCommandHeader client_input_header;
|
||||
PSOCommandHeader server_input_header;
|
||||
std::shared_ptr<PSOEncryption> client_input_crypt;
|
||||
std::shared_ptr<PSOEncryption> client_output_crypt;
|
||||
std::shared_ptr<PSOEncryption> server_input_crypt;
|
||||
std::shared_ptr<PSOEncryption> server_output_crypt;
|
||||
|
||||
void send_to_end(const std::string& data, bool to_server);
|
||||
|
||||
static void dispatch_on_listen_accept(struct evconnlistener* listener,
|
||||
evutil_socket_t fd, struct sockaddr *address, int socklen, void* ctx);
|
||||
static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx);
|
||||
static void dispatch_on_client_input(struct bufferevent* bev, void* ctx);
|
||||
static void dispatch_on_client_error(struct bufferevent* bev, short events,
|
||||
void* ctx);
|
||||
static void dispatch_on_server_input(struct bufferevent* bev, void* ctx);
|
||||
static void dispatch_on_server_error(struct bufferevent* bev, short events,
|
||||
void* ctx);
|
||||
|
||||
void on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd,
|
||||
struct sockaddr *address, int socklen);
|
||||
void on_listen_error(struct evconnlistener* listener);
|
||||
void on_client_input(struct bufferevent* bev);
|
||||
void on_client_error(struct bufferevent* bev, short events);
|
||||
void on_server_input(struct bufferevent* bev);
|
||||
void on_server_error(struct bufferevent* bev, short events);
|
||||
|
||||
size_t get_size_field(const PSOCommandHeader* header);
|
||||
size_t get_command_field(const PSOCommandHeader* header);
|
||||
|
||||
void receive_and_process_commands(bool from_server);
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
#include "ProxyShell.hh"
|
||||
|
||||
#include <event2/event.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <phosg/Strings.hh>
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
ProxyShell::ProxyShell(std::shared_ptr<struct event_base> base,
|
||||
std::shared_ptr<ServerState> state,
|
||||
std::shared_ptr<ProxyServer> proxy_server) : Shell(base, state),
|
||||
proxy_server(proxy_server) { }
|
||||
|
||||
void ProxyShell::execute_command(const string& command) {
|
||||
// find the entry in the command table and run the command
|
||||
size_t command_end = skip_non_whitespace(command, 0);
|
||||
size_t args_begin = skip_whitespace(command, command_end);
|
||||
string command_name = command.substr(0, command_end);
|
||||
string command_args = command.substr(args_begin);
|
||||
|
||||
if (command_name == "exit") {
|
||||
throw exit_shell();
|
||||
|
||||
} else if (command_name == "help") {
|
||||
fprintf(stderr, "\
|
||||
commands:\n\
|
||||
help\n\
|
||||
you\'re reading it now\n\
|
||||
exit (or ctrl+d)\n\
|
||||
shut down the proxy\n\
|
||||
sc <data>\n\
|
||||
send a command to the client\n\
|
||||
ss <data>\n\
|
||||
send a command to the server\n\
|
||||
chat <text>\n\
|
||||
send a chat message to the server\n\
|
||||
dchat <data>\n\
|
||||
send a chat message to the server with arbitrary data in it\n\
|
||||
marker <color-id>\n\
|
||||
send a lobby marker message to the server\n\
|
||||
event <event-id>\n\
|
||||
send a lobby event update to yourself\n\
|
||||
");
|
||||
|
||||
} else if ((command_name == "sc") || (command_name == "ss")) {
|
||||
bool to_client = (command_name[1] == 'c');
|
||||
string data = parse_data_string(command_args);
|
||||
if (data.size() & 3) {
|
||||
throw invalid_argument("data size is not a multiple of 4");
|
||||
}
|
||||
if (data.size() == 0) {
|
||||
throw invalid_argument("no data given");
|
||||
}
|
||||
uint16_t* size_field = reinterpret_cast<uint16_t*>(const_cast<char*>(data.data() + 2));
|
||||
*size_field = data.size();
|
||||
|
||||
log(INFO, "%s (from proxy):", to_client ? "server" : "client");
|
||||
print_data(stderr, data);
|
||||
|
||||
if (to_client) {
|
||||
this->proxy_server->send_to_client(data);
|
||||
} else {
|
||||
this->proxy_server->send_to_server(data);
|
||||
}
|
||||
|
||||
} else if ((command_name == "chat") || (command_name == "dchat")) {
|
||||
string data(12, '\0');
|
||||
data[0] = 0x06;
|
||||
data.push_back('\x09');
|
||||
data.push_back('E');
|
||||
if (command_name == "dchat") {
|
||||
data += parse_data_string(command_args);
|
||||
} else {
|
||||
data += command_args;
|
||||
}
|
||||
data.push_back('\0');
|
||||
data.resize((data.size() + 3) & (~3));
|
||||
uint16_t* size_field = reinterpret_cast<uint16_t*>(const_cast<char*>(data.data() + 2));
|
||||
*size_field = data.size();
|
||||
|
||||
log(INFO, "client (from proxy):");
|
||||
print_data(stderr, data);
|
||||
this->proxy_server->send_to_server(data);
|
||||
|
||||
} else if (command_name == "marker") {
|
||||
string data("\x89\x00\x04\x00", 4);
|
||||
data[1] = stod(command_args);
|
||||
|
||||
log(INFO, "client (from proxy):");
|
||||
print_data(stderr, data);
|
||||
this->proxy_server->send_to_server(data);
|
||||
|
||||
} else if (command_name == "event") {
|
||||
string data("\xDA\x00\x04\x00", 4);
|
||||
data[1] = stod(command_args);
|
||||
|
||||
log(INFO, "server (from proxy):");
|
||||
print_data(stderr, data);
|
||||
this->proxy_server->send_to_client(data);
|
||||
|
||||
} else {
|
||||
throw invalid_argument("unknown command; try \'help\'");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
#include "Shell.hh"
|
||||
#include "ProxyServer.hh"
|
||||
|
||||
|
||||
|
||||
class ProxyShell : public Shell {
|
||||
public:
|
||||
ProxyShell(std::shared_ptr<struct event_base> base,
|
||||
std::shared_ptr<ServerState> state,
|
||||
std::shared_ptr<ProxyServer> proxy_server);
|
||||
virtual ~ProxyShell() = default;
|
||||
ProxyShell(const ProxyShell&) = delete;
|
||||
ProxyShell(ProxyShell&&) = delete;
|
||||
ProxyShell& operator=(const ProxyShell&) = delete;
|
||||
ProxyShell& operator=(ProxyShell&&) = delete;
|
||||
|
||||
protected:
|
||||
std::shared_ptr<ProxyServer> proxy_server;
|
||||
|
||||
virtual void execute_command(const std::string& command);
|
||||
};
|
||||
+539
@@ -0,0 +1,539 @@
|
||||
#include "Quest.hh"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/Random.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
|
||||
#include "Compression.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
#include "Text.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
struct PSODownloadQuestHeader {
|
||||
// When sending a DLQ to the client, this is the DECOMPRESSED size. When
|
||||
// reading it from a GCI file, this is the COMPRESSED size.
|
||||
uint32_t size;
|
||||
// Note: use PSO PC encryption, even for GC quests.
|
||||
uint32_t encryption_seed;
|
||||
|
||||
void byteswap() {
|
||||
this->size = bswap32(this->size);
|
||||
this->encryption_seed = bswap32(this->encryption_seed);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
bool category_is_mode(QuestCategory category) {
|
||||
return (category == QuestCategory::Battle) ||
|
||||
(category == QuestCategory::Challenge) ||
|
||||
(category == QuestCategory::Episode3);
|
||||
}
|
||||
|
||||
const char* name_for_category(QuestCategory category) {
|
||||
switch (category) {
|
||||
case QuestCategory::Retrieval:
|
||||
return "Retrieval";
|
||||
case QuestCategory::Extermination:
|
||||
return "Extermination";
|
||||
case QuestCategory::Event:
|
||||
return "Event";
|
||||
case QuestCategory::Shop:
|
||||
return "Shop";
|
||||
case QuestCategory::VR:
|
||||
return "VR";
|
||||
case QuestCategory::Tower:
|
||||
return "Tower";
|
||||
case QuestCategory::GovernmentEpisode1:
|
||||
return "GovernmentEpisode1";
|
||||
case QuestCategory::GovernmentEpisode2:
|
||||
return "GovernmentEpisode2";
|
||||
case QuestCategory::GovernmentEpisode4:
|
||||
return "GovernmentEpisode4";
|
||||
case QuestCategory::Download:
|
||||
return "Download";
|
||||
case QuestCategory::Battle:
|
||||
return "Battle";
|
||||
case QuestCategory::Challenge:
|
||||
return "Challenge";
|
||||
case QuestCategory::Solo:
|
||||
return "Solo";
|
||||
case QuestCategory::Episode3:
|
||||
return "Episode3";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct PSOQuestHeaderDC { // same for dc v1 and v2, thankfully
|
||||
uint32_t start_offset;
|
||||
uint32_t unknown_offset1;
|
||||
uint32_t size;
|
||||
uint32_t unused;
|
||||
uint8_t is_download;
|
||||
uint8_t unknown1;
|
||||
uint16_t quest_number; // 0xFFFF for challenge quests
|
||||
char name[0x20];
|
||||
char short_description[0x80];
|
||||
char long_description[0x120];
|
||||
};
|
||||
|
||||
struct PSOQuestHeaderPC {
|
||||
uint32_t start_offset;
|
||||
uint32_t unknown_offset1;
|
||||
uint32_t size;
|
||||
uint32_t unused;
|
||||
uint8_t is_download;
|
||||
uint8_t unknown1;
|
||||
uint16_t quest_number; // 0xFFFF for challenge quests
|
||||
char16_t name[0x20];
|
||||
char16_t short_description[0x80];
|
||||
char16_t long_description[0x120];
|
||||
};
|
||||
|
||||
struct PSOQuestHeaderGC {
|
||||
uint32_t start_offset;
|
||||
uint32_t unknown_offset1;
|
||||
uint32_t size;
|
||||
uint32_t unused;
|
||||
uint8_t is_download;
|
||||
uint8_t unknown1;
|
||||
uint8_t quest_number;
|
||||
uint8_t episode; // 1 = ep2. apparently some quests have 0xFF here, which means ep1 (?)
|
||||
char name[0x20];
|
||||
char short_description[0x80];
|
||||
char long_description[0x120];
|
||||
};
|
||||
|
||||
struct PSOQuestHeaderGCEpisode3 {
|
||||
// there's actually a lot of other important stuff in here but I'm lazy. it
|
||||
// looks like map data, cutscene data, and maybe special cards used during
|
||||
// the quest
|
||||
uint8_t unused[0x1DF0];
|
||||
char name[0x14];
|
||||
char location[0x14];
|
||||
char location2[0x3C];
|
||||
char description[0x190];
|
||||
uint8_t unused2[0x3A34];
|
||||
};
|
||||
|
||||
struct PSOQuestHeaderBB {
|
||||
uint32_t start_offset;
|
||||
uint32_t unknown_offset1;
|
||||
uint32_t size;
|
||||
uint32_t unused;
|
||||
uint16_t quest_number; // 0xFFFF for challenge quests
|
||||
uint16_t unused2;
|
||||
uint8_t episode; // 0 = ep1, 1 = ep2, 2 = ep4
|
||||
uint8_t max_players;
|
||||
uint8_t joinable_in_progress;
|
||||
uint8_t unknown;
|
||||
char16_t name[0x20];
|
||||
char16_t short_description[0x80];
|
||||
char16_t long_description[0x120];
|
||||
};
|
||||
|
||||
|
||||
|
||||
Quest::Quest(const string& bin_filename)
|
||||
: quest_id(-1),
|
||||
category(QuestCategory::Unknown),
|
||||
episode(0),
|
||||
is_dcv1(false),
|
||||
joinable(false),
|
||||
gci_format(false) {
|
||||
|
||||
if (ends_with(bin_filename, ".bin.gci")) {
|
||||
this->gci_format = true;
|
||||
this->file_basename = bin_filename.substr(0, bin_filename.size() - 8);
|
||||
} else if (ends_with(bin_filename, ".bin")) {
|
||||
this->file_basename = bin_filename.substr(0, bin_filename.size() - 4);
|
||||
} else {
|
||||
throw runtime_error("quest does not have a valid .bin file");
|
||||
}
|
||||
|
||||
string basename;
|
||||
{
|
||||
size_t slash_pos = bin_filename.rfind('/');
|
||||
if (slash_pos != string::npos) {
|
||||
basename = bin_filename.substr(slash_pos + 1);
|
||||
} else {
|
||||
basename = bin_filename;
|
||||
}
|
||||
}
|
||||
basename.resize(basename.size() - (this->gci_format ? 8 : 4));
|
||||
|
||||
// quest filenames are like:
|
||||
// b###-VV.bin for battle mode
|
||||
// c###-VV.bin for challenge mode
|
||||
// e###-gc3.bin for episode 3
|
||||
// q###-CAT-VV.bin for normal quests
|
||||
|
||||
if (basename.empty()) {
|
||||
throw invalid_argument("empty filename");
|
||||
}
|
||||
|
||||
if (basename[0] == 'b') {
|
||||
this->category = QuestCategory::Battle;
|
||||
} else if (basename[0] == 'c') {
|
||||
this->category = QuestCategory::Challenge;
|
||||
} else if (basename[0] == 'e') {
|
||||
this->category = QuestCategory::Episode3;
|
||||
} else if (basename[0] != 'q') {
|
||||
throw invalid_argument("filename does not indicate mode");
|
||||
}
|
||||
|
||||
// if the quest category is still unknown, expect 3 tokens (one of them will
|
||||
// tell us the category)
|
||||
vector<string> tokens = split(basename, '-');
|
||||
if (tokens.size() != (2 + (this->category == QuestCategory::Unknown))) {
|
||||
throw invalid_argument("incorrect filename format");
|
||||
}
|
||||
|
||||
// parse the number out of the first token
|
||||
this->quest_id = strtoull(tokens[0].c_str() + 1, NULL, 10);
|
||||
|
||||
// get the category from the second token if needed
|
||||
if (this->category == QuestCategory::Unknown) {
|
||||
if (tokens[1] == "gov") {
|
||||
if (this->episode == 0) {
|
||||
this->category = QuestCategory::GovernmentEpisode1;
|
||||
} else if (this->episode == 1) {
|
||||
this->category = QuestCategory::GovernmentEpisode2;
|
||||
} else if (this->episode == 2) {
|
||||
this->category = QuestCategory::GovernmentEpisode4;
|
||||
} else {
|
||||
throw invalid_argument("government quest has incorrect episode");
|
||||
}
|
||||
} else {
|
||||
static const unordered_map<std::string, QuestCategory> name_to_category({
|
||||
{"ret", QuestCategory::Retrieval},
|
||||
{"ext", QuestCategory::Extermination},
|
||||
{"evt", QuestCategory::Event},
|
||||
{"shp", QuestCategory::Shop},
|
||||
{"vr", QuestCategory::VR},
|
||||
{"twr", QuestCategory::Tower},
|
||||
{"dl", QuestCategory::Download},
|
||||
{"1p", QuestCategory::Solo},
|
||||
});
|
||||
this->category = name_to_category.at(tokens[1]);
|
||||
}
|
||||
tokens.erase(tokens.begin() + 1);
|
||||
}
|
||||
|
||||
static const unordered_map<std::string, GameVersion> name_to_version({
|
||||
{"d1", GameVersion::DC},
|
||||
{"dc", GameVersion::DC},
|
||||
{"pc", GameVersion::PC},
|
||||
{"gc", GameVersion::GC},
|
||||
{"gc3", GameVersion::GC},
|
||||
{"bb", GameVersion::BB},
|
||||
});
|
||||
this->version = name_to_version.at(tokens[1]);
|
||||
|
||||
// the rest of the information needs to be fetched from the .bin file's
|
||||
// contents
|
||||
|
||||
auto bin_compressed = this->bin_contents();
|
||||
auto bin_decompressed = prs_decompress(*bin_compressed);
|
||||
|
||||
switch (this->version) {
|
||||
case GameVersion::Patch:
|
||||
throw invalid_argument("patch server quests are not valid");
|
||||
break;
|
||||
|
||||
case GameVersion::DC: {
|
||||
if (bin_decompressed.size() < sizeof(PSOQuestHeaderDC)) {
|
||||
throw invalid_argument("file is too small for header");
|
||||
}
|
||||
auto* header = reinterpret_cast<const PSOQuestHeaderDC*>(bin_decompressed.data());
|
||||
this->joinable = false;
|
||||
this->episode = 0;
|
||||
this->name = decode_sjis(header->name);
|
||||
this->short_description = decode_sjis(header->short_description);
|
||||
this->long_description = decode_sjis(header->long_description);
|
||||
this->is_dcv1 = (tokens[1] == "d1");
|
||||
break;
|
||||
}
|
||||
|
||||
case GameVersion::PC: {
|
||||
if (bin_decompressed.size() < sizeof(PSOQuestHeaderPC)) {
|
||||
throw invalid_argument("file is too small for header");
|
||||
}
|
||||
auto* header = reinterpret_cast<const PSOQuestHeaderPC*>(bin_decompressed.data());
|
||||
this->joinable = false;
|
||||
this->episode = 0;
|
||||
this->name = header->name;
|
||||
this->short_description = header->short_description;
|
||||
this->long_description = header->long_description;
|
||||
break;
|
||||
}
|
||||
|
||||
case GameVersion::GC: {
|
||||
if (this->category == QuestCategory::Episode3) {
|
||||
// these all appear to be the same size
|
||||
if (bin_decompressed.size() != sizeof(PSOQuestHeaderGCEpisode3)) {
|
||||
throw invalid_argument("file is incorrect size");
|
||||
}
|
||||
auto* header = reinterpret_cast<const PSOQuestHeaderGCEpisode3*>(bin_decompressed.data());
|
||||
this->joinable = false;
|
||||
this->episode = 0xFF;
|
||||
this->name = decode_sjis(header->name);
|
||||
this->short_description = decode_sjis(header->location2);
|
||||
this->long_description = decode_sjis(header->description);
|
||||
} else {
|
||||
if (bin_decompressed.size() < sizeof(PSOQuestHeaderGC)) {
|
||||
throw invalid_argument("file is too small for header");
|
||||
}
|
||||
auto* header = reinterpret_cast<const PSOQuestHeaderGC*>(bin_decompressed.data());
|
||||
this->joinable = false;
|
||||
this->episode = (header->episode == 1);
|
||||
this->name = decode_sjis(header->name);
|
||||
this->short_description = decode_sjis(header->short_description);
|
||||
this->long_description = decode_sjis(header->long_description);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case GameVersion::BB: {
|
||||
if (bin_decompressed.size() < sizeof(PSOQuestHeaderBB)) {
|
||||
throw invalid_argument("file is too small for header");
|
||||
}
|
||||
auto* header = reinterpret_cast<const PSOQuestHeaderBB*>(bin_decompressed.data());
|
||||
this->joinable = header->joinable_in_progress;
|
||||
this->episode = header->episode;
|
||||
this->name = header->name;
|
||||
this->short_description = header->short_description;
|
||||
this->long_description = header->long_description;
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static string basename_for_filename(const std::string& filename) {
|
||||
size_t slash_pos = filename.rfind('/');
|
||||
if (slash_pos != string::npos) {
|
||||
return filename.substr(slash_pos + 1);
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
|
||||
std::string Quest::bin_filename() const {
|
||||
return basename_for_filename(this->file_basename + ".bin");
|
||||
}
|
||||
|
||||
std::string Quest::dat_filename() const {
|
||||
return basename_for_filename(this->file_basename + ".dat");
|
||||
}
|
||||
|
||||
shared_ptr<const string> Quest::bin_contents() const {
|
||||
if (!this->bin_contents_ptr) {
|
||||
if (this->gci_format) {
|
||||
this->bin_contents_ptr.reset(new string(this->decode_gci(this->file_basename + ".bin.gci")));
|
||||
} else {
|
||||
this->bin_contents_ptr.reset(new string(load_file(this->file_basename + ".bin")));
|
||||
}
|
||||
}
|
||||
return this->bin_contents_ptr;
|
||||
}
|
||||
|
||||
shared_ptr<const string> Quest::dat_contents() const {
|
||||
if (!this->dat_contents_ptr) {
|
||||
if (this->gci_format) {
|
||||
this->dat_contents_ptr.reset(new string(this->decode_gci(this->file_basename + ".dat.gci")));
|
||||
} else {
|
||||
this->dat_contents_ptr.reset(new string(load_file(this->file_basename + ".dat")));
|
||||
}
|
||||
}
|
||||
return this->dat_contents_ptr;
|
||||
}
|
||||
|
||||
string Quest::decode_gci(const string& filename) {
|
||||
|
||||
string data = load_file(filename);
|
||||
if (data.size() < 0x2080 + sizeof(PSODownloadQuestHeader)) {
|
||||
throw runtime_error(string_printf(
|
||||
"GCI file is truncated before download quest header (have 0x%zX bytes)", data.size()));
|
||||
}
|
||||
PSODownloadQuestHeader* h = reinterpret_cast<PSODownloadQuestHeader*>(
|
||||
const_cast<char*>(data.data() + 0x2080));
|
||||
h->byteswap();
|
||||
|
||||
string compressed_data_with_header = data.substr(0x2088, h->size);
|
||||
|
||||
// For now, we can only load unencrypted quests, unfortunately
|
||||
// TODO: Figure out how GCI encryption works and implement it here.
|
||||
|
||||
// Unlike the DLQ header, this one is stored little-endian. The compressed
|
||||
// data immediately follows this header.
|
||||
struct DecryptedHeader {
|
||||
uint32_t unknown1;
|
||||
uint32_t unknown2;
|
||||
uint32_t decompressed_size;
|
||||
uint32_t unknown4;
|
||||
};
|
||||
if (compressed_data_with_header.size() < sizeof(DecryptedHeader)) {
|
||||
throw runtime_error("GCI file compressed data truncated during header");
|
||||
}
|
||||
DecryptedHeader* dh = reinterpret_cast<DecryptedHeader*>(const_cast<char*>(
|
||||
compressed_data_with_header.data()));
|
||||
if (dh->unknown1 || dh->unknown2 || dh->unknown4) {
|
||||
throw runtime_error("GCI file appears to be encrypted");
|
||||
}
|
||||
|
||||
string data_to_decompress = compressed_data_with_header.substr(sizeof(DecryptedHeader));
|
||||
string decompressed_data = prs_decompress(data_to_decompress);
|
||||
|
||||
if (decompressed_data.size() < dh->decompressed_size - 8) {
|
||||
throw runtime_error(string_printf(
|
||||
"GCI decompressed data is smaller than expected size (have 0x%zX bytes, expected 0x%zX bytes)",
|
||||
decompressed_data.size(), dh->decompressed_size - 8));
|
||||
}
|
||||
|
||||
// The caller expects to get PRS-compressed data when calling bin_contents()
|
||||
// and dat_contents(), so we shouldn't decompress it here.
|
||||
return data_to_decompress;
|
||||
}
|
||||
|
||||
|
||||
|
||||
QuestIndex::QuestIndex(const char* directory) : directory(directory) {
|
||||
auto filename_set = list_directory(this->directory);
|
||||
vector<string> filenames(filename_set.begin(), filename_set.end());
|
||||
sort(filenames.begin(), filenames.end());
|
||||
for (const auto& filename : filenames) {
|
||||
string full_path = this->directory + "/" + filename;
|
||||
|
||||
if (ends_with(filename, ".gba")) {
|
||||
shared_ptr<string> contents(new string(load_file(full_path)));
|
||||
this->gba_file_contents.emplace(make_pair(filename, contents));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ends_with(filename, ".bin") || ends_with(filename, ".bin.gci")) {
|
||||
// try {
|
||||
shared_ptr<Quest> q(new Quest(full_path));
|
||||
this->version_id_to_quest.emplace(make_pair(q->version, q->quest_id), q);
|
||||
this->version_name_to_quest.emplace(make_pair(q->version, q->name), q);
|
||||
string ascii_name = encode_sjis(q->name);
|
||||
log(INFO, "indexed quest %s (%s-%" PRId64 ", %s, episode=%hhu, joinable=%s, dcv1=%s)",
|
||||
ascii_name.c_str(), name_for_version(q->version), q->quest_id,
|
||||
name_for_category(q->category), q->episode,
|
||||
q->joinable ? "true" : "false", q->is_dcv1 ? "true" : "false");
|
||||
// } catch (const exception& e) {
|
||||
// log(WARNING, "failed to parse quest file %s (%s)", filename.c_str(), e.what());
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<const Quest> QuestIndex::get(GameVersion version,
|
||||
uint32_t id) const {
|
||||
return this->version_id_to_quest.at(make_pair(version, id));
|
||||
}
|
||||
|
||||
shared_ptr<const string> QuestIndex::get_gba(const string& name) const {
|
||||
return this->gba_file_contents.at(name);
|
||||
}
|
||||
|
||||
vector<shared_ptr<const Quest>> QuestIndex::filter(GameVersion version,
|
||||
bool is_dcv1, QuestCategory category, uint8_t episode) const {
|
||||
auto it = this->version_id_to_quest.lower_bound(make_pair(version, 0));
|
||||
auto end_it = this->version_id_to_quest.upper_bound(make_pair(version, 0xFFFFFFFF));
|
||||
|
||||
vector<shared_ptr<const Quest>> ret;
|
||||
for (; it != end_it; it++) {
|
||||
shared_ptr<const Quest> q = it->second;
|
||||
if ((q->is_dcv1 != is_dcv1) || (q->category != category)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// only check episode and solo if the category isn't a mode (that is, ignore
|
||||
// episode if querying for battle/challange/solo quests)
|
||||
if (!category_is_mode(category) && ((q->episode != episode))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ret.emplace_back(q);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
static string create_download_quest_file(const string& compressed_data,
|
||||
size_t decompressed_size) {
|
||||
string data(8, '\0');
|
||||
auto* header = reinterpret_cast<PSODownloadQuestHeader*>(const_cast<char*>(
|
||||
compressed_data.data()));
|
||||
header->size = decompressed_size + sizeof(PSODownloadQuestHeader);
|
||||
header->encryption_seed = random_object<uint32_t>();
|
||||
data += compressed_data;
|
||||
|
||||
// add extra bytes if necessary so encryption won't fail
|
||||
data.resize((data.size() + 3) & (~3));
|
||||
|
||||
// TODO: for DC quests, do we use DC encryption?
|
||||
PSOPCEncryption encr(header->encryption_seed);
|
||||
encr.encrypt(const_cast<char*>(data.data() + sizeof(PSODownloadQuestHeader)),
|
||||
data.size() - sizeof(PSODownloadQuestHeader));
|
||||
return data;
|
||||
}
|
||||
|
||||
shared_ptr<Quest> Quest::create_download_quest(const string& file_basename) const {
|
||||
if (this->category == QuestCategory::Download) {
|
||||
throw invalid_argument("quest is already a download quest");
|
||||
}
|
||||
|
||||
string decompressed_bin = prs_decompress(*this->bin_contents());
|
||||
void* data_ptr = const_cast<char*>(decompressed_bin.data());
|
||||
switch (this->version) {
|
||||
case GameVersion::DC:
|
||||
reinterpret_cast<PSOQuestHeaderDC*>(data_ptr)->is_download = 0x01;
|
||||
break;
|
||||
case GameVersion::PC:
|
||||
reinterpret_cast<PSOQuestHeaderDC*>(data_ptr)->is_download = 0x01;
|
||||
break;
|
||||
case GameVersion::GC:
|
||||
reinterpret_cast<PSOQuestHeaderDC*>(data_ptr)->is_download = 0x01;
|
||||
break;
|
||||
case GameVersion::BB:
|
||||
throw invalid_argument("PSOBB does not support download quests");
|
||||
default:
|
||||
throw invalid_argument("unknown game version");
|
||||
}
|
||||
|
||||
shared_ptr<Quest> dlq(new Quest(file_basename));
|
||||
dlq->quest_id = this->quest_id;
|
||||
dlq->category = QuestCategory::Download;
|
||||
dlq->episode = this->episode;
|
||||
dlq->is_dcv1 = this->is_dcv1;
|
||||
dlq->joinable = this->joinable;
|
||||
dlq->version = this->version;
|
||||
dlq->name = this->name;
|
||||
dlq->short_description = this->short_description;
|
||||
dlq->long_description = this->long_description;
|
||||
|
||||
dlq->bin_contents_ptr.reset(new string(create_download_quest_file(
|
||||
prs_compress(decompressed_bin), decompressed_bin.size())));
|
||||
|
||||
auto dat_contents = this->dat_contents();
|
||||
dlq->dat_contents_ptr.reset(new string(create_download_quest_file(
|
||||
*dat_contents, prs_decompress_size(*dat_contents))));
|
||||
|
||||
save_file(dlq->bin_filename(), *dlq->bin_contents_ptr);
|
||||
save_file(dlq->dat_filename(), *dlq->dat_contents_ptr);
|
||||
|
||||
return dlq;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Version.hh"
|
||||
|
||||
|
||||
|
||||
enum class QuestCategory {
|
||||
Unknown = -1,
|
||||
Retrieval = 0,
|
||||
Extermination,
|
||||
Event,
|
||||
Shop,
|
||||
VR,
|
||||
Tower,
|
||||
GovernmentEpisode1,
|
||||
GovernmentEpisode2,
|
||||
GovernmentEpisode4,
|
||||
Download,
|
||||
Battle,
|
||||
Challenge,
|
||||
Solo,
|
||||
Episode3,
|
||||
};
|
||||
|
||||
bool category_is_mode(QuestCategory category);
|
||||
const char* name_for_category(QuestCategory category);
|
||||
|
||||
|
||||
|
||||
class Quest {
|
||||
private:
|
||||
static std::string decode_gci(const std::string& filename);
|
||||
|
||||
public:
|
||||
int64_t quest_id;
|
||||
QuestCategory category;
|
||||
uint8_t episode; // 0 = ep1, 1 = ep2, 2 = ep4, 0xFF = ep3
|
||||
bool is_dcv1;
|
||||
bool joinable;
|
||||
GameVersion version;
|
||||
std::string file_basename; // we append -<version>.<bin/dat> when reading
|
||||
bool gci_format;
|
||||
std::u16string name;
|
||||
std::u16string short_description;
|
||||
std::u16string long_description;
|
||||
|
||||
// these are populated when requested
|
||||
mutable std::shared_ptr<std::string> bin_contents_ptr;
|
||||
mutable std::shared_ptr<std::string> dat_contents_ptr;
|
||||
|
||||
Quest(const std::string& file_basename);
|
||||
|
||||
std::string bin_filename() const;
|
||||
std::string dat_filename() const;
|
||||
|
||||
std::shared_ptr<const std::string> bin_contents() const;
|
||||
std::shared_ptr<const std::string> dat_contents() const;
|
||||
|
||||
std::shared_ptr<Quest> create_download_quest(
|
||||
const std::string& file_basename) const;
|
||||
};
|
||||
|
||||
struct QuestIndex {
|
||||
std::string directory;
|
||||
|
||||
std::map<std::pair<GameVersion, uint64_t>, std::shared_ptr<Quest>> version_id_to_quest;
|
||||
std::map<std::pair<GameVersion, std::u16string>, std::shared_ptr<Quest>> version_name_to_quest;
|
||||
|
||||
std::map<std::string, std::vector<std::shared_ptr<Quest>>> category_to_quests;
|
||||
|
||||
std::map<std::string, std::shared_ptr<std::string>> gba_file_contents;
|
||||
|
||||
QuestIndex(const char* directory);
|
||||
|
||||
std::shared_ptr<const Quest> get(GameVersion version, uint32_t id) const;
|
||||
std::shared_ptr<const std::string> get_gba(const std::string& name) const;
|
||||
std::vector<std::shared_ptr<const Quest>> filter(GameVersion version,
|
||||
bool is_dcv1, QuestCategory category, uint8_t episode) const;
|
||||
};
|
||||
|
||||
Quest create_download_quest(const Quest& src, size_t version);
|
||||
@@ -0,0 +1,24 @@
|
||||
#include "RareItemSet.hh"
|
||||
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Random.hh>
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
RareItemSet::RareItemSet(const char* filename, uint8_t episode,
|
||||
uint8_t difficulty, uint8_t secid) {
|
||||
scoped_fd fd(filename, O_RDONLY);
|
||||
size_t offset = (episode * 0x6400) + (difficulty * 0x1900) + (secid * 0x0280);
|
||||
preadx(fd, this, sizeof(*this), offset);
|
||||
}
|
||||
|
||||
bool sample_rare_item(uint8_t pc) {
|
||||
int8_t shift = ((pc >> 3) & 0x1F) - 4;
|
||||
if (shift < 0) {
|
||||
shift = 0;
|
||||
}
|
||||
uint32_t rate = ((2 << shift) * ((pc & 7) + 7));
|
||||
return (random_object<uint32_t>() < rate);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
|
||||
|
||||
struct RareItemDrop {
|
||||
uint8_t probability;
|
||||
uint8_t item_code[3];
|
||||
};
|
||||
|
||||
struct RareItemSet {
|
||||
RareItemDrop rares[0x65]; // 0000 - 0194 in file
|
||||
uint8_t box_areas[0x1E]; // 0194 - 01B2 in file
|
||||
RareItemDrop box_rares[0x1E]; // 01B2 - 022A in file
|
||||
uint8_t unused[0x56];
|
||||
|
||||
RareItemSet(const char* filename, uint8_t episode, uint8_t difficulty,
|
||||
uint8_t secid);
|
||||
}; // 0x280 in size; describes one difficulty, section ID, and episode
|
||||
|
||||
bool sample_rare_item(uint8_t pc);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "Client.hh"
|
||||
#include "ServerState.hh"
|
||||
|
||||
|
||||
void process_connect(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c);
|
||||
void process_disconnect(std::shared_ptr<ServerState> s,
|
||||
std::shared_ptr<Client> c);
|
||||
void process_command(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c,
|
||||
uint16_t command, uint32_t flag, uint16_t size, const void* data);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#include "PSOProtocol.hh"
|
||||
#include "Client.hh"
|
||||
#include "Lobby.hh"
|
||||
#include "Client.hh"
|
||||
#include "ServerState.hh"
|
||||
|
||||
|
||||
void check_size(uint16_t size, uint16_t min_size, uint16_t max_size = 0);
|
||||
|
||||
void process_subcommand(std::shared_ptr<ServerState> s,
|
||||
std::shared_ptr<Lobby> l, std::shared_ptr<Client> c, uint8_t command,
|
||||
uint8_t flag, const PSOSubcommand* sub, size_t count);
|
||||
+2186
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,186 @@
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stddef.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#include <memory>
|
||||
#include <phosg/Strings.hh>
|
||||
|
||||
#include "Client.hh"
|
||||
#include "Lobby.hh"
|
||||
#include "Server.hh"
|
||||
#include "Menu.hh"
|
||||
#include "Quest.hh"
|
||||
#include "Text.hh"
|
||||
|
||||
|
||||
|
||||
#define MAIN_MENU_ID 0x512CBD43
|
||||
#define INFORMATION_MENU_ID 0x93320CAA
|
||||
#define LOBBY_MENU_ID 0x01F8471B
|
||||
#define GAME_MENU_ID 0x205CD430
|
||||
#define QUEST_MENU_ID 0x7F02CA94
|
||||
#define QUEST_FILTER_MENU_ID 0xC38CA039
|
||||
|
||||
#define MAIN_MENU_GO_TO_LOBBY 0x00000001
|
||||
#define MAIN_MENU_INFORMATION 0x00000002
|
||||
#define MAIN_MENU_DISCONNECT 0x00000003
|
||||
#define INFORMATION_MENU_GO_BACK 0xFFFFFFFF
|
||||
|
||||
|
||||
|
||||
void send_command(std::shared_ptr<Client> c, uint16_t command,
|
||||
uint32_t flag = 0, const void* data = NULL, size_t size = 0);
|
||||
|
||||
void send_command_excluding_client(std::shared_ptr<Lobby> l,
|
||||
std::shared_ptr<Client> c, uint16_t command, uint32_t flag = 0,
|
||||
const void* data = NULL, size_t size = 0);
|
||||
|
||||
void send_command(std::shared_ptr<Lobby> l, uint16_t command, uint32_t flag = 0,
|
||||
const void* data = NULL, size_t size = 0);
|
||||
|
||||
void send_command(std::shared_ptr<ServerState> s, uint16_t command,
|
||||
uint32_t flag = 0, const void* data = NULL, size_t size = 0);
|
||||
|
||||
template <typename TARGET, typename STRUCT>
|
||||
void send_command(std::shared_ptr<TARGET> c, uint16_t command, uint32_t flag,
|
||||
const STRUCT& data) {
|
||||
send_command(c, command, flag, &data, sizeof(data));
|
||||
}
|
||||
|
||||
template <typename TARGET>
|
||||
void send_command(std::shared_ptr<TARGET> c, uint16_t command, uint32_t flag,
|
||||
const std::string& data) {
|
||||
send_command(c, command, flag, data.data(), data.size());
|
||||
}
|
||||
|
||||
template <typename TARGET, typename STRUCT>
|
||||
void send_command(std::shared_ptr<TARGET> c, uint16_t command, uint32_t flag,
|
||||
const std::vector<STRUCT>& data) {
|
||||
send_command(c, command, flag, data.data(), data.size() * sizeof(STRUCT));
|
||||
}
|
||||
|
||||
template <typename TARGET, typename STRUCT, typename ENTRY>
|
||||
void send_command(std::shared_ptr<TARGET> c, uint16_t command, uint32_t flag,
|
||||
const STRUCT& data, const std::vector<ENTRY>& array_data) {
|
||||
std::string all_data(reinterpret_cast<const char*>(&data), sizeof(STRUCT));
|
||||
all_data.append(reinterpret_cast<const char*>(array_data.data()),
|
||||
array_data.size() * sizeof(ENTRY));
|
||||
send_command(c, command, flag, all_data.data(), all_data.size());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void send_server_init(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c,
|
||||
bool initial_connection);
|
||||
void send_update_client_config(std::shared_ptr<Client> c);
|
||||
|
||||
void send_reconnect(std::shared_ptr<Client> c, uint32_t address, uint16_t port);
|
||||
void send_pc_gc_split_reconnect(std::shared_ptr<Client> c, uint32_t address,
|
||||
uint16_t pc_port, uint16_t gc_port);
|
||||
|
||||
void send_client_init_bb(std::shared_ptr<Client> c, uint32_t error);
|
||||
void send_team_and_key_config_bb(std::shared_ptr<Client> c);
|
||||
void send_player_preview_bb(std::shared_ptr<Client> c, uint8_t player_index,
|
||||
const PlayerDispDataBBPreview* preview);
|
||||
void send_accept_client_checksum_bb(std::shared_ptr<Client> c);
|
||||
void send_guild_card_header_bb(std::shared_ptr<Client> c);
|
||||
void send_guild_card_chunk_bb(std::shared_ptr<Client> c, size_t chunk_index);
|
||||
void send_stream_file_bb(std::shared_ptr<Client> c);
|
||||
void send_approve_player_choice_bb(std::shared_ptr<Client> c);
|
||||
void send_complete_player_bb(std::shared_ptr<Client> c);
|
||||
|
||||
void send_check_directory_patch(std::shared_ptr<Client> c, const char* dir);
|
||||
|
||||
void send_message_box(std::shared_ptr<Client> c, const char16_t* text);
|
||||
void send_lobby_name(std::shared_ptr<Client> c, const char16_t* text);
|
||||
void send_quest_info(std::shared_ptr<Client> c, const char16_t* text);
|
||||
void send_lobby_message_box(std::shared_ptr<Client> c, const char16_t* text);
|
||||
void send_ship_info(std::shared_ptr<Client> c, const char16_t* text);
|
||||
void send_text_message(std::shared_ptr<Client> c, const char16_t* text);
|
||||
void send_text_message(std::shared_ptr<Lobby> l, const char16_t* text);
|
||||
void send_text_message(std::shared_ptr<ServerState> l, const char16_t* text);
|
||||
void send_chat_message(std::shared_ptr<Client> c, uint32_t from_serial_number,
|
||||
const char16_t* from_name, const char16_t* text);
|
||||
void send_simple_mail(std::shared_ptr<Client> c, uint32_t from_serial_number,
|
||||
const char16_t* from_name, const char16_t* text);
|
||||
|
||||
template <typename TARGET>
|
||||
void send_text_message_printf(std::shared_ptr<TARGET> t, const char* format, ...) {
|
||||
va_list va;
|
||||
va_start(va, format);
|
||||
std::string buf = string_vprintf(format, va);
|
||||
va_end(va);
|
||||
std::u16string decoded = decode_sjis(buf);
|
||||
return send_text_message(t, decoded.c_str());
|
||||
}
|
||||
|
||||
void send_info_board(std::shared_ptr<Client> c, std::shared_ptr<Lobby> l);
|
||||
|
||||
void send_card_search_result(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c,
|
||||
std::shared_ptr<Client> result, std::shared_ptr<Lobby> result_lobby);
|
||||
|
||||
void send_guild_card(std::shared_ptr<Client> c, std::shared_ptr<Client> source);
|
||||
void send_menu(std::shared_ptr<Client> c, const char16_t* menu_name,
|
||||
uint32_t menu_id, const std::vector<MenuItem>& items, bool is_info_menu);
|
||||
void send_game_menu(std::shared_ptr<Client> c, std::shared_ptr<ServerState> s);
|
||||
void send_quest_menu(std::shared_ptr<Client> c, uint32_t menu_id,
|
||||
const std::vector<std::shared_ptr<const Quest>>& quests, bool is_download_menu);
|
||||
void send_quest_menu(std::shared_ptr<Client> c, uint32_t menu_id,
|
||||
const std::vector<MenuItem>& items, bool is_download_menu);
|
||||
void send_lobby_list(std::shared_ptr<Client> c, std::shared_ptr<ServerState> s);
|
||||
|
||||
void send_join_lobby(std::shared_ptr<Client> c, std::shared_ptr<Lobby> l);
|
||||
void send_player_join_notification(std::shared_ptr<Client> c,
|
||||
std::shared_ptr<Lobby> l, std::shared_ptr<Client> joining_client);
|
||||
void send_player_leave_notification(std::shared_ptr<Lobby> l,
|
||||
uint8_t leaving_client_id);
|
||||
void send_get_player_info(std::shared_ptr<Client> c);
|
||||
|
||||
void send_arrow_update(std::shared_ptr<Lobby> l);
|
||||
void send_resume_game(std::shared_ptr<Lobby> l,
|
||||
std::shared_ptr<Client> ready_client);
|
||||
|
||||
enum PlayerStatsChange {
|
||||
SubtractHP = 0,
|
||||
SubtractTP = 1,
|
||||
SubtractMeseta = 2,
|
||||
AddHP = 3,
|
||||
AddTP = 4,
|
||||
};
|
||||
|
||||
void send_player_stats_change(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c,
|
||||
PlayerStatsChange which, uint32_t amount);
|
||||
void send_warp(std::shared_ptr<Client> c, uint32_t area);
|
||||
|
||||
void send_drop_item(std::shared_ptr<Lobby> l, const ItemData& item,
|
||||
bool from_enemy, uint8_t area, float x, float y, uint16_t request_id);
|
||||
void send_drop_stacked_item(std::shared_ptr<Lobby> l, const ItemData& item,
|
||||
uint8_t area, float x, float y);
|
||||
void send_pick_up_item(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c, uint32_t id,
|
||||
uint8_t area);
|
||||
void send_create_inventory_item(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c,
|
||||
const ItemData& item);
|
||||
void send_destroy_item(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c,
|
||||
uint32_t item_id, uint32_t amount);
|
||||
void send_bank(std::shared_ptr<Client> c);
|
||||
void send_shop(std::shared_ptr<Client> c, uint8_t shop_type);
|
||||
void send_level_up(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c);
|
||||
void send_give_experience(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c,
|
||||
uint32_t amount);
|
||||
void send_ep3_card_list_update(std::shared_ptr<Client> c);
|
||||
void send_ep3_rank_update(std::shared_ptr<Client> c);
|
||||
void send_ep3_map_list(std::shared_ptr<Lobby> l);
|
||||
void send_ep3_map_data(std::shared_ptr<Lobby> l, uint32_t map_id);
|
||||
|
||||
void send_quest_file(std::shared_ptr<Client> c, const std::string& basename,
|
||||
const std::string& contents, bool is_download_quest, bool is_ep3_quest);
|
||||
|
||||
void send_server_time(std::shared_ptr<Client> c);
|
||||
|
||||
void send_change_event(std::shared_ptr<Client> c, uint8_t new_event);
|
||||
void send_change_event(std::shared_ptr<Lobby> l, uint8_t new_event);
|
||||
void send_change_event(std::shared_ptr<ServerState> s, uint8_t new_event);
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
#include "Server.hh"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/bufferevent.h>
|
||||
#include <event2/event.h>
|
||||
#include <event2/listener.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/Network.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "PSOProtocol.hh"
|
||||
#include "ReceiveCommands.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
void Server::disconnect_client(struct bufferevent* bev) {
|
||||
this->disconnect_client(this->bev_to_client.at(bev));
|
||||
}
|
||||
|
||||
void Server::disconnect_client(shared_ptr<Client> c) {
|
||||
this->bev_to_client.erase(c->bev);
|
||||
struct bufferevent* bev = c->bev;
|
||||
c->bev = NULL;
|
||||
|
||||
// if the output buffer is not empty, move the client into the draining pool
|
||||
// instead of disconnecting it, to make sure all the data gets sent
|
||||
struct evbuffer* out_buffer = bufferevent_get_output(bev);
|
||||
if (evbuffer_get_length(out_buffer) == 0) {
|
||||
bufferevent_free(bev);
|
||||
} else {
|
||||
// the callbacks will free it when all the data is sent or the client
|
||||
// disconnects
|
||||
bufferevent_setcb(bev, NULL,
|
||||
Server::dispatch_on_disconnecting_client_output,
|
||||
Server::dispatch_on_disconnecting_client_error, this);
|
||||
bufferevent_disable(bev, EV_READ);
|
||||
}
|
||||
|
||||
process_disconnect(this->state, c);
|
||||
}
|
||||
|
||||
void Server::dispatch_on_listen_accept(
|
||||
struct evconnlistener* listener, evutil_socket_t fd,
|
||||
struct sockaddr* address, int socklen, void* ctx) {
|
||||
reinterpret_cast<Server*>(ctx)->on_listen_accept(listener, fd, address,
|
||||
socklen);
|
||||
}
|
||||
|
||||
void Server::dispatch_on_listen_error(struct evconnlistener* listener,
|
||||
void* ctx) {
|
||||
reinterpret_cast<Server*>(ctx)->on_listen_error(listener);
|
||||
}
|
||||
|
||||
void Server::dispatch_on_client_input(struct bufferevent* bev, void* ctx) {
|
||||
reinterpret_cast<Server*>(ctx)->on_client_input(bev);
|
||||
}
|
||||
|
||||
void Server::dispatch_on_client_error(struct bufferevent* bev, short events,
|
||||
void* ctx) {
|
||||
reinterpret_cast<Server*>(ctx)->on_client_error(bev, events);
|
||||
}
|
||||
|
||||
void Server::dispatch_on_disconnecting_client_output(struct bufferevent* bev,
|
||||
void* ctx) {
|
||||
reinterpret_cast<Server*>(ctx)->on_disconnecting_client_output(bev);
|
||||
}
|
||||
|
||||
void Server::dispatch_on_disconnecting_client_error(struct bufferevent* bev,
|
||||
short events, void* ctx) {
|
||||
reinterpret_cast<Server*>(ctx)->on_disconnecting_client_error(bev, events);
|
||||
}
|
||||
|
||||
void Server::on_listen_accept(struct evconnlistener* listener,
|
||||
evutil_socket_t fd, struct sockaddr*, int) {
|
||||
|
||||
int listen_fd = evconnlistener_get_fd(listener);
|
||||
ListeningSocket* listening_socket;
|
||||
try {
|
||||
listening_socket = &this->listening_sockets.at(listen_fd);
|
||||
} catch (const out_of_range& e) {
|
||||
log(WARNING, "[Server] can\'t determine version for socket %d; disconnecting client",
|
||||
listen_fd);
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
log(INFO, "[Server] client connected via fd %d", listen_fd);
|
||||
|
||||
struct bufferevent *bev = bufferevent_socket_new(this->base.get(), fd,
|
||||
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
|
||||
shared_ptr<Client> c(new Client(bev, listening_socket->version,
|
||||
listening_socket->behavior));
|
||||
this->bev_to_client.emplace(make_pair(bev, c));
|
||||
|
||||
bufferevent_setcb(bev, &Server::dispatch_on_client_input, NULL,
|
||||
&Server::dispatch_on_client_error, this);
|
||||
bufferevent_enable(bev, EV_READ | EV_WRITE);
|
||||
|
||||
process_connect(this->state, c);
|
||||
}
|
||||
|
||||
void Server::on_listen_error(struct evconnlistener* listener) {
|
||||
int err = EVUTIL_SOCKET_ERROR();
|
||||
log(ERROR, "[Server] failure on listening socket %d: %d (%s)",
|
||||
evconnlistener_get_fd(listener), err, evutil_socket_error_to_string(err));
|
||||
event_base_loopexit(this->base.get(), NULL);
|
||||
}
|
||||
|
||||
void Server::on_client_input(struct bufferevent* bev) {
|
||||
shared_ptr<Client> c;
|
||||
try {
|
||||
c = this->bev_to_client.at(bev);
|
||||
} catch (const out_of_range& e) {
|
||||
log(WARNING, "[Server] received message from client with no configuration");
|
||||
|
||||
// ignore all the data
|
||||
// TODO: we probably should disconnect them or something
|
||||
struct evbuffer* in_buffer = bufferevent_get_input(bev);
|
||||
evbuffer_drain(in_buffer, evbuffer_get_length(in_buffer));
|
||||
return;
|
||||
}
|
||||
|
||||
if (c->should_disconnect) {
|
||||
this->disconnect_client(bev);
|
||||
return;
|
||||
}
|
||||
|
||||
c->last_recv_time = now();
|
||||
this->receive_and_process_commands(c);
|
||||
|
||||
if (c->should_disconnect) {
|
||||
this->disconnect_client(bev);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Server::on_disconnecting_client_output(struct bufferevent* bev) {
|
||||
bufferevent_free(bev);
|
||||
}
|
||||
|
||||
void Server::on_client_error(struct bufferevent* bev, short events) {
|
||||
if (events & BEV_EVENT_ERROR) {
|
||||
int err = EVUTIL_SOCKET_ERROR();
|
||||
log(WARNING, "[Server] client caused %d (%s)", err,
|
||||
evutil_socket_error_to_string(err));
|
||||
}
|
||||
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
|
||||
this->disconnect_client(bev);
|
||||
}
|
||||
}
|
||||
|
||||
void Server::on_disconnecting_client_error(struct bufferevent* bev,
|
||||
short events) {
|
||||
if (events & BEV_EVENT_ERROR) {
|
||||
int err = EVUTIL_SOCKET_ERROR();
|
||||
log(WARNING, "[Server] disconnecting client caused %d (%s)", err,
|
||||
evutil_socket_error_to_string(err));
|
||||
}
|
||||
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
|
||||
bufferevent_free(bev);
|
||||
}
|
||||
}
|
||||
|
||||
void Server::receive_and_process_commands(shared_ptr<Client> c) {
|
||||
struct evbuffer* buf = bufferevent_get_input(c->bev);
|
||||
size_t header_size = (c->version == GameVersion::BB) ? 8 : 4;
|
||||
|
||||
// read as much data into recv_buffer as we can and decrypt it
|
||||
size_t existing_bytes = c->recv_buffer.size();
|
||||
size_t new_bytes = evbuffer_get_length(buf);
|
||||
new_bytes &= ~(header_size - 1); // only read in multiples of header_size
|
||||
c->recv_buffer.resize(existing_bytes + new_bytes);
|
||||
void* recv_ptr = const_cast<char*>(c->recv_buffer.data() + existing_bytes);
|
||||
if (evbuffer_remove(buf, recv_ptr, new_bytes) != static_cast<ssize_t>(new_bytes)) {
|
||||
throw runtime_error("some bytes could not be read from the receive buffer");
|
||||
}
|
||||
|
||||
// decrypt the received data if encryption is enabled
|
||||
if (c->crypt_in.get()) {
|
||||
c->crypt_in->decrypt(recv_ptr, new_bytes);
|
||||
}
|
||||
|
||||
// process as many commands as possible
|
||||
size_t offset = 0;
|
||||
while (offset < c->recv_buffer.size()) {
|
||||
const PSOCommandHeader* header = reinterpret_cast<const PSOCommandHeader*>(
|
||||
c->recv_buffer.data() + offset);
|
||||
size_t size = header->size(c->version);
|
||||
if (offset + size > c->recv_buffer.size()) {
|
||||
break; // don't have a complete command; we're done for now
|
||||
}
|
||||
|
||||
// if we get here, then we have a complete, decrypted command waiting to be
|
||||
// processed. we copy it out and append zeroes on the end so that it's safe
|
||||
// to call string functions on the buffer in command handlers
|
||||
string data = c->recv_buffer.substr(offset + header_size, size - header_size);
|
||||
data.append(4, '\0');
|
||||
try {
|
||||
process_command(this->state, c, header->command(c->version),
|
||||
header->flag(c->version), size - header_size, data.data());
|
||||
} catch (const exception& e) {
|
||||
log(INFO, "[Server] error in client stream: %s", e.what());
|
||||
c->should_disconnect = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// BB pads commands to 8-byte boundaries, so if we see a shorter command,
|
||||
// skip over the padding
|
||||
offset += (size + header_size - 1) & ~(header_size - 1);
|
||||
}
|
||||
|
||||
// remove the processed commands from the receive buffer
|
||||
c->recv_buffer = c->recv_buffer.substr(offset);
|
||||
}
|
||||
|
||||
Server::Server(shared_ptr<struct event_base> base,
|
||||
shared_ptr<ServerState> state) : base(base), state(state) { }
|
||||
|
||||
void Server::listen(const string& socket_path, GameVersion version,
|
||||
ServerBehavior behavior) {
|
||||
int fd = ::listen(socket_path, 0, SOMAXCONN);
|
||||
log(INFO, "[Server] listening on unix socket %s (version %s) on fd %d",
|
||||
socket_path.c_str(), name_for_version(version), fd);
|
||||
this->add_socket(fd, version, behavior);
|
||||
}
|
||||
|
||||
void Server::listen(const string& addr, int port, GameVersion version,
|
||||
ServerBehavior behavior) {
|
||||
int fd = ::listen(addr, port, SOMAXCONN);
|
||||
string netloc_str = render_netloc(addr, port);
|
||||
log(INFO, "[Server] listening on tcp interface %s (version %s) on fd %d",
|
||||
netloc_str.c_str(), name_for_version(version), fd);
|
||||
this->add_socket(fd, version, behavior);
|
||||
}
|
||||
|
||||
void Server::listen(int port, GameVersion version, ServerBehavior behavior) {
|
||||
this->listen("", port, version, behavior);
|
||||
}
|
||||
|
||||
Server::ListeningSocket::ListeningSocket(Server* s, int fd,
|
||||
GameVersion version, ServerBehavior behavior) :
|
||||
fd(fd), version(version), behavior(behavior), listener(
|
||||
evconnlistener_new(s->base.get(), Server::dispatch_on_listen_accept, s,
|
||||
LEV_OPT_REUSEABLE, 0, this->fd), evconnlistener_free) {
|
||||
evconnlistener_set_error_cb(this->listener.get(),
|
||||
Server::dispatch_on_listen_error);
|
||||
}
|
||||
|
||||
void Server::add_socket(int fd, GameVersion version, ServerBehavior behavior) {
|
||||
this->listening_sockets.emplace(piecewise_construct, forward_as_tuple(fd),
|
||||
forward_as_tuple(this, fd, version, behavior));
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
#include "Client.hh"
|
||||
#include "ServerState.hh"
|
||||
|
||||
|
||||
|
||||
class Server {
|
||||
public:
|
||||
Server() = delete;
|
||||
Server(const Server&) = delete;
|
||||
Server(Server&&) = delete;
|
||||
Server(std::shared_ptr<struct event_base> base,
|
||||
std::shared_ptr<ServerState> state);
|
||||
virtual ~Server() = default;
|
||||
|
||||
void listen(const std::string& socket_path, GameVersion version, ServerBehavior initial_state);
|
||||
void listen(const std::string& addr, int port, GameVersion version, ServerBehavior initial_state);
|
||||
void listen(int port, GameVersion version, ServerBehavior initial_state);
|
||||
void add_socket(int fd, GameVersion version, ServerBehavior initial_state);
|
||||
|
||||
private:
|
||||
std::shared_ptr<struct event_base> base;
|
||||
|
||||
struct ListeningSocket {
|
||||
int fd;
|
||||
GameVersion version;
|
||||
ServerBehavior behavior;
|
||||
std::unique_ptr<struct evconnlistener, void(*)(struct evconnlistener*)> listener;
|
||||
|
||||
ListeningSocket(Server* s, int fd, GameVersion version,
|
||||
ServerBehavior behavior);
|
||||
};
|
||||
std::unordered_map<int, ListeningSocket> listening_sockets;
|
||||
std::unordered_map<struct bufferevent*, std::shared_ptr<Client>> bev_to_client;
|
||||
|
||||
std::shared_ptr<ServerState> state;
|
||||
|
||||
static void dispatch_on_listen_accept(struct evconnlistener* listener,
|
||||
evutil_socket_t fd, struct sockaddr *address, int socklen, void* ctx);
|
||||
static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx);
|
||||
static void dispatch_on_client_input(struct bufferevent* bev, void* ctx);
|
||||
static void dispatch_on_client_error(struct bufferevent* bev, short events,
|
||||
void* ctx);
|
||||
static void dispatch_on_disconnecting_client_output(struct bufferevent* bev,
|
||||
void* ctx);
|
||||
static void dispatch_on_disconnecting_client_error(struct bufferevent* bev,
|
||||
short events, void* ctx);
|
||||
|
||||
void disconnect_client(struct bufferevent* bev);
|
||||
void disconnect_client(std::shared_ptr<Client> c);
|
||||
|
||||
void on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd,
|
||||
struct sockaddr *address, int socklen);
|
||||
void on_listen_error(struct evconnlistener* listener);
|
||||
void on_client_input(struct bufferevent* bev);
|
||||
void on_client_error(struct bufferevent* bev, short events);
|
||||
void on_disconnecting_client_output(struct bufferevent* bev);
|
||||
void on_disconnecting_client_error(struct bufferevent* bev, short events);
|
||||
|
||||
void receive_and_process_commands(std::shared_ptr<Client> c);
|
||||
};
|
||||
@@ -0,0 +1,187 @@
|
||||
#include "ServerShell.hh"
|
||||
|
||||
#include <event2/event.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <phosg/Strings.hh>
|
||||
|
||||
#include "ChatCommands.hh"
|
||||
#include "ServerState.hh"
|
||||
#include "SendCommands.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
ServerShell::ServerShell(std::shared_ptr<struct event_base> base,
|
||||
std::shared_ptr<ServerState> state) : Shell(base, state) { }
|
||||
|
||||
void ServerShell::print_prompt() {
|
||||
fwrite("newserv> ", 9, 1, stdout);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
void ServerShell::execute_command(const string& command) {
|
||||
// find the entry in the command table and run the command
|
||||
size_t command_end = skip_non_whitespace(command, 0);
|
||||
size_t args_begin = skip_whitespace(command, command_end);
|
||||
string command_name = command.substr(0, command_end);
|
||||
string command_args = command.substr(args_begin);
|
||||
|
||||
if (command_name == "exit") {
|
||||
throw exit_shell();
|
||||
|
||||
} else if (command_name == "help") {
|
||||
fprintf(stderr, "\
|
||||
commands:\n\
|
||||
help\n\
|
||||
you\'re reading it now\n\
|
||||
exit (or ctrl+d)\n\
|
||||
shut down the server\n\
|
||||
reload <item> ...\n\
|
||||
reload data. <item> can be licenses, battle-params, level-table, or quests.\n\
|
||||
add-license <parameters>\n\
|
||||
add a license to the server. <parameters> is some subset of the following:\n\
|
||||
username=<username> (bb username)\n\
|
||||
bb-password=<password> (bb password)\n\
|
||||
gc-password=<password> (gc password)\n\
|
||||
access-key=<access-key> (gc/pc access key)\n\
|
||||
serial=<serial-number> (gc/pc serial number; required for all licenses)\n\
|
||||
privileges=<privilege-mask> (can be normal, mod, admin, root, or numeric)\n\
|
||||
delete-license <serial-number>\n\
|
||||
delete a license from the server\n\
|
||||
list-licenses\n\
|
||||
list all licenses registered on the server\n\
|
||||
set-allow-unregistered-users <true/false>\n\
|
||||
enable or disable allowing unregistered users on the server. disabling this\n\
|
||||
does not disconnect unregistered users who are already connected.\n\
|
||||
set-event <event>\n\
|
||||
set the event in all lobbies\n\
|
||||
announce <message>\n\
|
||||
send an announcement message to all players\n\
|
||||
");
|
||||
|
||||
} else if (command_name == "reload") {
|
||||
auto types = split(command_args, ' ');
|
||||
if (types.empty()) {
|
||||
throw invalid_argument("no data type given");
|
||||
}
|
||||
for (const string& type : types) {
|
||||
if (type == "licenses") {
|
||||
shared_ptr<LicenseManager> lm(new LicenseManager("system/licenses.nsi"));
|
||||
this->state->license_manager = lm;
|
||||
} else if (type == "battle-params") {
|
||||
shared_ptr<BattleParamTable> bpt(new BattleParamTable("system/blueburst/BattleParamEntry"));
|
||||
this->state->battle_params = bpt;
|
||||
} else if (type == "level-table") {
|
||||
shared_ptr<LevelTable> lt(new LevelTable("system/blueburst/PlyLevelTbl.prs", true));
|
||||
this->state->level_table = lt;
|
||||
} else if (type == "quests") {
|
||||
shared_ptr<QuestIndex> qi(new QuestIndex("system/quests"));
|
||||
this->state->quest_index = qi;
|
||||
} else {
|
||||
throw invalid_argument("incorrect data type");
|
||||
}
|
||||
}
|
||||
|
||||
} else if (command_name == "add-license") {
|
||||
shared_ptr<License> l(new License());
|
||||
memset(l.get(), 0, sizeof(License));
|
||||
|
||||
for (const string& token : split(command_args, ' ')) {
|
||||
if (starts_with(token, "username=")) {
|
||||
if (token.size() >= 29) {
|
||||
throw invalid_argument("username too long");
|
||||
}
|
||||
strcpy(l->username, token.c_str() + 9);
|
||||
|
||||
} else if (starts_with(token, "bb-password=")) {
|
||||
if (token.size() >= 32) {
|
||||
throw invalid_argument("bb-password too long");
|
||||
}
|
||||
strcpy(l->bb_password, token.c_str() + 12);
|
||||
|
||||
} else if (starts_with(token, "gc-password=")) {
|
||||
if (token.size() > 20) {
|
||||
throw invalid_argument("gc-password too long");
|
||||
}
|
||||
strcpy(l->gc_password, token.c_str() + 12);
|
||||
|
||||
} else if (starts_with(token, "access-key=")) {
|
||||
if (token.size() > 23) {
|
||||
throw invalid_argument("access-key is too long");
|
||||
}
|
||||
strcpy(l->access_key, token.c_str() + 11);
|
||||
|
||||
} else if (starts_with(token, "serial=")) {
|
||||
l->serial_number = stoul(token.substr(7));
|
||||
|
||||
} else if (starts_with(token, "privileges=")) {
|
||||
string mask = token.substr(11);
|
||||
if (mask == "normal") {
|
||||
l->privileges = 0;
|
||||
} else if (mask == "mod") {
|
||||
l->privileges = Privilege::Moderator;
|
||||
} else if (mask == "admin") {
|
||||
l->privileges = Privilege::Administrator;
|
||||
} else if (mask == "root") {
|
||||
l->privileges = Privilege::Root;
|
||||
} else {
|
||||
l->privileges = stoul(mask);
|
||||
}
|
||||
|
||||
} else {
|
||||
throw invalid_argument("incorrect field");
|
||||
}
|
||||
}
|
||||
|
||||
if (!l->serial_number) {
|
||||
throw invalid_argument("license does not contain serial number");
|
||||
}
|
||||
|
||||
this->state->license_manager->add(l);
|
||||
fprintf(stderr, "license added\n");
|
||||
|
||||
} else if (command_name == "delete-license") {
|
||||
uint32_t serial_number = stoul(command_args);
|
||||
this->state->license_manager->remove(serial_number);
|
||||
fprintf(stderr, "license deleted\n");
|
||||
|
||||
} else if (command_name == "list-licenses") {
|
||||
for (const auto& l : this->state->license_manager->snapshot()) {
|
||||
string s = l.str();
|
||||
fprintf(stderr, "%s\n", s.c_str());
|
||||
}
|
||||
|
||||
} else if (command_name == "set-allow-unregistered-users") {
|
||||
if (command_args == "true") {
|
||||
this->state->allow_unregistered_users = true;
|
||||
} else if (command_args == "false") {
|
||||
this->state->allow_unregistered_users = false;
|
||||
} else {
|
||||
throw invalid_argument("argument must be true or false");
|
||||
}
|
||||
fprintf(stderr, "unregistered users are now %s\n",
|
||||
this->state->allow_unregistered_users ? "allowed" : "disallowed");
|
||||
|
||||
} else if (command_name == "set-event") {
|
||||
uint8_t event_id = event_for_name(command_args);
|
||||
if (event_id == 0xFF) {
|
||||
throw invalid_argument("invalid event");
|
||||
}
|
||||
|
||||
this->state->pre_lobby_event = event_id;
|
||||
for (const auto& l : this->state->all_lobbies()) {
|
||||
l->event = event_id;
|
||||
}
|
||||
send_change_event(this->state, event_id);
|
||||
|
||||
} else if (command_name == "announce") {
|
||||
u16string message16 = decode_sjis(command_args);
|
||||
send_text_message(this->state, message16.c_str());
|
||||
|
||||
} else {
|
||||
throw invalid_argument("unknown command; try \'help\'");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
#include "Shell.hh"
|
||||
|
||||
|
||||
|
||||
class ServerShell : public Shell {
|
||||
public:
|
||||
ServerShell(std::shared_ptr<struct event_base> base,
|
||||
std::shared_ptr<ServerState> state);
|
||||
virtual ~ServerShell() = default;
|
||||
ServerShell(const ServerShell&) = delete;
|
||||
ServerShell(ServerShell&&) = delete;
|
||||
ServerShell& operator=(const ServerShell&) = delete;
|
||||
ServerShell& operator=(ServerShell&&) = delete;
|
||||
|
||||
protected:
|
||||
virtual void print_prompt();
|
||||
virtual void execute_command(const std::string& command);
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
#include "ServerState.hh"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "SendCommands.hh"
|
||||
#include "NetworkAddresses.hh"
|
||||
#include "Text.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
ServerState::ServerState() : run_dns_server(true),
|
||||
allow_unregistered_users(false),
|
||||
run_shell_behavior(RunShellBehavior::Default), next_lobby_id(1),
|
||||
pre_lobby_event(0) {
|
||||
memset(&this->default_key_file, 0, sizeof(this->default_key_file));
|
||||
|
||||
this->main_menu.emplace_back(MAIN_MENU_GO_TO_LOBBY, u"Go to lobby",
|
||||
u"Join the lobby.", 0);
|
||||
this->main_menu.emplace_back(MAIN_MENU_INFORMATION, u"Information",
|
||||
u"View server information.", MenuItemFlag::RequiresMessageBoxes);
|
||||
this->main_menu.emplace_back(MAIN_MENU_DISCONNECT, u"Disconnect",
|
||||
u"Disconnect.", 0);
|
||||
|
||||
for (size_t x = 0; x < 20; x++) {
|
||||
auto lobby_name = decode_sjis(string_printf("LOBBY%zu", x + 1));
|
||||
shared_ptr<Lobby> l(new Lobby());
|
||||
l->flags |= LobbyFlag::Public | LobbyFlag::Default | LobbyFlag::Persistent |
|
||||
((x > 14) ? LobbyFlag::Episode3 : 0);
|
||||
l->block = x + 1;
|
||||
l->type = x;
|
||||
char16cpy(l->name, lobby_name.c_str(), 0x24);
|
||||
l->max_clients = 12;
|
||||
this->add_lobby(l);
|
||||
}
|
||||
}
|
||||
|
||||
void ServerState::add_client_to_available_lobby(shared_ptr<Client> c) {
|
||||
auto it = this->id_to_lobby.lower_bound(0);
|
||||
for (; it != this->id_to_lobby.end(); it++) {
|
||||
if (!(it->second->flags & LobbyFlag::Public)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
it->second->add_client(c);
|
||||
break;
|
||||
} catch (const out_of_range&) { }
|
||||
}
|
||||
|
||||
if (it == this->id_to_lobby.end()) {
|
||||
throw out_of_range("all lobbies full");
|
||||
}
|
||||
|
||||
// send a join message to the joining player, and notifications to all others
|
||||
this->send_lobby_join_notifications(it->second, c);
|
||||
}
|
||||
|
||||
void ServerState::remove_client_from_lobby(shared_ptr<Client> c) {
|
||||
auto l = this->id_to_lobby.at(c->lobby_id);
|
||||
l->remove_client(c);
|
||||
send_player_leave_notification(l, c->lobby_client_id);
|
||||
}
|
||||
|
||||
void ServerState::change_client_lobby(shared_ptr<Client> c, shared_ptr<Lobby> new_lobby) {
|
||||
uint8_t old_lobby_client_id = c->lobby_client_id;
|
||||
|
||||
shared_ptr<Lobby> current_lobby = this->find_lobby(c->lobby_id);
|
||||
try {
|
||||
if (current_lobby) {
|
||||
current_lobby->move_client_to_lobby(new_lobby, c);
|
||||
} else {
|
||||
new_lobby->add_client(c);
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
send_lobby_message_box(c, u"$C6Can't change lobby\n\n$C7The lobby is full.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (current_lobby) {
|
||||
send_player_leave_notification(current_lobby, old_lobby_client_id);
|
||||
if (!(current_lobby->flags & LobbyFlag::Persistent) && (current_lobby->count_clients() == 0)) {
|
||||
this->remove_lobby(current_lobby->lobby_id);
|
||||
}
|
||||
}
|
||||
this->send_lobby_join_notifications(new_lobby, c);
|
||||
}
|
||||
|
||||
void ServerState::send_lobby_join_notifications(shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> joining_client) {
|
||||
for (auto& other_client : l->clients) {
|
||||
if (!other_client) {
|
||||
continue;
|
||||
} else if (other_client == joining_client) {
|
||||
send_join_lobby(joining_client, l);
|
||||
} else {
|
||||
send_player_join_notification(other_client, l, joining_client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<Lobby> ServerState::find_lobby(uint32_t lobby_id) {
|
||||
return this->id_to_lobby.at(lobby_id);
|
||||
}
|
||||
|
||||
vector<shared_ptr<Lobby>> ServerState::all_lobbies() {
|
||||
vector<shared_ptr<Lobby>> ret;
|
||||
for (auto& it : this->id_to_lobby) {
|
||||
ret.emplace_back(it.second);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ServerState::add_lobby(shared_ptr<Lobby> l) {
|
||||
l->lobby_id = this->next_lobby_id++;
|
||||
if (this->id_to_lobby.count(l->lobby_id)) {
|
||||
throw logic_error("lobby already exists with the given id");
|
||||
}
|
||||
this->id_to_lobby.emplace(l->lobby_id, l);
|
||||
}
|
||||
|
||||
void ServerState::remove_lobby(uint32_t lobby_id) {
|
||||
this->id_to_lobby.erase(lobby_id);
|
||||
}
|
||||
|
||||
shared_ptr<Client> ServerState::find_client(const char16_t* identifier,
|
||||
uint64_t serial_number, shared_ptr<Lobby> l) {
|
||||
|
||||
if ((serial_number == 0) && identifier) {
|
||||
try {
|
||||
string encoded = encode_sjis(identifier);
|
||||
serial_number = stoull(encoded, NULL, 0);
|
||||
} catch (const exception&) { }
|
||||
}
|
||||
|
||||
// look in the current lobby first
|
||||
if (l) {
|
||||
try {
|
||||
return l->find_client(identifier, serial_number);
|
||||
} catch (const out_of_range&) { }
|
||||
}
|
||||
|
||||
// look in all lobbies if not found
|
||||
for (auto& other_l : this->all_lobbies()) {
|
||||
if (l == other_l) {
|
||||
continue; // don't bother looking again
|
||||
}
|
||||
try {
|
||||
return other_l->find_client(identifier, serial_number);
|
||||
} catch (const out_of_range&) { }
|
||||
}
|
||||
|
||||
throw out_of_range("client not found");
|
||||
}
|
||||
|
||||
uint32_t ServerState::connect_address_for_client(std::shared_ptr<Client> c) {
|
||||
// TODO: we can do something much smarter here, like use the sockname to find
|
||||
// out which interface the client is connected to, and return that address
|
||||
if (is_local_address(c->remote_addr)) {
|
||||
return this->local_address;
|
||||
} else {
|
||||
return this->external_address;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <phosg/Concurrency.hh>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
|
||||
#include "Client.hh"
|
||||
#include "Items.hh"
|
||||
#include "LevelTable.hh"
|
||||
#include "License.hh"
|
||||
#include "Lobby.hh"
|
||||
#include "Menu.hh"
|
||||
#include "Quest.hh"
|
||||
|
||||
|
||||
|
||||
struct PortConfiguration {
|
||||
uint16_t port;
|
||||
GameVersion version;
|
||||
ServerBehavior behavior;
|
||||
};
|
||||
|
||||
struct ServerState {
|
||||
enum class RunShellBehavior {
|
||||
Default = 0,
|
||||
Always,
|
||||
Never,
|
||||
};
|
||||
|
||||
std::u16string name;
|
||||
std::unordered_map<std::string, PortConfiguration> port_configuration;
|
||||
std::string username;
|
||||
bool run_dns_server;
|
||||
bool allow_unregistered_users;
|
||||
RunShellBehavior run_shell_behavior;
|
||||
PSOBBEncryption::KeyFile default_key_file;
|
||||
std::shared_ptr<const QuestIndex> quest_index;
|
||||
std::shared_ptr<const LevelTable> level_table;
|
||||
std::shared_ptr<const BattleParamTable> battle_params;
|
||||
std::shared_ptr<const CommonItemCreator> common_item_creator;
|
||||
|
||||
std::shared_ptr<LicenseManager> license_manager;
|
||||
|
||||
std::vector<MenuItem> main_menu;
|
||||
std::shared_ptr<std::vector<MenuItem>> information_menu;
|
||||
std::shared_ptr<std::vector<std::u16string>> information_contents;
|
||||
std::u16string welcome_message;
|
||||
|
||||
std::map<int64_t, std::shared_ptr<Lobby>> id_to_lobby;
|
||||
std::atomic<int32_t> next_lobby_id;
|
||||
uint8_t pre_lobby_event;
|
||||
|
||||
std::map<std::string, uint32_t> all_addresses;
|
||||
uint32_t local_address;
|
||||
uint32_t external_address;
|
||||
|
||||
ServerState();
|
||||
|
||||
void add_client_to_available_lobby(std::shared_ptr<Client> c);
|
||||
void remove_client_from_lobby(std::shared_ptr<Client> c);
|
||||
void change_client_lobby(std::shared_ptr<Client> c,
|
||||
std::shared_ptr<Lobby> new_lobby);
|
||||
|
||||
void send_lobby_join_notifications(std::shared_ptr<Lobby> l,
|
||||
std::shared_ptr<Client> joining_client);
|
||||
|
||||
std::shared_ptr<Lobby> find_lobby(uint32_t lobby_id);
|
||||
std::vector<std::shared_ptr<Lobby>> all_lobbies();
|
||||
|
||||
void add_lobby(std::shared_ptr<Lobby> l);
|
||||
void remove_lobby(uint32_t lobby_id);
|
||||
|
||||
std::shared_ptr<Client> find_client(const char16_t* identifier = NULL,
|
||||
uint64_t serial_number = 0, std::shared_ptr<Lobby> l = NULL);
|
||||
|
||||
uint32_t connect_address_for_client(std::shared_ptr<Client> c);
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
#include "Shell.hh"
|
||||
|
||||
#include <event2/event.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <phosg/Strings.hh>
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
Shell::exit_shell::exit_shell() : runtime_error("shell exited") { }
|
||||
|
||||
|
||||
|
||||
Shell::Shell(std::shared_ptr<struct event_base> base,
|
||||
std::shared_ptr<ServerState> state) : base(base), state(state),
|
||||
read_event(event_new(this->base.get(), 0, EV_READ | EV_PERSIST,
|
||||
&Shell::dispatch_read_stdin, this), event_free),
|
||||
prompt_event(event_new(this->base.get(), 0, EV_TIMEOUT,
|
||||
&Shell::dispatch_print_prompt, this), event_free) {
|
||||
event_add(this->read_event.get(), NULL);
|
||||
|
||||
// schedule an event to print the prompt as soon as the event loop starts
|
||||
// running. we do this so the prompt appears after any initialization
|
||||
// messages that come after starting the shell
|
||||
struct timeval tv = {0, 0};
|
||||
event_add(this->prompt_event.get(), &tv);
|
||||
|
||||
this->poll.add(0, POLLIN);
|
||||
}
|
||||
|
||||
void Shell::dispatch_print_prompt(evutil_socket_t, short, void* ctx) {
|
||||
reinterpret_cast<Shell*>(ctx)->print_prompt();
|
||||
}
|
||||
|
||||
void Shell::print_prompt() {
|
||||
// default behavior: no prompt
|
||||
}
|
||||
|
||||
void Shell::dispatch_read_stdin(evutil_socket_t, short, void* ctx) {
|
||||
reinterpret_cast<Shell*>(ctx)->read_stdin();
|
||||
}
|
||||
|
||||
void Shell::read_stdin() {
|
||||
bool any_command_read = false;
|
||||
for (;;) {
|
||||
auto poll_result = this->poll.poll();
|
||||
short fd_events = 0;
|
||||
try {
|
||||
fd_events = poll_result.at(0);
|
||||
} catch (const out_of_range&) { }
|
||||
|
||||
if (!(fd_events & POLLIN)) {
|
||||
break;
|
||||
}
|
||||
|
||||
string command(2048, '\0');
|
||||
if (!fgets(const_cast<char*>(command.data()), command.size(), stdin)) {
|
||||
if (!any_command_read) {
|
||||
// ctrl+d probably; we should exit
|
||||
fputc('\n', stderr);
|
||||
event_base_loopexit(this->base.get(), NULL);
|
||||
return;
|
||||
} else {
|
||||
break; // probably not EOF; just no more commands for now
|
||||
}
|
||||
}
|
||||
|
||||
// trim the extra data off the string
|
||||
size_t len = strlen(command.c_str());
|
||||
if (len == 0) {
|
||||
break;
|
||||
}
|
||||
if (command[len - 1] == '\n') {
|
||||
len--;
|
||||
}
|
||||
command.resize(len);
|
||||
any_command_read = true;
|
||||
|
||||
try {
|
||||
execute_command(command);
|
||||
} catch (const exit_shell&) {
|
||||
event_base_loopexit(this->base.get(), NULL);
|
||||
return;
|
||||
} catch (const exception& e) {
|
||||
fprintf(stderr, "FAILED: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
this->print_prompt();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <phosg/Filesystem.hh>
|
||||
|
||||
#include "ServerState.hh"
|
||||
|
||||
|
||||
|
||||
class Shell {
|
||||
public:
|
||||
Shell(std::shared_ptr<struct event_base> base,
|
||||
std::shared_ptr<ServerState> state);
|
||||
virtual ~Shell() = default;
|
||||
Shell(const Shell&) = delete;
|
||||
Shell(Shell&&) = delete;
|
||||
Shell& operator=(const Shell&) = delete;
|
||||
Shell& operator=(Shell&&) = delete;
|
||||
|
||||
protected:
|
||||
std::shared_ptr<struct event_base> base;
|
||||
std::shared_ptr<ServerState> state;
|
||||
std::unique_ptr<struct event, void (*)(struct event*)> read_event;
|
||||
std::unique_ptr<struct event, void (*)(struct event*)> prompt_event;
|
||||
Poll poll;
|
||||
|
||||
class exit_shell : public std::runtime_error {
|
||||
public:
|
||||
exit_shell();
|
||||
~exit_shell() = default;
|
||||
};
|
||||
|
||||
static void dispatch_print_prompt(evutil_socket_t fd, short events, void* ctx);
|
||||
static void dispatch_read_stdin(evutil_socket_t fd, short events, void* ctx);
|
||||
virtual void print_prompt();
|
||||
void read_stdin();
|
||||
virtual void execute_command(const std::string& command) = 0;
|
||||
};
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
#include "Text.hh"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <vector>
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
int char16cmp(const char16_t* s1, const char16_t* s2, size_t count) {
|
||||
size_t x;
|
||||
for (x = 0; x < count && s1[x] != 0 && s2[x] != 0; x++) {
|
||||
if (s1[x] < s2[x]) {
|
||||
return -1;
|
||||
} else if (s1[x] > s2[x]) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (s1[x] < s2[x]) {
|
||||
return -1;
|
||||
} else if (s1[x] > s2[x]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void char16cpy(char16_t* dest, const char16_t* src, size_t count) {
|
||||
size_t x;
|
||||
for (x = 0; x < count && src[x] != 0; x++) {
|
||||
dest[x] = src[x];
|
||||
}
|
||||
if (x < count) {
|
||||
dest[x] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
size_t char16len(const char16_t* s) {
|
||||
size_t x;
|
||||
for (x = 0; s[x] != 0; x++);
|
||||
return x;
|
||||
}
|
||||
|
||||
|
||||
|
||||
static vector<char16_t> unicode_to_sjis_table_data;
|
||||
static vector<char16_t> sjis_to_unicode_table_data;
|
||||
|
||||
static void load_sjis_tables() {
|
||||
unicode_to_sjis_table_data.resize(0x10000);
|
||||
sjis_to_unicode_table_data.resize(0x10000);
|
||||
|
||||
// TODO: this is inefficient; it makes multiple copies of the string
|
||||
auto file_contents = load_file("system/sjis-table.ini");
|
||||
auto lines = split(file_contents, '\n');
|
||||
for (auto line : lines) {
|
||||
auto tokens = split(line, '\t');
|
||||
if (tokens.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
char16_t sjis_char = stoul(tokens[0], NULL, 0);
|
||||
char16_t unicode_char = stoul(tokens[1], NULL, 0);
|
||||
|
||||
unicode_to_sjis_table_data[unicode_char] = sjis_char;
|
||||
sjis_to_unicode_table_data[sjis_char] = unicode_char;
|
||||
}
|
||||
}
|
||||
|
||||
static const vector<char16_t>& sjis_to_unicode_table() {
|
||||
if (sjis_to_unicode_table_data.empty()) {
|
||||
load_sjis_tables();
|
||||
}
|
||||
return sjis_to_unicode_table_data;
|
||||
}
|
||||
|
||||
static const vector<char16_t>& unicode_to_sjis_table() {
|
||||
if (unicode_to_sjis_table_data.empty()) {
|
||||
load_sjis_tables();
|
||||
}
|
||||
return unicode_to_sjis_table_data;
|
||||
}
|
||||
|
||||
void encode_sjis(char* dest, const char16_t* source, size_t max) {
|
||||
const auto& table = unicode_to_sjis_table();
|
||||
while (*source && (--max)) {
|
||||
*(dest++) = table[*(source++)];
|
||||
};
|
||||
*dest = 0;
|
||||
}
|
||||
|
||||
void decode_sjis(char16_t* dest, const char* source, size_t max) {
|
||||
const auto& table = sjis_to_unicode_table();
|
||||
while (*source && (--max)) {
|
||||
char16_t src_char = *(source++);
|
||||
if (src_char & 0x80) {
|
||||
src_char = (src_char << 8) | *(source++);
|
||||
if ((src_char & 0xFF) == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
*(dest++) = table[src_char];
|
||||
};
|
||||
*dest = 0;
|
||||
}
|
||||
|
||||
std::string encode_sjis(const char16_t* source) {
|
||||
const auto& table = unicode_to_sjis_table();
|
||||
string ret;
|
||||
while (*source) {
|
||||
ret.push_back(table[*(source++)]);
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::u16string decode_sjis(const char* source) {
|
||||
const auto& table = sjis_to_unicode_table();
|
||||
u16string ret;
|
||||
while (*source) {
|
||||
char16_t src_char = *(source++);
|
||||
if (src_char & 0x80) {
|
||||
src_char = (src_char << 8) | *(source++);
|
||||
if ((src_char & 0xFF) == 0) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
ret.push_back(table[src_char]);
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string encode_sjis(const std::u16string& source) {
|
||||
const auto& table = unicode_to_sjis_table();
|
||||
string ret;
|
||||
for (char16_t ch : source) {
|
||||
ret.push_back(table[ch]);
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::u16string decode_sjis(const std::string& source) {
|
||||
const auto& table = sjis_to_unicode_table();
|
||||
u16string ret;
|
||||
for (size_t x = 0; x < source.size(); x++) {
|
||||
char16_t src_char = source[x];
|
||||
if (src_char & 0x80) {
|
||||
src_char = (src_char << 8) | source[++x];
|
||||
if ((src_char & 0xFF) == 0) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
ret.push_back(table[src_char]);
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void add_language_marker_inplace(char* a, char e, size_t dest_count) {
|
||||
if ((a[0] == '\t') && (a[1] != 'C')) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t existing_count = strlen(a);
|
||||
if (existing_count > dest_count - 3) {
|
||||
existing_count = dest_count - 3;
|
||||
}
|
||||
memmove(&a[2], a, (existing_count + 1) * sizeof(char));
|
||||
a[0] = '\t';
|
||||
a[1] = e;
|
||||
a[existing_count + 2] = 0;
|
||||
}
|
||||
|
||||
void add_language_marker_inplace(char16_t* a, char16_t e, size_t dest_count) {
|
||||
if ((a[0] == '\t') && (a[1] != 'C')) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t existing_count = char16len(a);
|
||||
if (existing_count > dest_count - 3) {
|
||||
existing_count = dest_count - 3;
|
||||
}
|
||||
memmove(&a[2], a, (existing_count + 1) * sizeof(char16_t));
|
||||
a[0] = '\t';
|
||||
a[1] = e;
|
||||
a[existing_count + 2] = 0;
|
||||
}
|
||||
|
||||
void remove_language_marker_inplace(char* a) {
|
||||
if ((a[0] == '\t') && (a[1] != 'C')) {
|
||||
strcpy(a, &a[2]);
|
||||
}
|
||||
}
|
||||
|
||||
void remove_language_marker_inplace(char16_t* a) {
|
||||
if ((a[0] == '\t') && (a[1] != 'C')) {
|
||||
char16cpy(a, &a[2], char16len(a) - 2);
|
||||
}
|
||||
}
|
||||
|
||||
std::string add_language_marker(const std::string& s, char marker) {
|
||||
if ((s.size() >= 2) && (s[0] == '\t') && (s[1] != 'C')) {
|
||||
return s;
|
||||
}
|
||||
|
||||
string ret;
|
||||
ret.push_back('\t');
|
||||
ret.push_back(marker);
|
||||
return ret + s;
|
||||
}
|
||||
|
||||
std::u16string add_language_marker(const std::u16string& s, char16_t marker) {
|
||||
if ((s.size() >= 2) && (s[0] == L'\t') && (s[1] != L'C')) {
|
||||
return s;
|
||||
}
|
||||
|
||||
u16string ret;
|
||||
ret.push_back(L'\t');
|
||||
ret.push_back(marker);
|
||||
return ret + s;
|
||||
}
|
||||
|
||||
std::string remove_language_marker(const std::string& s) {
|
||||
if ((s.size() < 2) || (s[0] != '\t') || (s[1] == 'C')) {
|
||||
return s;
|
||||
}
|
||||
return s.substr(2);
|
||||
}
|
||||
|
||||
std::u16string remove_language_marker(const std::u16string& s) {
|
||||
if ((s.size() < 2) || (s[0] != L'\t') || (s[1] == L'C')) {
|
||||
return s;
|
||||
}
|
||||
return s.substr(2);
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
|
||||
int char16cmp(const char16_t* s1, const char16_t* s2, size_t count);
|
||||
void char16cpy(char16_t* dest, const char16_t* src, size_t count);
|
||||
size_t char16len(const char16_t* s);
|
||||
|
||||
|
||||
void encode_sjis(char* dest, const char16_t* source, size_t dest_count);
|
||||
void decode_sjis(char16_t* dest, const char* source, size_t dest_count);
|
||||
std::string encode_sjis(const char16_t* source);
|
||||
std::u16string decode_sjis(const char* source);
|
||||
std::string encode_sjis(const std::u16string& source);
|
||||
std::u16string decode_sjis(const std::string& source);
|
||||
|
||||
|
||||
void add_language_marker_inplace(char* s, char marker, size_t dest_count);
|
||||
void add_language_marker_inplace(char16_t* s, char16_t marker, size_t dest_count);
|
||||
void remove_language_marker_inplace(char* s);
|
||||
void remove_language_marker_inplace(char16_t* s);
|
||||
std::string add_language_marker(const std::string& s, char marker);
|
||||
std::u16string add_language_marker(const std::u16string& s, char16_t marker);
|
||||
std::string remove_language_marker(const std::string& s);
|
||||
std::u16string remove_language_marker(const std::u16string& s);
|
||||
|
||||
|
||||
template <typename T>
|
||||
void replace_char_inplace(T* a, T f, T r) {
|
||||
while (*a) {
|
||||
if (*a == f) {
|
||||
*a = r;
|
||||
}
|
||||
a++;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
size_t add_color_inplace(T* a) {
|
||||
T* d = a;
|
||||
T* orig_d = d;
|
||||
|
||||
while (*a) {
|
||||
if (*a == '$') {
|
||||
*(d++) = '\t';
|
||||
} else if (*a == '#') {
|
||||
*(d++) = '\n';
|
||||
} else if (*a == '%') {
|
||||
a++;
|
||||
if (*a == 's') {
|
||||
*(d++) = '$';
|
||||
} else if (*a == '%') {
|
||||
*(d++) = '%';
|
||||
} else if (*a == 'n') {
|
||||
*(d++) = '#';
|
||||
} else {
|
||||
*(d++) = *a;
|
||||
}
|
||||
} else {
|
||||
*(d++) = *a;
|
||||
}
|
||||
a++;
|
||||
}
|
||||
*d = 0;
|
||||
|
||||
return d - orig_d;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void add_color_inplace(std::basic_string<T>& a, size_t header_bytes) {
|
||||
size_t count = add_color_inplace(const_cast<T*>(a.data() + header_bytes));
|
||||
a.resize(count + header_bytes);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "Version.hh"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include <strings.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
uint16_t flags_for_version(GameVersion version, uint8_t sub_version) {
|
||||
switch (sub_version) {
|
||||
case 0x00: // initial check (before 9E recognition)
|
||||
switch (version) {
|
||||
case GameVersion::DC:
|
||||
return ClientFlag::DefaultV2DC;
|
||||
case GameVersion::GC:
|
||||
return ClientFlag::DefaultV3GC;
|
||||
case GameVersion::PC:
|
||||
return ClientFlag::DefaultV2PC;
|
||||
case GameVersion::Patch:
|
||||
return ClientFlag::DefaultV2PC;
|
||||
case GameVersion::BB:
|
||||
return ClientFlag::DefaultV3BB;
|
||||
}
|
||||
break;
|
||||
case 0x29: // PSO PC
|
||||
return ClientFlag::DefaultV2PC;
|
||||
case 0x30: // ???
|
||||
case 0x31: // PSO Ep1&2 US10, US11, EU10, JP10
|
||||
case 0x33: // PSO Ep1&2 EU50HZ
|
||||
case 0x34: // PSO Ep1&2 JP11
|
||||
return ClientFlag::DefaultV3GC;
|
||||
case 0x32: // PSO Ep1&2 US12, JP12
|
||||
case 0x35: // PSO Ep1&2 US12, JP12
|
||||
case 0x36: // PSO Ep1&2 US12, JP12
|
||||
case 0x39: // PSO Ep1&2 US12, JP12
|
||||
return ClientFlag::DefaultV3GCPlus;
|
||||
case 0x40: // PSO Ep3 trial
|
||||
case 0x41: // PSO Ep3 US
|
||||
case 0x42: // PSO Ep3 JP
|
||||
case 0x43: // PSO Ep3 UK
|
||||
return ClientFlag::DefaultV4;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* name_for_version(GameVersion version) {
|
||||
switch (version) {
|
||||
case GameVersion::GC:
|
||||
return "GC";
|
||||
case GameVersion::PC:
|
||||
return "PC";
|
||||
case GameVersion::BB:
|
||||
return "BB";
|
||||
case GameVersion::DC:
|
||||
return "DC";
|
||||
case GameVersion::Patch:
|
||||
return "Patch";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
GameVersion version_for_name(const char* name) {
|
||||
if (!strcasecmp(name, "DC") || !strcasecmp(name, "DreamCast")) {
|
||||
return GameVersion::DC;
|
||||
}
|
||||
if (!strcasecmp(name, "PC")) {
|
||||
return GameVersion::PC;
|
||||
}
|
||||
if (!strcasecmp(name, "GC") || !strcasecmp(name, "GameCube")) {
|
||||
return GameVersion::GC;
|
||||
}
|
||||
if (!strcasecmp(name, "BB") || !strcasecmp(name, "BlueBurst") ||
|
||||
!strcasecmp(name, "Blue Burst")) {
|
||||
return GameVersion::BB;
|
||||
}
|
||||
throw invalid_argument("incorrect version name");
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
|
||||
|
||||
enum class GameVersion {
|
||||
DC = 0,
|
||||
PC,
|
||||
Patch,
|
||||
GC,
|
||||
BB,
|
||||
};
|
||||
|
||||
enum ClientFlag {
|
||||
// after joining a lobby, client will no longer send D6 commands when they close message boxes
|
||||
NoMessageBoxCloseConfirmationAfterLobbyJoin = 0x0004,
|
||||
// client has the above flag and has already joined a lobby
|
||||
NoMessageBoxCloseConfirmation = 0x0008,
|
||||
// client can see Ep3 lobbies
|
||||
CanSeeExtraLobbies = 0x0010,
|
||||
// client is episode 3 and should use its game mechanic
|
||||
Episode3Games = 0x0020,
|
||||
// client is DC v1 (disables some features)
|
||||
IsDCv1 = 0x0040,
|
||||
// client is loading into a game
|
||||
Loading = 0x0080,
|
||||
|
||||
// client is in the information menu (login server only)
|
||||
InInformationMenu = 0x0100,
|
||||
// client is at the welcome message (login server only)
|
||||
AtWelcomeMessage = 0x0200,
|
||||
|
||||
DefaultV1 = IsDCv1,
|
||||
DefaultV2DC = 0x0000,
|
||||
DefaultV2PC = 0x0000,
|
||||
DefaultV3GC = 0x0000,
|
||||
DefaultV3GCPlus = NoMessageBoxCloseConfirmationAfterLobbyJoin,
|
||||
DefaultV3BB = NoMessageBoxCloseConfirmationAfterLobbyJoin | NoMessageBoxCloseConfirmation,
|
||||
DefaultV4 = NoMessageBoxCloseConfirmationAfterLobbyJoin | CanSeeExtraLobbies | Episode3Games,
|
||||
};
|
||||
|
||||
uint16_t flags_for_version(GameVersion version, uint8_t sub_version);
|
||||
const char* name_for_version(GameVersion version);
|
||||
GameVersion version_for_name(const char* name);
|
||||
Reference in New Issue
Block a user