switch to CMake

This commit is contained in:
Martin Michelsen
2021-12-29 11:51:17 -08:00
parent 6b5766449e
commit e8f23e4b2e
62 changed files with 253 additions and 326 deletions
+934
View File
@@ -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());
}
}
+38
View File
@@ -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);
+45
View File
@@ -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;
}
+82
View File
@@ -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);
};
+365
View File
@@ -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");
}
}
}
+11
View File
@@ -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);
+107
View File
@@ -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);
}
}
}
}
+33
View File
@@ -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);
};
+34
View File
@@ -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));
}
+40
View File
@@ -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
View File
@@ -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;
}
+25
View File
@@ -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;
};
+53
View File
@@ -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;
}
+31
View File
@@ -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
View File
@@ -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;
}
+73
View File
@@ -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
View File
@@ -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);
}
}
+88
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+9
View File
@@ -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
View File
@@ -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);
};
+97
View File
@@ -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));
}
+21
View File
@@ -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);
+443
View File
@@ -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;
}
}
+74
View File
@@ -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;
};
+50
View File
@@ -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");
}
+40
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+394
View File
@@ -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);
}
}
+71
View File
@@ -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);
};
+107
View File
@@ -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\'");
}
}
+28
View File
@@ -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
View File
@@ -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;
}
+88
View File
@@ -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);
+24
View File
@@ -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);
}
+22
View File
@@ -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
+12
View File
@@ -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
+14
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+186
View File
@@ -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
View File
@@ -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));
}
+69
View File
@@ -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);
};
+187
View File
@@ -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\'");
}
}
+25
View File
@@ -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);
};
+166
View File
@@ -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;
}
}
+82
View File
@@ -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);
};
+93
View File
@@ -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();
}
+42
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+80
View File
@@ -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");
}
+45
View File
@@ -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);