initial commit

This commit is contained in:
Martin Michelsen
2018-10-31 23:55:32 -07:00
commit 5b8c26ccd7
814 changed files with 12277 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
.DS_Store
*.o
newserv
+739
View File
@@ -0,0 +1,739 @@
#include "ChatCommands.hh"
#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;
////////////////////////////////////////////////////////////////////////////////
vector<u16string> section_id_to_name({
u"Viridia", u"Greennill", u"Skyly", u"Bluefull", u"Purplenum", u"Pinkal",
u"Redria", u"Oran", u"Yellowboze", u"Whitill"});
unordered_map<u16string, uint8_t> name_to_section_id({
{u"viridia", 0},
{u"greennill", 1},
{u"skyly", 2},
{u"bluefull", 3},
{u"purplenum", 4},
{u"pinkal", 5},
{u"redria", 6},
{u"oran", 7},
{u"yellowboze", 8},
{u"whitill", 9}});
vector<u16string> lobby_event_to_name({
u"none", u"xmas", u"none", u"val", u"easter", u"hallo", u"sonic",
u"newyear", u"spring", u"white", u"wedding", u"fall", u"s-summer",
u"s-spring", u"summer"});
unordered_map<u16string, uint8_t> name_to_lobby_event({
{u"none", 0},
{u"xmas", 1},
{u"val", 3},
{u"easter", 4},
{u"hallo", 5},
{u"sonic", 6},
{u"newyear", 7},
{u"spring", 8},
{u"white", 9},
{u"wedding", 10},
{u"fall", 11},
{u"s-summer", 12},
{u"s-spring", 13},
{u"summer", 14},
});
unordered_map<uint8_t, u16string> lobby_type_to_name({
{0x00, u"normal"},
{0x0F, u"inormal"},
{0x10, u"ipc"},
{0x11, u"iball"},
{0x67, u"cave2u"},
{0xD4, u"cave1"},
{0xE9, u"planet"},
{0xEA, u"clouds"},
{0xED, u"cave"},
{0xEE, u"jungle"},
{0xEF, u"forest2-2"},
{0xF0, u"forest2-1"},
{0xF1, u"windpower"},
{0xF2, u"overview"},
{0xF3, u"seaside"},
{0xF4, u"some?"},
{0xF5, u"dmorgue"},
{0xF6, u"caelum"},
{0xF8, u"digital"},
{0xF9, u"boss1"},
{0xFA, u"boss2"},
{0xFB, u"boss3"},
{0xFC, u"dragon"},
{0xFD, u"derolle"},
{0xFE, u"volopt"},
{0xFF, u"darkfalz"},
});
unordered_map<u16string, uint8_t> name_to_lobby_type({
{u"normal", 0x00},
{u"inormal", 0x0F},
{u"ipc", 0x10},
{u"iball", 0x11},
{u"cave1", 0xD4},
{u"cave2u", 0x67},
{u"dragon", 0xFC},
{u"derolle", 0xFD},
{u"volopt", 0xFE},
{u"darkfalz", 0xFF},
{u"planet", 0xE9},
{u"clouds", 0xEA},
{u"cave", 0xED},
{u"jungle", 0xEE},
{u"forest2-2", 0xEF},
{u"forest2-1", 0xF0},
{u"windpower", 0xF1},
{u"overview", 0xF2},
{u"seaside", 0xF3},
{u"some?", 0xF4},
{u"dmorgue", 0xF5},
{u"caelum", 0xF6},
{u"digital", 0xF8},
{u"boss1", 0xF9},
{u"boss2", 0xFA},
{u"boss3", 0xFB},
{u"knight", 0xFC},
{u"sky", 0xFE},
{u"morgue", 0xFF},
});
vector<u16string> tech_id_to_name({
u"foie", u"gifoie", u"rafoie",
u"barta", u"gibarta", u"rabarta",
u"zonde", u"gizonde", u"razonde",
u"grants", u"deband", u"jellen", u"zalure", u"shifta",
u"ryuker", u"resta", u"anti", u"reverser", u"megid"});
unordered_map<u16string, uint8_t> name_to_tech_id({
{u"foie", 0},
{u"gifoie", 1},
{u"rafoie", 2},
{u"barta", 3},
{u"gibarta", 4},
{u"rabarta", 5},
{u"zonde", 6},
{u"gizonde", 7},
{u"razonde", 8},
{u"grants", 9},
{u"deband", 10},
{u"jellen", 11},
{u"zalure", 12},
{u"shifta", 13},
{u"ryuker", 14},
{u"resta", 15},
{u"anti", 16},
{u"reverser", 17},
{u"megid", 18},
});
vector<u16string> npc_id_to_name({
u"ninja", u"rico", u"sonic", u"knuckles", u"tails", u"flowen", u"elly"});
unordered_map<u16string, uint8_t> name_to_npc_id({
{u"ninja", 0},
{u"rico", 1},
{u"sonic", 2},
{u"knuckles", 3},
{u"tails", 4},
{u"flowen", 5},
{u"elly", 6}});
////////////////////////////////////////////////////////////////////////////////
// Checks
static void check_privileges(shared_ptr<Client> c, uint64_t mask) {
if (!c->license) {
throw runtime_error("not logged in");
}
if ((c->license->privileges & mask) != mask) {
throw runtime_error("insufficient permissions");
}
}
static void check_version(shared_ptr<Client> c, GameVersion version) {
if (c->version != version) {
throw runtime_error("incorrect version");
}
}
static void check_not_version(shared_ptr<Client> c, GameVersion version) {
if (c->version == version) {
throw runtime_error("incorrect version");
}
}
static void check_is_game(shared_ptr<Lobby> l, bool is_game) {
if (l->is_game() != is_game) {
throw runtime_error(is_game ? "can only be used in games" : "can only be used in lobbies");
}
}
static void check_cheats_enabled(shared_ptr<Lobby> l) {
if (!(l->flags & LobbyFlag::CheatsEnabled)) {
throw runtime_error("can only be used in cheat mode");
}
}
static void check_is_leader(shared_ptr<Lobby> l, shared_ptr<Client> c) {
if (l->leader_id != c->lobby_client_id) {
throw runtime_error("you are not the game leader");
}
}
////////////////////////////////////////////////////////////////////////////////
// Message commands
static void command_lobby_info(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
shared_ptr<Client> c, const char16_t* args) {
// no preconditions - everyone can use this command
string buffer;
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, "$C6Lobby ID: %08X\n%s\nSection ID: %s\nCheat mode: %s",
l->lobby_id, level_string.c_str(),
section_id_to_name.at(l->section_id).c_str(),
(l->flags & LobbyFlag::CheatsEnabled) ? u"on" : u"off");
} else {
send_text_message_printf(c, "$C6Lobby ID: %08X", l->lobby_id);
}
}
static void command_ax(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
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> l,
shared_ptr<Client> c, const char16_t* args) {
check_privileges(c, Privilege::Announce);
// TODO: implement this
}
static void command_arrow(shared_ptr<ServerState> s, 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> s, shared_ptr<Lobby> l,
shared_ptr<Client> c, const char16_t* args) {
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) ? u"enabled" : u"disabled");
// if cheat mode was disabled, turn off all the cheat features that were on
if (!(l->flags & LobbyFlag::CheatsEnabled)) {
rw_guard g(l->lock, true);
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> s, shared_ptr<Lobby> l,
shared_ptr<Client> c, const char16_t* args) {
check_is_game(l, false);
check_privileges(c, Privilege::ChangeEvent);
{
rw_guard g(l->lock, true);
l->event = name_to_lobby_event.at(args);
}
send_command(l, 0xDA, l->event, NULL, 0);
}
static void command_lobby_event_all(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
shared_ptr<Client> c, const char16_t* args) {
check_privileges(c, Privilege::ChangeEvent);
uint8_t event = name_to_lobby_event.at(args);
for (auto l : s->all_lobbies()) {
if (l->is_game() || !(l->flags & LobbyFlag::Default)) {
continue;
}
{
rw_guard g(l->lock, true);
l->event = event;
}
send_command(l, 0xDA, event, NULL, 0);
}
}
static void command_lobby_type(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
shared_ptr<Client> c, const char16_t* args) {
check_is_game(l, false);
check_privileges(c, Privilege::ChangeEvent);
{
rw_guard g(l->lock, true);
l->type = name_to_lobby_type.at(args);
if (l->type < ((l->flags & LobbyFlag::Episode3) ? 20 : 15)) {
l->type = l->block - 1;
}
}
rw_guard g(l->lock, false);
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> s, 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 locked with password:\n%s",
encoded.c_str());
}
}
static void command_min_level(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
shared_ptr<Client> c, const char16_t* args) {
check_is_game(l, true);
check_is_leader(l, c);
u16string buffer;
{
rw_guard g(l->lock, true);
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> s, shared_ptr<Lobby> l,
shared_ptr<Client> c, const char16_t* args) {
check_is_game(l, true);
check_is_leader(l, c);
{
rw_guard g(l->lock, true);
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") {
c->player.disp.section_id = name_to_section_id.at(decode_sjis(tokens[1]));
} 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 {
c->player.disp.extra_model = name_to_npc_id.at(decode_sjis(tokens[1]));
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 = name_to_tech_id.at(decode_sjis(tokens[1]));
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> s, shared_ptr<Lobby> l,
shared_ptr<Client> c, const char16_t* args) {
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;
}
rw_guard g(target->lock, true);
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> s, 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> s, shared_ptr<Lobby> l,
shared_ptr<Client> c, const char16_t* args) {
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> s, shared_ptr<Lobby> l,
shared_ptr<Client> c, const char16_t* args) {
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> s, 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());
}
}
////////////////////////////////////////////////////////////////////////////////
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) {
u16string command_name;
u16string text_str(text);
size_t space_pos = text_str.find(L' ');
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 exception& e) {
send_text_message_printf(c, "$C6Failed:\n%s", e.what());
return;
}
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <memory>
#include "ServerState.hh"
#include "Lobby.hh"
#include "Client.hh"
void process_chat_command(std::shared_ptr<ServerState> s, std::shared_ptr<Lobby> l,
std::shared_ptr<Client> c, const char16_t* text);
+47
View File
@@ -0,0 +1,47 @@
#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), in_information_menu(false), 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) {
rw_guard g(this->lock, false);
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;
}
+85
View File
@@ -0,0 +1,85 @@
#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 {
rw_lock lock;
// 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
bool in_information_menu;
// 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);
};
+260
View File
@@ -0,0 +1,260 @@
#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 read_offset = 0;
while (read_offset < data.size()) {
// look for a chunk of data in history matching what's at the current offset
ssize_t best_offset = 0;
size_t best_size = 0;
for (ssize_t this_offset = -3;
(this_offset + data.size() >= 0) &&
(this_offset > -0x1FF0) &&
(best_size < 255);
this_offset--) {
// for this offset, expand the match as much as possible
size_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.size() - 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("maximumoutput 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");
}
}
}
}
+10
View File
@@ -0,0 +1,10 @@
#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);
+131
View File
@@ -0,0 +1,131 @@
#include "DNSServer.hh"
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
#include <netinet/in.h>
#include <phosg/Network.hh>
#include <phosg/Strings.hh>
#include <vector>
#include <string>
#include "NetworkAddresses.hh"
using namespace std;
DNSServer::DNSServer(uint32_t local_connect_address,
uint32_t external_connect_address) :
should_exit(false), local_connect_address(local_connect_address),
external_connect_address(external_connect_address) { }
DNSServer::~DNSServer() {
for (int fd : this->fds) {
close(fd);
}
}
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) {
this->fds.emplace(fd);
}
void DNSServer::start() {
this->t = thread(&DNSServer::run_thread, this);
}
void DNSServer::schedule_stop() {
this->should_exit = true;
}
void DNSServer::wait_for_stop() {
this->t.join();
}
void DNSServer::run_thread() {
vector<pollfd> poll_fds;
for (int fd : this->fds) {
poll_fds.emplace_back();
auto& pfd = poll_fds.back();
pfd.fd = fd;
pfd.events = POLLIN;
pfd.revents = 0;
}
while (!this->should_exit) {
sockaddr_in remote;
socklen_t remote_size = sizeof(sockaddr_in);
memset(&remote, 0, remote_size);
// 10 second timeout
int num_fds = poll(poll_fds.data(), poll_fds.size(), 10000);
if (num_fds < 0) {
auto s = string_for_error(errno);
log(ERROR, "DNS server terminating due to error: %s", s.c_str());
break;
}
if (num_fds == 0) {
continue;
}
for (const auto& pfd : poll_fds) {
if (!(pfd.revents & POLLIN)) {
continue;
}
string input(2048, 0);
ssize_t bytes = recvfrom(pfd.fd, const_cast<char*>(input.data()),
input.size(), 0, reinterpret_cast<sockaddr*>(&remote), &remote_size);
if (bytes > 0) {
input.resize(bytes);
uint32_t connect_address;
if (is_local_address(remote.sin_addr.s_addr)) {
connect_address = this->local_connect_address;
} else {
connect_address = this->external_connect_address;
}
string output = this->build_response(input, connect_address);
if (!output.empty()) {
sendto(pfd.fd, output.data(), output.size(), 0,
reinterpret_cast<sockaddr*>(&remote), remote_size);
}
}
}
}
}
string DNSServer::build_response(const std::string& input,
uint32_t connect_address) {
if (input.size() < 0x0C) {
return "";
}
string ret;
size_t name_len = strlen(input.data() + 0x0C) + 1;
ret.append(input.substr(0, 2));
ret.append("\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00", 10);
ret.append(input.substr(12, name_len));
ret.append("\x00\x01\x00\x01\xC0\x0C\x00\x01\x00\x01\x00\x00\x00\x3C\x00\x04", 16);
ret.append(reinterpret_cast<const char*>(&connect_address), 4);
return ret;
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <atomic>
#include <thread>
#include <string>
#include <set>
class DNSServer {
public:
DNSServer(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);
virtual void start();
virtual void schedule_stop();
virtual void wait_for_stop();
private:
std::atomic<bool> should_exit;
std::thread t;
std::set<int> fds;
uint32_t local_connect_address;
uint32_t external_connect_address;
void run_thread();
static std::string build_response(const std::string& input,
uint32_t connect_address);
};
+40
View File
@@ -0,0 +1,40 @@
#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 {
lock_guard<mutex> g(this->lock);
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)));
{
lock_guard<mutex> g(this->lock);
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));
}
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <memory>
#include <mutex>
#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;
mutable std::mutex lock;
};
+462
View File
@@ -0,0 +1,462 @@
#include "Items.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_locked(shared_ptr<Lobby> l, 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 = (rand() << 30) ^ (rand() << 15) ^ rand();
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_item(bool is_box, uint8_t episode,
uint8_t difficulty, uint8_t area, uint8_t section_id) const {
// 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] = rand() % 7;
break;
case 0x01: // equipment
switch (rand() % 4) {
case 0x00: // weapon
item.item_data1[1] = 1 + (rand() % 12); // random normal class
item.item_data1[2] = difficulty + (rand() % 3); // 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] |= (rand() % 41); // give a special
}
for (size_t x = 0, y = 0; (x < 5) && (y < 3); x++) { // percentages
if ((rand() % 11) < 2) { // 1/11 chance of getting each type of percentage
item.item_data1[6 + (y * 2)] = x + 1;
item.item_data1[7 + (y * 2)] = (rand() % 11) * 5;
y++;
}
}
break;
case 0x01: // armor
item.item_data1[0] = 0x01;
item.item_data1[1] = 0x01;
item.item_data1[2] = (6 * difficulty) + (rand() % ((area / 2) + 2)); // standard type based on difficulty and area
if (item.item_data1[2] > 0x17) {
item.item_data1[2] = 0x17; // no standard types above 0x17
}
if ((rand() % 10) < 2) { // +/-
item.item_data1[4] = rand() % 6;
item.item_data1[6] = rand() % 3;
}
item.item_data1[5] = rand() % 5; // slots
break;
case 0x02: // shield
item.item_data1[0] = 0x01;
item.item_data1[1] = 0x02;
item.item_data1[2] = (5 * difficulty) + (rand() % ((area / 2) + 2)); // standard type based on difficulty and area
if (item.item_data1[2] > 0x14) {
item.item_data1[2] = 0x14; // no standard types above 0x14
}
if ((rand() % 11) < 2) { // +/-
item.item_data1[4] = rand() % 6;
item.item_data1[6] = rand() % 6;
}
break;
case 0x03: { // unit
const auto& type_table = this->unit_types.at(difficulty);
uint8_t type = type_table[rand() % type_table.size()];
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] = rand() % 19; // 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 + (rand() % ((area - 1) / 2));
} else {
item.item_data1[2] = difficulty + (rand() % 1);
}
if (item.item_data1[2] > 6) {
item.item_data1[2] = 6;
}
} else {
item.item_data1[2] = (5 * difficulty) + (rand() % ((area * 3) / 2)); // 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] = rand() % 3; // mono, di, tri
break;
case 0x05: // consumable
item.item_data1[0] = 0x03;
item.item_data1[5] = 0x01;
switch (rand() % 3) {
case 0: // antidote / antiparalysis
item.item_data1[1] = 6;
item.item_data1[2] = rand() & 1;
break;
case 1: // telepipe / trap vision
item.item_data1[1] = 7 + rand() & 1;
break;
case 2: // sol / moon / star atomizer
item.item_data1[1] = 3 + rand() % 3;
break;
}
break;
case 0x06: // consumable
item.item_data1[0] = 0x03;
item.item_data1[5] = 0x01;
item.item_data1[1] = rand() & 1; // mate or fluid
if (difficulty == 0) {
item.item_data1[2] = rand() & 1; // only mono and di on normal
} else if (difficulty == 3) {
item.item_data1[2] = 1 + rand() & 1; // only di and tri on ultimate
} else {
item.item_data1[2] = rand() % 3; // else, any of the three
}
break;
case 0x07: // meseta
item.item_data1[0] = 0x04;
item.item_data2d = (90 * difficulty) + ((rand() % 20) * (area * 2)); // meseta amount
break;
default:
throw out_of_range("no item created");
}
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_locked(std::shared_ptr<Lobby> l, 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_item(bool is_box, uint8_t episode, uint8_t difficulty,
uint8_t area, uint8_t section_id) const;
};
+51
View File
@@ -0,0 +1,51 @@
#include "LevelTable.hh"
#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;
};
+112
View File
@@ -0,0 +1,112 @@
#include <stdio.h>
#include <string.h>
#include <phosg/Filesystem.hh>
#include <phosg/Time.hh>
#include "License.hh"
using namespace std;
LicenseManager::LicenseManager(const std::string& filename) : filename(filename) {
auto licenses = load_vector_file<License>(this->filename);
for (const auto& read_license : licenses) {
shared_ptr<License> license(new License(read_license));
this->bb_username_to_license.emplace(license->username, license);
this->serial_number_to_license.emplace(license->serial_number, license);
}
}
void LicenseManager::save_locked() 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 {
rw_guard g(this->lock, false);
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 {
rw_guard g(this->lock, false);
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 {
rw_guard g(this->lock, false);
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;
}
void LicenseManager::ban_until(uint32_t serial_number, uint64_t end_time) {
rw_guard g(this->lock, false);
this->serial_number_to_license.at(serial_number)->ban_end_time = end_time;
this->save_locked();
}
void LicenseManager::add(shared_ptr<License> l) {
{
rw_guard g(this->lock, true);
this->serial_number_to_license.emplace(l->serial_number, l);
if (l->username[0]) {
this->bb_username_to_license.emplace(l->username, l);
}
}
rw_guard g(this->lock, false);
this->save_locked();
}
void LicenseManager::remove(uint32_t serial_number) {
{
rw_guard g(this->lock, true);
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);
}
}
rw_guard g(this->lock, false);
this->save_locked();
}
+63
View File
@@ -0,0 +1,63 @@
#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)
};
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);
void add(std::shared_ptr<License> l);
void remove(uint32_t serial_number);
protected:
void save_locked() const;
mutable rw_lock lock;
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;
};
+202
View File
@@ -0,0 +1,202 @@
#include "Lobby.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), 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->lobby_id < 0);
}
void Lobby::reassign_leader_on_client_departure_locked(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;
}
}
throw out_of_range("no clients remain");
}
bool Lobby::any_client_loading() const {
rw_guard g(this->lock, false);
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 {
rw_guard g(this->lock, false);
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) {
rw_guard g(this->lock, true);
this->add_client_locked(c);
}
void Lobby::add_client_locked(shared_ptr<Client> c) {
rw_guard g(this->lock, true);
size_t index;
for (index = 0; index < this->max_clients; index++) {
if (!this->clients[index].get()) {
this->clients[index] = c;
}
}
if (index >= this->max_clients) {
throw out_of_range("no space left in lobby");
}
c->lobby_client_id = index;
c->lobby_id = this->lobby_id;
// if index == 0, then there might not be anyone else in the lobby; if this is
// the case, set the leader id as well
if (index == 0) {
for (index = 1; index < this->max_clients; index++) {
if (this->clients[index].get()) {
break;
}
}
if (index >= this->max_clients) {
this->leader_id = 0;
}
}
}
void Lobby::remove_client(shared_ptr<Client> c) {
rw_guard g(this->lock, true);
this->remove_client_locked(c);
}
void Lobby::remove_client_locked(shared_ptr<Client> c) {
rw_guard g(this->lock, true);
if (this->clients[c->lobby_client_id] != c) {
throw logic_error("client\'s lobby client id does not match client list");
}
this->clients[c->lobby_client_id] = NULL;
c->lobby_id = 0;
this->reassign_leader_on_client_departure_locked(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;
}
// deadlock prevention: lock the lobbies in increasing order of memory address
vector<rw_guard> guards;
uint8_t* this_ptr = reinterpret_cast<uint8_t*>(this);
uint8_t* dest_ptr = reinterpret_cast<uint8_t*>(dest_lobby.get());
if (this_ptr < dest_ptr) {
guards.emplace_back(this->lock, true);
guards.emplace_back(dest_lobby->lock, true);
} else {
guards.emplace_back(dest_lobby->lock, true);
guards.emplace_back(this->lock, true);
}
dest_lobby->add_client_locked(c);
this->remove_client_locked(c);
}
shared_ptr<Client> Lobby::find_client(const char16_t* identifier,
uint64_t serial_number) {
rw_guard g(this->lock, false);
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) {
rw_guard g(this->lock, true);
this->item_id_to_floor_item.emplace(item.data.item_id, item);
}
void Lobby::remove_item(uint32_t item_id, PlayerInventoryItem* item) {
rw_guard g(this->lock, true);
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(uint32_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);
}
}
+90
View File
@@ -0,0 +1,90 @@
#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
};
struct Lobby {
mutable rw_lock lock;
uint64_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[36];
char16_t name[36];
//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_locked(size_t leaving_client_id);
size_t count_clients() const;
bool any_client_loading() const;
void add_client(std::shared_ptr<Client> c);
void add_client_locked(std::shared_ptr<Client> c);
void remove_client(std::shared_ptr<Client> c);
void remove_client_locked(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(uint32_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);
};
+170
View File
@@ -0,0 +1,170 @@
#include <arpa/inet.h>
#include <signal.h>
#include <unordered_map>
#include <phosg/JSON.hh>
#include <phosg/Strings.hh>
#include <set>
#include "NetworkAddresses.hh"
#include "DNSServer.hh"
#include "ServerState.hh"
#include "Server.hh"
#include "FileContentsCache.hh"
#include "Text.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 = d.at("ServerName")->as_string();
// 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<unordered_map<uint32_t, u16string>> id_to_information_contents(
new unordered_map<uint32_t, u16string>());
uint32_t item_id = 1;
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);
id_to_information_contents->emplace(item_id, decode_sjis(v.at(2)->as_string()));
item_id++;
}
s->information_menu = information_menu;
s->id_to_information_contents = id_to_information_contents;
s->num_threads = d.at("Threads")->as_int();
auto local_address_str = d.at("LocalAddress")->as_string();
uint32_t local_address = inet_addr(local_address_str.c_str());
if (s->all_addresses.emplace(local_address).second) {
log(INFO, "added local address: %hhu.%hhu.%hhu.%hhu",
static_cast<uint8_t>(local_address >> 24), static_cast<uint8_t>(local_address >> 16),
static_cast<uint8_t>(local_address >> 8), static_cast<uint8_t>(local_address));
}
auto external_address_str = d.at("LocalAddress")->as_string();
uint32_t external_address = inet_addr(external_address_str.c_str());
if (s->all_addresses.emplace(external_address).second) {
log(INFO, "added external address: %hhu.%hhu.%hhu.%hhu",
static_cast<uint8_t>(external_address >> 24), static_cast<uint8_t>(external_address >> 16),
static_cast<uint8_t>(external_address >> 8), static_cast<uint8_t>(external_address));
}
}
int main(int argc,char* argv[]) {
log(INFO, "fuzziqer software newserv");
signal(SIGPIPE, SIG_IGN);
log(INFO, "creating server state");
shared_ptr<ServerState> state(new ServerState());
log(INFO, "reading network addresses");
state->all_addresses = get_local_address_list();
for (uint32_t addr : state->all_addresses) {
log(INFO, "found address: %hhu.%hhu.%hhu.%hhu",
static_cast<uint8_t>(addr >> 24), static_cast<uint8_t>(addr >> 16),
static_cast<uint8_t>(addr >> 8), static_cast<uint8_t>(addr));
}
log(INFO, "loading configuration");
auto config_json = JSONObject::load("system/config.json");
populate_state_from_config(state, config_json);
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"));
log(INFO, "starting dns server");
DNSServer dns_server(state->local_address, state->external_address);
// TODO: call dns_server.listen appropriately
dns_server.start();
log(INFO, "starting game server");
Server game_server(state);
// TODO: call game_server.listen appropriately
game_server.start();
for (;;) {
sigset_t s;
sigemptyset(&s);
sigsuspend(&s);
}
log(INFO, "waiting for servers to terminate");
dns_server.schedule_stop();
game_server.schedule_stop();
dns_server.wait_for_stop();
game_server.wait_for_stop();
return 0;
}
+20
View File
@@ -0,0 +1,20 @@
OBJECTS=FileContentsCache.o Menu.o PSOProtocol.o Client.o Lobby.o \
ServerState.o Server.o License.o PSOEncryption.o Player.o SendCommands.o \
ChatCommands.o ReceiveSubcommands.o ReceiveCommands.o Version.o Items.o \
LevelTable.o Compression.o Quest.o RareItemSet.o Map.o NetworkAddresses.o \
Text.o DNSServer.o Main.o
CXX=g++
CXXFLAGS=-I/opt/local/include -I/usr/local/include -std=c++14 -g -DHAVE_INTTYPES_H -DHAVE_NETINET_IN_H -Wall -Werror
LDFLAGS=-L/opt/local/lib -L/usr/local/lib -std=c++14 -levent -lphosg -lpthread
EXECUTABLE=newserv
all: $(EXECUTABLE)
$(EXECUTABLE): $(OBJECTS)
$(CXX) $(OBJECTS) $(LDFLAGS) -o $(EXECUTABLE)
clean:
find . -name \*.o -delete
rm -rf *.dSYM $(EXECUTABLE) gmon.out
.PHONY: clean test
+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);
};
+79
View File
@@ -0,0 +1,79 @@
#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 res_sin->sin_addr.s_addr;
}
set<uint32_t> get_local_address_list() {
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);
set<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(bswap32(sin->sin_addr.s_addr));
}
return ret;
}
bool is_local_address(uint32_t addr) {
uint8_t net = addr & 0xFF;
if ((net != 127) && (net != 172) && (net != 10) && (net != 192)) {
return false;
}
return true;
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <stdint.h>
#include <set>
// 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::set<uint32_t> get_local_address_list();
uint32_t get_connected_address(int fd);
bool is_local_address(uint32_t daddr);
+586
View File
@@ -0,0 +1,586 @@
#include "PSOEncryption.hh"
#include <stdio.h>
#include <string.h>
#include <stdexcept>
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] = (((this->stream[0] >> 9) ^ (this->stream[this->offset] << 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)));
this->offset++;
source1++;
source2++;
source3++;
}
for (size_t x = 0; x < 4; x++) {
this->update_stream();
}
}
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();
}
}
static const uint32_t psobb_private_key[0x400] = {
0xD1310BA6,0x98DFB5AC,0x2FFD72DB,0xD01ADFB7,0xB8E1AFED,0x6A267E96,0xBA7C9045,0xF12C7F99,
0x24A19947,0xB3916CF7,0x0801F2E2,0x858EFC16,0x636920D8,0x71574E69,0xA458FEA3,0xF4933D7E,
0x0D95748F,0x728EB658,0x718BCD58,0x82154AEE,0x7B54A41D,0xC25A59B5,0x9C30D539,0x2AF26013,
0xC5D1B023,0x286085F0,0xCA417918,0xB8DB38EF,0x8E79DCB0,0x603A180E,0x6C9E0E8B,0xB01E8A3E,
0xD71577C1,0xBD314B27,0x78AF2FDA,0x55605C60,0xE65525F3,0xAA55AB94,0x57489862,0x63E81440,
0x55CA396A,0x2AAB10B6,0xB4CC5C34,0x1141E8CE,0xA15486AF,0x7C72E993,0xB3EE1411,0x636FBC2A,
0x2BA9C55D,0x741831F6,0xCE5C3E16,0x9B87931E,0xAFD6BA33,0x6C24CF5C,0x7A325381,0x28958677,
0x3B8F4898,0x6B4BB9AF,0xC4BFE81B,0x66282193,0x61D809CC,0xFB21A991,0x487CAC60,0x5DEC8032,
0xEF845D5D,0xE98575B1,0xDC262302,0xEB651B88,0x23893E81,0xD396ACC5,0x0F6D6FF3,0x83F44239,
0x2E0B4482,0xA4842004,0x69C8F04A,0x9E1F9B5E,0x21C66842,0xF6E96C9A,0x670C9C61,0xABD388F0,
0x6A51A0D2,0xD8542F68,0x960FA728,0xAB5133A3,0x6EEF0B6C,0x137A3BE4,0xBA3BF050,0x7EFB2A98,
0xA1F1651D,0x39AF0176,0x66CA593E,0x82430E88,0x8CEE8619,0x456F9FB4,0x7D84A5C3,0x3B8B5EBE,
0xE06F75D8,0x85C12073,0x401A449F,0x56C16AA6,0x4ED3AA62,0x363F7706,0x1BFEDF72,0x429B023D,
0x37D0D724,0xD00A1248,0xDB0FEAD3,0x49F1C09B,0x075372C9,0x80991B7B,0x25D479D8,0xF6E8DEF7,
0xE3FE501A,0xB6794C3B,0x976CE0BD,0x04C006BA,0xC1A94FB6,0x409F60C4,0x5E5C9EC2,0x196A2463,
0x68FB6FAF,0x3E6C53B5,0x1339B2EB,0x3B52EC6F,0x6DFC511F,0x9B30952C,0xCC814544,0xAF5EBD09,
0xBEE3D004,0xDE334AFD,0x660F2807,0x192E4BB3,0xC0CBA857,0x45C8740F,0xD20B5F39,0xB9D3FBDB,
0x5579C0BD,0x1A60320A,0xD6A100C6,0x402C7279,0x679F25FE,0xFB1FA3CC,0x8EA5E9F8,0xDB3222F8,
0x3C7516DF,0xFD616B15,0x2F501EC8,0xAD0552AB,0x323DB5FA,0xFD238760,0x53317B48,0x3E00DF82,
0x9E5C57BB,0xCA6F8CA0,0x1A87562E,0xDF1769DB,0xD542A8F6,0x287EFFC3,0xAC6732C6,0x8C4F5573,
0x695B27B0,0xBBCA58C8,0xE1FFA35D,0xB8F011A0,0x10FA3D98,0xFD2183B8,0x4AFCB56C,0x2DD1D35B,
0x9A53E479,0xB6F84565,0xD28E49BC,0x4BFB9790,0xE1DDF2DA,0xA4CB7E33,0x62FB1341,0xCEE4C6E8,
0xEF20CADA,0x36774C01,0xD07E9EFE,0x2BF11FB4,0x95DBDA4D,0xAE909198,0xEAAD8E71,0x6B93D5A0,
0xD08ED1D0,0xAFC725E0,0x8E3C5B2F,0x8E7594B7,0x8FF6E2FB,0xF2122B64,0x8888B812,0x900DF01C,
0x4FAD5EA0,0x688FC31C,0xD1CFF191,0xB3A8C1AD,0x2F2F2218,0xBE0E1777,0xEA752DFE,0x8B021FA1,
0xE5A0CC0F,0xB56F74E8,0x18ACF3D6,0xCE89E299,0xB4A84FE0,0xFD13E0B7,0x7CC43B81,0xD2ADA8D9,
0x165FA266,0x80957705,0x93CC7314,0x211A1477,0xE6AD2065,0x77B5FA86,0xC75442F5,0xFB9D35CF,
0xEBCDAF0C,0x7B3E89A0,0xD6411BD3,0xAE1E7E49,0x00250E2D,0x2071B35E,0x226800BB,0x57B8E0AF,
0x2464369B,0xF009B91E,0x5563911D,0x59DFA6AA,0x78C14389,0xD95A537F,0x207D5BA2,0x02E5B9C5,
0x83260376,0x6295CFA9,0x11C81968,0x4E734A41,0xB3472DCA,0x7B14A94A,0x1B510052,0x9A532915,
0xD60F573F,0xBC9BC6E4,0x2B60A476,0x81E67400,0x08BA6FB5,0x571BE91F,0xF296EC6B,0x2A0DD915,
0xB6636521,0xE7B9F9B6,0xFF34052E,0xC5855664,0x53B02D5D,0xA99F8FA1,0x08BA4799,0x6E85076A,
0x4B7A70E9,0xB5B32944,0xDB75092E,0xC4192623,0xAD6EA6B0,0x49A7DF7D,0x9CEE60B8,0x8FEDB266,
0xECAA8C71,0x699A17FF,0x5664526C,0xC2B19EE1,0x193602A5,0x75094C29,0xA0591340,0xE4183A3E,
0x3F54989A,0x5B429D65,0x6B8FE4D6,0x99F73FD6,0xA1D29C07,0xEFE830F5,0x4D2D38E6,0xF0255DC1,
0x4CDD2086,0x8470EB26,0x6382E9C6,0x021ECC5E,0x09686B3F,0x3EBAEFC9,0x3C971814,0x6B6A70A1,
0x687F3584,0x52A0E286,0xB79C5305,0xAA500737,0x3E07841C,0x7FDEAE5C,0x8E7D44EC,0x5716F2B8,
0xB03ADA37,0xF0500C0D,0xF01C1F04,0x0200B3FF,0xAE0CF51A,0x3CB574B2,0x25837A58,0xDC0921BD,
0xD19113F9,0x7CA92FF6,0x94324773,0x22F54701,0x3AE5E581,0x37C2DADC,0xC8B57634,0x9AF3DDA7,
0xA9446146,0x0FD0030E,0xECC8C73E,0xA4751E41,0xE238CD99,0x3BEA0E2F,0x3280BBA1,0x183EB331,
0x4E548B38,0x4F6DB908,0x6F420D03,0xF60A04BF,0x2CB81290,0x24977C79,0x5679B072,0xBCAF89AF,
0xDE9A771F,0xD9930810,0xB38BAE12,0xDCCF3F2E,0x5512721F,0x2E6B7124,0x501ADDE6,0x9F84CD87,
0x7A584718,0x7408DA17,0xBC9F9ABC,0xE94B7D8C,0xEC7AEC3A,0xDB851DFA,0x63094366,0xC464C3D2,
0xEF1C1847,0x3215D908,0xDD433B37,0x24C2BA16,0x12A14D43,0x2A65C451,0x50940002,0x133AE4DD,
0x71DFF89E,0x10314E55,0x81AC77D6,0x5F11199B,0x043556F1,0xD7A3C76B,0x3C11183B,0x5924A509,
0xF28FE6ED,0x97F1FBFA,0x9EBABF2C,0x1E153C6E,0x86E34570,0xEAE96FB1,0x860E5E0A,0x5A3E2AB3,
0x771FE71C,0x4E3D06FA,0x2965DCB9,0x99E71D0F,0x803E89D6,0x5266C825,0x2E4CC978,0x9C10B36A,
0xC6150EBA,0x94E2EA78,0xA5FC3C53,0x1E0A2DF4,0xF2F74EA7,0x361D2B3D,0x1939260F,0x19C27960,
0x5223A708,0xF71312B6,0xEBADFE6E,0xEAC31F66,0xE3BC4595,0xA67BC883,0xB17F37D1,0x018CFF28,
0xC332DDEF,0xBE6C5AA5,0x65582185,0x68AB9802,0xEECEA50F,0xDB2F953B,0x2AEF7DAD,0x5B6E2F84,
0x1521B628,0x29076170,0xECDD4775,0x619F1510,0x13CCA830,0xEB61BD96,0x0334FE1E,0xAA0363CF,
0xB5735C90,0x4C70A239,0xD59E9E0B,0xCBAADE14,0xEECC86BC,0x60622CA7,0x9CAB5CAB,0xB2F3846E,
0x648B1EAF,0x19BDF0CA,0xA02369B9,0x655ABB50,0x40685A32,0x3C2AB4B3,0x319EE9D5,0xC021B8F7,
0x9B540B19,0x875FA099,0x95F7997E,0x623D7DA8,0xF837889A,0x97E32D77,0x11ED935F,0x16681281,
0x0E358829,0xC7E61FD6,0x96DEDFA1,0x7858BA99,0x57F584A5,0x1B227263,0x9B83C3FF,0x1AC24696,
0xCDB30AEB,0x532E3054,0x8FD948E4,0x6DBC3128,0x58EBF2EF,0x34C6FFEA,0xFE28ED61,0xEE7C3C73,
0x5D4A14D9,0xE864B7E3,0x42105D14,0x203E13E0,0x45EEE2B6,0xA3AAABEA,0xDB6C4F15,0xFACB4FD0,
0xC742F442,0xEF6ABBB5,0x654F3B1D,0x41CD2105,0xD81E799E,0x86854DC7,0xE44B476A,0x3D816250,
0xCF62A1F2,0x5B8D2646,0xFC8883A0,0xC1C7B6A3,0x7F1524C3,0x69CB7492,0x47848A0B,0x5692B285,
0x095BBF00,0xAD19489D,0x1462B174,0x23820E00,0x58428D2A,0x0C55F5EA,0x1DADF43E,0x233F7061,
0x3372F092,0x8D937E41,0xD65FECF1,0x6C223BDB,0x7CDE3759,0xCBEE7460,0x4085F2A7,0xCE77326E,
0xA6078084,0x19F8509E,0xE8EFD855,0x61D99735,0xA969A7AA,0xC50C06C2,0x5A04ABFC,0x800BCADC,
0x9E447A2E,0xC3453484,0xFDD56705,0x0E1E9EC9,0xDB73DBD3,0x105588CD,0x675FDA79,0xE3674340,
0xC5C43465,0x713E38D8,0x3D28F89E,0xF16DFF20,0x153E21E7,0x8FB03D4A,0xE6E39F2B,0xDB83ADF7,
0xE93D5A68,0x948140F7,0xF64C261C,0x94692934,0x411520F7,0x7602D4F7,0xBCF46B2E,0xD4A20068,
0xD4082471,0x3320F46A,0x43B7D4B7,0x500061AF,0x1E39F62E,0x97244546,0x14214F74,0xBF8B8840,
0x4D95FC1D,0x96B591AF,0x70F4DDD3,0x66A02F45,0xBFBC09EC,0x03BD9785,0x7FAC6DD0,0x31CB8504,
0x96EB27B3,0x55FD3941,0xDA2547E6,0xABCA0A9A,0x28507825,0x530429F4,0x0A2C86DA,0xE9B66DFB,
0x68DC1462,0xD7486900,0x680EC0A4,0x27A18DEE,0x4F3FFEA2,0xE887AD8C,0xB58CE006,0x7AF4D6B6,
0xAACE1E7C,0xD3375FEC,0xCE78A399,0x406B2A42,0x20FE9E35,0xD9F385B9,0xEE39D7AB,0x3B124E8B,
0x1DC9FAF7,0x4B6D1856,0x26A36631,0xEAE397B2,0x3A6EFA74,0xDD5B4332,0x6841E7F7,0xCA7820FB,
0xFB0AF54E,0xD8FEB397,0x454056AC,0xBA489527,0x55533A3A,0x20838D87,0xFE6BA9B7,0xD096954B,
0x55A867BC,0xA1159A58,0xCCA92963,0x99E1DB33,0xA62A4A56,0x3F3125F9,0x5EF47E1C,0x9029317C,
0xFDF8E802,0x04272F70,0x80BB155C,0x05282CE3,0x95C11548,0xE4C66D22,0x48C1133F,0xC70F86DC,
0x07F9C9EE,0x41041F0F,0x404779A4,0x5D886E17,0x325F51EB,0xD59BC0D1,0xF2BCC18F,0x41113564,
0x257B7834,0x602A9C60,0xDFF8E8A3,0x1F636C1B,0x0E12B4C2,0x02E1329E,0xAF664FD1,0xCAD18115,
0x6B2395E0,0x333E92E1,0x3B240B62,0xEEBEB922,0x85B2A20E,0xE6BA0D99,0xDE720C8C,0x2DA2F728,
0xD0127845,0x95B794FD,0x647D0862,0xE7CCF5F0,0x5449A36F,0x877D48FA,0xC39DFD27,0xF33E8D1E,
0x0A476341,0x992EFF74,0x3A6F6EAB,0xF4F8FD37,0xA812DC60,0xA1EBDDF8,0x991BE14C,0xDB6E6B0D,
0xC67B5510,0x6D672C37,0x2765D43B,0xDCD0E804,0xF1290DC7,0xCC00FFA3,0xB5390F92,0x690FED0B,
0x667B9FFB,0xCEDB7D9C,0xA091CF0B,0xD9155EA3,0xBB132F88,0x515BAD24,0x7B9479BF,0x763BD6EB,
0x37392EB3,0xCC115979,0x8026E297,0xF42E312D,0x6842ADA7,0xC66A2B3B,0x12754CCC,0x782EF11C,
0x6A124237,0xB79251E7,0x06A1BBE6,0x4BFB6350,0x1A6B1018,0x11CAEDFA,0x3D25BDD8,0xE2E1C3C9,
0x44421659,0x0A121386,0xD90CEC6E,0xD5ABEA2A,0x64AF674E,0xDA86A85F,0xBEBFE988,0x64E4C3FE,
0x9DBC8057,0xF0F7C086,0x60787BF8,0x6003604D,0xD1FD8346,0xF6381FB0,0x7745AE04,0xD736FCCC,
0x83426B33,0xF01EAB71,0xB0804187,0x3C005E5F,0x77A057BE,0xBDE8AE24,0x55464299,0xBF582E61,
0x4E58F48F,0xF2DDFDA2,0xF474EF38,0x8789BDC2,0x5366F9C3,0xC8B38E74,0xB475F255,0x46FCD9B9,
0x7AEB2661,0x8B1DDF84,0x846A0E79,0x915F95E2,0x466E598E,0x20B45770,0x8CD55591,0xC902DE4C,
0xB90BACE1,0xBB8205D0,0x11A86248,0x7574A99E,0xB77F19B6,0xE0A9DC09,0x662D09A1,0xC4324633,
0xE85A1F02,0x09F0BE8C,0x4A99A025,0x1D6EFE10,0x1AB93D1D,0x0BA5A4DF,0xA186F20F,0x2868F169,
0xDCB7DA83,0x573906FE,0xA1E2CE9B,0x4FCD7F52,0x50115E01,0xA70683FA,0xA002B5C4,0x0DE6D027,
0x9AF88C27,0x773F8641,0xC3604C06,0x61A806B5,0xF0177A28,0xC0F586E0,0x006058AA,0x30DC7D62,
0x11E69ED7,0x2338EA63,0x53C2DD94,0xC2C21634,0xBBCBEE56,0x90BCB6DE,0xEBFC7DA1,0xCE591D76,
0x6F05E409,0x4B7C0188,0x39720A3D,0x7C927C24,0x86E3725F,0x724D9DB9,0x1AC15BB4,0xD39EB8FC,
0xED545578,0x08FCA5B5,0xD83D7CD3,0x4DAD0FC4,0x1E50EF5E,0xB161E6F8,0xA28514D9,0x6C51133C,
0x6FD5C7E7,0x56E14EC4,0x362ABFCE,0xDDC6C837,0xD79A3234,0x92638212,0x670EFA8E,0x406000E0,
0x3A39CE37,0xD3FAF5CF,0xABC27737,0x5AC52D1B,0x5CB0679E,0x4FA33742,0xD3822740,0x99BC9BBE,
0xD5118E9D,0xBF0F7315,0xD62D1C7E,0xC700C47B,0xB78C1B6B,0x21A19045,0xB26EB1BE,0x6A366EB4,
0x5748AB2F,0xBC946E79,0xC6A376D2,0x6549C2C8,0x530FF8EE,0x468DDE7D,0xD5730A1D,0x4CD04DC6,
0x2939BBDB,0xA9BA4650,0xAC9526E8,0xBE5EE304,0xA1FAD5F0,0x6A2D519A,0x63EF8CE2,0x9A86EE22,
0xC089C2B8,0x43242EF6,0xA51E03AA,0x9CF2D0A4,0x83C061BA,0x9BE96A4D,0x8FE51550,0xBA645BD6,
0x2826A2F9,0xA73A3AE1,0x4BA99586,0xEF5562E9,0xC72FEFD3,0xF752F7DA,0x3F046F69,0x77FA0A59,
0x80E4A915,0x87B08601,0x9B09E6AD,0x3B3EE593,0xE990FD5A,0x9E34D797,0x2CF0B7D9,0x022B8B51,
0x96D5AC3A,0x017DA67D,0xD1CF3ED6,0x7C7D2D28,0x1F9F25CF,0xADF2B89B,0x5AD6B472,0x5A88F54C,
0xE029AC71,0xE019A5E6,0x47B0ACFD,0xED93FA9B,0xE8D3C48D,0x283B57CC,0xF8D56629,0x79132E28,
0x785F0191,0xED756055,0xF7960E44,0xE3D35E8C,0x15056DD4,0x88F46DBA,0x03A16125,0x0564F0BD,
0xC3EB9E15,0x3C9057A2,0x97271AEC,0xA93A072A,0x1B3F6D9B,0x1E6321F5,0xF59C66FB,0x26DCF319,
0x7533D928,0xB155FDF5,0x03563482,0x8ABA3CBB,0x28517711,0xC20AD9F8,0xABCC5167,0xCCAD925F,
0x4DE81751,0x3830DC8E,0x379D5862,0x9320F991,0xEA7A90C2,0xFB3E7BCE,0x5121CE64,0x774FBE32,
0xA8B6E37E,0xC3293D46,0x48DE5369,0x6413E680,0xA2AE0810,0xDD6DB224,0x69852DFD,0x09072166,
0xB39A460A,0x6445C0DD,0x586CDECF,0x1C20C8AE,0x5BBEF7DD,0x1B588D40,0xCCD2017F,0x6BB4E3BB,
0xDDA26A7E,0x3A59FF45,0x3E350A44,0xBCB4CDD5,0x72EACEA8,0xFA6484BB,0x8D6612AE,0xBF3C6F47,
0xD29BE463,0x542F5D9E,0xAEC2771B,0xF64E6370,0x740E0D8D,0xE75B1357,0xF8721671,0xAF537D5D,
0x4040CB08,0x4EB4E2CC,0x34D2466A,0x0115AF84,0xE1B00428,0x95983A1D,0x06B89FB4,0xCE6EA048,
0x6F3F3B82,0x3520AB82,0x011A1D4B,0x277227F8,0x611560B1,0xE7933FDC,0xBB3A792B,0x344525BD,
0xA08839E1,0x51CE794B,0x2F32C9B7,0xA01FBAC9,0xE01CC87E,0xBCC7D1F6,0xCF0111C3,0xA1E8AAC7,
0x1A908749,0xD44FBD9A,0xD0DADECB,0xD50ADA38,0x0339C32A,0xC6913667,0x8DF9317C,0xE0B12B4F,
0xF79E59B7,0x43F5BB3A,0xF2D519FF,0x27D9459C,0xBF97222C,0x15E6FC2A,0x0F91FC71,0x9B941525,
0xFAE59361,0xCEB69CEB,0xC2A86459,0x12BAA8D1,0xB6C1075E,0xE3056A0C,0x10D25065,0xCB03A442,
0xE0EC6E0E,0x1698DB3B,0x4C98A0BE,0x3278E964,0x9F1F9532,0xE0D392DF,0xD3A0342B,0x8971F21E,
0x1B0A7441,0x4BA3348C,0xC5BE7120,0xC37632D8,0xDF359F8D,0x9B992F2E,0xE60B6F47,0x0FE3F11D,
0xE54CDA54,0x1EDAD891,0xCE6279CF,0xCD3E7E6F,0x1618B166,0xFD2C1D05,0x848FD2C5,0xF6FB2299,
0xF523F357,0xA6327623,0x93A83531,0x56CCCD02,0xACF08162,0x5A75EBB5,0x6E163697,0x88D273CC,
0xDE966292,0x81B949D0,0x4C50901B,0x71C65614,0xE6C6C7BD,0x327A140A,0x45E1D006,0xC3F27B9A,
0xC9AA53FD,0x62A80F00,0xBB25BFE2,0x35BDD2F6,0x71126905,0xB2040222,0xB6CBCF7C,0xCD769C2B,
0x53113EC0,0x1640E3D3,0x38ABBD60,0x2547ADF0,0xBA38209C,0xF746CE76,0x77AFA1C5,0x20756060,
0x85CBFE4E,0x8AE88DD8,0x7AAAF9B0,0x4CF9AA7E,0x1948C25C,0x02FB8A8C,0x01C36AE4,0xD6EBE1F9,
0x90D4F869,0xA65CDEA0,0x3F09252D,0xC208E69F,0xB74E6132,0xCE77E25B,0x578FDFE3,0x3AC372E6};
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 eax, ecx, edx, ebx, ebp, esi, edi;
edx = 0;
ecx = 0;
eax = 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 eax, ecx, edx, ebx, ebp, esi, edi;
edx = 0;
ecx = 0;
eax = 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 void* seed) : offset(0) {
uint32_t eax, ecx, edx, ebx, ebp, esi, edi, ou, x;
uint8_t s[48];
memcpy(s, seed, sizeof(s));
for (size_t x = 0; x < 48; x += 3) {
s[x] ^= 0x19;
s[x + 1] ^= 0x16;
s[x + 2] ^= 0x18;
}
this->stream[0] = 0x243F6A88;
this->stream[1] = 0x85A308D3;
this->stream[2] = 0x13198A2E;
this->stream[3] = 0x03707344;
this->stream[4] = 0xA4093822;
this->stream[5] = 0x299F31D0;
this->stream[6] = 0x082EFA98;
this->stream[7] = 0xEC4E6C89;
this->stream[8] = 0x452821E6;
this->stream[9] = 0x38D01377;
this->stream[10] = 0xBE5466CF;
this->stream[11] = 0x34E90C6C;
this->stream[12] = 0xC0AC29B7;
this->stream[13] = 0xC97C50DD;
this->stream[14] = 0x3F84D5B5;
this->stream[15] = 0xB5470917;
this->stream[16] = 0x9216D5D9;
this->stream[17] = 0x8979FB1B;
memcpy(&this->stream[18], psobb_private_key, sizeof(psobb_private_key));
ecx = 0;
ebx = 0;
while (ebx < 0x12) {
ebp = static_cast<uint32_t>(s[ecx]) << 0x18;
eax = ecx + 1;
edx = eax - ((eax / 48) * 48);
eax = (static_cast<uint32_t>(s[edx]) << 0x10) & 0xFF0000;
ebp = (ebp | eax) & 0xffff00ff;
eax = ecx + 2;
edx = eax - ((eax / 48) * 48);
eax = (static_cast<uint32_t>(s[edx]) << 0x08) & 0xFF00;
ebp = (ebp | eax) & 0xffffff00;
eax = ecx + 3;
ecx = ecx + 4;
edx = eax - ((eax / 48) * 48);
eax = static_cast<uint32_t>(s[edx]);
ebp = ebp | eax;
eax = ecx;
edx = eax - ((eax / 48) * 48);
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;
}
}
+61
View File
@@ -0,0 +1,61 @@
#include <inttypes.h>
#include <stddef.h>
#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);
private:
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);
private:
void update_stream();
uint32_t next();
uint32_t stream[GC_STREAM_LENGTH];
uint16_t offset;
};
class PSOBBEncryption : public PSOEncryption {
public:
explicit PSOBBEncryption(const void* key);
virtual void encrypt(void* data, size_t size);
virtual void decrypt(void* data, size_t size);
private:
void update_stream();
uint32_t stream[BB_STREAM_LENGTH];
uint16_t offset;
};
+43
View File
@@ -0,0 +1,43 @@
#include "PSOProtocol.hh"
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;
}
}
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;
}
}
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;
}
}
+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;
};
+648
View File
@@ -0,0 +1,648 @@
#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;
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;
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;
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;
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;
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;
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;
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
decode_sjis(this->info_board, pc->info_board);
memcpy(&this->blocked, pc->blocked, sizeof(uint32_t) * 30);
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();
decode_sjis(this->info_board, gc.info_board, 0xAC);
memcpy(&this->blocked, gc.blocked, sizeof(uint32_t) * 30);
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)
char16cpy(this->info_board, bb.info_board, 0xAC);
memcpy(&this->blocked, bb.blocked, sizeof(uint32_t) * 30);
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;
char16cpy(bb.name, this->disp.name, 24);
char16cpy(bb.team_name, this->team_name, 16);
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);
char16cpy(bb.auto_reply, this->auto_reply, 0xAC);
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);
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);
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");
}
char16cpy(this->auto_reply, player.auto_reply, 0xAC);
this->bank = player.bank;
memcpy(&this->challenge_data, &player.challenge_data, 0x0140);
this->disp = player.disp;
char16cpy(this->guild_card_desc,player.guild_card_desc, 0x58);
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();
char16cpy(player.auto_reply, this->auto_reply, 0xAC);
player.bank = this->bank;
memcpy(&player.challenge_data, &this->challenge_data, 0x0140);
player.disp = this->disp;
char16cpy(player.guild_card_desc,this->guild_card_desc, 0x58);
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 "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;
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);
};
#define ERROR_COMBINE_ITEM_SPLIT 0x5FFFFFFE // this error code is used to tell calling functions that a combine item needs to be split
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);
+364
View File
@@ -0,0 +1,364 @@
#include "Quest.hh"
#include <string>
#include <unordered_map>
#include <phosg/Filesystem.hh>
#include <phosg/Strings.hh>
#include "Compression.hh"
#include "Text.hh"
using namespace std;
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::Government:
return "Government";
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 unknown1;
uint8_t unknown2;
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 unknown1;
uint8_t unknown2;
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;
uint16_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),
file_basename(bin_filename.substr(0, bin_filename.size() - 4)) {
string bin_basename;
{
size_t slash_pos = bin_filename.rfind('/');
if (slash_pos != string::npos) {
bin_basename = bin_filename.substr(slash_pos + 1);
} else {
bin_basename = bin_filename;
}
}
// 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 (bin_basename.empty()) {
throw invalid_argument("empty filename");
}
if (bin_basename[0] == 'b') {
this->category = QuestCategory::Battle;
} else if (bin_basename[0] == 'c') {
this->category = QuestCategory::Challenge;
} else if (bin_basename[0] == 'e') {
this->category = QuestCategory::Episode3;
} else if (bin_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(bin_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) {
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},
{"gov", QuestCategory::Government},
{"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({
{"dc1.bin", GameVersion::DC},
{"dc.bin", GameVersion::DC},
{"pc.bin", GameVersion::PC},
{"gc.bin", GameVersion::GC},
{"gc3.bin", GameVersion::GC},
{"bb.bin", 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] == "dc1.bin");
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) {
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->bin_contents_ptr) {
this->bin_contents_ptr.reset(new string(load_file(this->file_basename + ".dat")));
}
return this->bin_contents_ptr;
}
QuestIndex::QuestIndex(const char* directory) : directory(directory) {
for (const auto& filename : list_directory(this->directory)) {
string full_path = this->directory + "/" + filename;
if (ends_with(filename, ".gba")) {
this->gba_file_contents.emplace(filename, new string(load_file(full_path)));
continue;
}
if (ends_with(filename, ".bin")) {
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;
}
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#include <stdint.h>
#include <map>
#include <string>
#include "Version.hh"
enum class QuestCategory {
Unknown = -1,
Retrieval = 0,
Extermination,
Event,
Shop,
VR,
Tower,
Government,
Download,
Battle,
Challenge,
Solo,
Episode3,
};
bool category_is_mode(QuestCategory category);
const char* name_for_category(QuestCategory category);
struct Quest {
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
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;
};
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);
+19
View File
@@ -0,0 +1,19 @@
# newserv
newserv is a game server for Phantasy Star Online (PSO).
This project includes code that was reverse-engineered by the community in ages long past, and has been included in many projects since then. It also includes some game data from Phantasy Star Online itself; this data was originally created by Sega.
This project is a rewrite of a rewrite of a game server that I wrote many years ago. In its current state, do not expect this server to work - it builds on Mac OS, but I haven't found the time to set up a test environment where I can make the game connect to newserv.
## History
In ages long past (probably 2005? I honestly can't remember), I began writing a PSO server. This project became known as khyller, evolving into a full-featured environment supporting all versions of the game that I had access to - PC, GC, and BB. But as this evolution occurred, the code became increasingly ugly and hard to work with, littered with debugging filth that I never cleaned up and odd coding patterns that I had picked up over the years.
Sometime in 2006 or 2007, I abandoned khyller and rebuilt the entire thing from scratch, resulting in newserv. But this newserv was not the project you're looking at now; 2007's newserv was substantially cleaner in code than khyller but was still quite ugly, and it lacked a few of the more esoteric features I had originally written (for example, the ability to convert any quest into a download quest). I felt better about working with this code, but it still had some stability problems. It turns out that 2007's newserv's concurrency implementation was simply incorrect - I had derived the concept of a mutex myself (before taking any real computer engineering classes) but implemented it incorrectly. No wonder newserv would randomly crash after running seemingly fine for a few days.
Last weekend (October 2018), I had some random cause to reminisce. I looked back in my old code archives and came across newserv. Somehow inspired, I spent a weekend and a couple more evenings rewriting the entire project again, cleaning up ancient patterns I had used eleven years ago, replacing entire modules with simple STL containers, and eliminating even more support files in favor of configuration autodetection. The code is now suitably modern and the concurrency primitives it uses are correct (thought I haven't audited where exactly they're used; there are likely some missing lock contexts still).
## Future
Really, this project is mostly for my own nostalgia. Feel free to peruse if you'd like. If I'm suitably inspired again, I may boot up my copies of PSO and play around with it, and I'm sure I'll find numerous bugs the first time I do so. But I offer no guarantees on when this will happen, or if it will happen at all.
+25
View File
@@ -0,0 +1,25 @@
#include "RareItemSet.hh"
#include <phosg/Filesystem.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));
uint32_t x = ((rand() << 30) | (rand() << 15) | rand());
return (x < 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);
+2049
View File
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);
+2109
View File
File diff suppressed because it is too large Load Diff
+162
View File
@@ -0,0 +1,162 @@
#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
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(std::shared_ptr<Lobby> l, 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, 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<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);
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_chat_message(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);
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, std::shared_ptr<Client> c,
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_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);
+309
View File
@@ -0,0 +1,309 @@
#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 <thread>
#include "PSOProtocol.hh"
#include "ReceiveCommands.hh"
using namespace std;
Server::WorkerThread::WorkerThread(Server* server, int worker_num) :
server(server), worker_num(worker_num),
base(event_base_new(), event_base_free), t() {
this->thread_name = string_printf("Server::run_thread (worker_num=%d)",
worker_num);
}
void Server::WorkerThread::disconnect_client(struct bufferevent* bev) {
auto client = this->bev_to_client.at(bev);
this->bev_to_client.erase(bev);
{
rw_guard g(client->lock, true);
bufferevent_free(client->bev);
client->bev = NULL;
}
this->server->client_count--;
}
void Server::WorkerThread::dispatch_on_listen_accept(
struct evconnlistener *listener, evutil_socket_t fd,
struct sockaddr *address, int socklen, void *ctx) {
WorkerThread* wt = (WorkerThread*)ctx;
wt->server->on_listen_accept(*wt, listener, fd, address, socklen);
}
void Server::WorkerThread::dispatch_on_listen_error(
struct evconnlistener *listener, void *ctx) {
WorkerThread* wt = (WorkerThread*)ctx;
wt->server->on_listen_error(*wt, listener);
}
void Server::WorkerThread::dispatch_on_client_input(
struct bufferevent *bev, void *ctx) {
WorkerThread* wt = (WorkerThread*)ctx;
wt->server->on_client_input(*wt, bev);
}
void Server::WorkerThread::dispatch_on_client_error(
struct bufferevent *bev, short events, void *ctx) {
WorkerThread* wt = (WorkerThread*)ctx;
wt->server->on_client_error(*wt, bev, events);
}
void Server::WorkerThread::dispatch_check_for_thread_exit(
evutil_socket_t fd, short what, void* ctx) {
WorkerThread* wt = (WorkerThread*)ctx;
wt->server->check_for_thread_exit(*wt, fd, what);
}
void Server::on_listen_accept(Server::WorkerThread& wt,
struct evconnlistener *listener, evutil_socket_t fd,
struct sockaddr *address, int socklen) {
int fd_flags = fcntl(fd, F_GETFD, 0);
if (fd_flags >= 0) {
fcntl(fd, F_SETFD, fd_flags | FD_CLOEXEC);
}
int listen_fd = evconnlistener_get_fd(listener);
GameVersion version;
ServerBehavior initial_state;
try {
auto p = this->listen_fd_to_version_and_state.at(listen_fd);
version = p.first;
initial_state = p.second;
} catch (const out_of_range& e) {
log(WARNING, "[Server] can\'t determine version for socket %d; disconnecting client",
listen_fd);
close(fd);
return;
}
struct bufferevent *bev = bufferevent_socket_new(wt.base.get(), fd,
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_THREADSAFE | BEV_OPT_DEFER_CALLBACKS | BEV_OPT_UNLOCK_CALLBACKS);
auto emplace_ret = wt.bev_to_client.emplace(bev, new Client(bev, version, initial_state));
this->client_count++;
bufferevent_setcb(bev, &WorkerThread::dispatch_on_client_input, NULL,
&WorkerThread::dispatch_on_client_error, &wt);
bufferevent_enable(bev, EV_READ | EV_WRITE);
this->process_client_connect(emplace_ret.first->second);
}
void Server::on_listen_error(Server::WorkerThread& wt,
struct evconnlistener *listener) {
int err = EVUTIL_SOCKET_ERROR();
log(ERROR, "[Server] failure on listening socket %d: %d (%s)\n",
evconnlistener_get_fd(listener), err,
evutil_socket_error_to_string(err));
event_base_loopexit(wt.base.get(), NULL);
}
void Server::on_client_input(Server::WorkerThread& wt,
struct bufferevent *bev) {
shared_ptr<Client> c;
try {
c = wt.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
struct evbuffer* in_buffer = bufferevent_get_input(bev);
evbuffer_drain(in_buffer, evbuffer_get_length(in_buffer));
return;
}
if (c->should_disconnect) {
wt.disconnect_client(bev);
this->process_client_disconnect(c);
return;
}
c->last_recv_time = now();
this->receive_and_process_commands(c, bev);
}
void Server::on_client_error(Server::WorkerThread& wt,
struct bufferevent *bev, short events) {
shared_ptr<Client> c;
try {
c = wt.bev_to_client.at(bev);
} catch (const out_of_range& e) { }
if (events & BEV_EVENT_ERROR) {
int err = EVUTIL_SOCKET_ERROR();
log(WARNING, "[Server] client caused %d (%s)\n", err,
evutil_socket_error_to_string(err));
}
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
wt.disconnect_client(bev);
if (c) {
this->process_client_disconnect(c);
}
}
}
void Server::check_for_thread_exit(Server::WorkerThread& wt,
evutil_socket_t fd, short what) {
if (this->should_exit) {
event_base_loopexit(wt.base.get(), NULL);
}
}
void Server::receive_and_process_commands(shared_ptr<Client> c, struct bufferevent* bev) {
struct evbuffer* buf = bufferevent_get_input(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) != 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');
process_command(this->state, c, header->command(c->version),
header->flag(c->version), size - header_size, data.data());
// 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);
}
void Server::process_client_connect(std::shared_ptr<Client> c) {
process_connect(this->state, c);
}
void Server::process_client_disconnect(std::shared_ptr<Client> c) {
process_disconnect(this->state, c);
}
void Server::run_thread(int worker_num) {
WorkerThread& wt = this->threads[worker_num];
struct timeval tv = usecs_to_timeval(2000000);
struct event* ev = event_new(wt.base.get(), -1, EV_PERSIST,
&WorkerThread::dispatch_check_for_thread_exit, &wt);
event_add(ev, &tv);
event_base_dispatch(wt.base.get());
event_del(ev);
}
Server::Server(shared_ptr<ServerState> state) :
should_exit(false), client_count(0), state(state) {
for (size_t x = 0; x < this->state->num_threads; x++) {
this->threads.emplace_back(this, x);
}
}
void Server::listen(const string& socket_path, GameVersion version, ServerBehavior initial_state) {
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, initial_state);
}
void Server::listen(const string& addr, int port, GameVersion version, ServerBehavior initial_state) {
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, initial_state);
}
void Server::listen(int port, GameVersion version, ServerBehavior initial_state) {
this->listen("", port, version, initial_state);
}
void Server::add_socket(int fd, GameVersion version, ServerBehavior initial_state) {
this->listen_fd_to_version_and_state.emplace(piecewise_construct,
forward_as_tuple(fd), forward_as_tuple(version, initial_state));
}
void Server::start() {
for (auto& wt : this->threads) {
for (const auto& it : this->listen_fd_to_version_and_state) {
struct evconnlistener* listener = evconnlistener_new(wt.base.get(),
WorkerThread::dispatch_on_listen_accept, &wt, LEV_OPT_REUSEABLE, 0,
it.first);
if (!listener) {
throw runtime_error("can\'t create evconnlistener");
}
evconnlistener_set_error_cb(listener, WorkerThread::dispatch_on_listen_error);
wt.listeners.emplace(listener, evconnlistener_free);
}
wt.t = thread(&Server::run_thread, this, wt.worker_num);
}
}
void Server::schedule_stop() {
log(INFO, "[Server] scheduling exit for all threads");
this->should_exit = true;
for (const auto& it : listen_fd_to_version_and_state) {
log(INFO, "[Server] closing listening fd %d", it.first);
close(it.first);
}
}
void Server::wait_for_stop() {
for (auto& wt : this->threads) {
if (!wt.t.joinable()) {
continue;
}
log(INFO, "[Server] waiting for worker %d to terminate", wt.worker_num);
wt.t.join();
}
log(INFO, "[Server] shutdown complete");
}
+77
View File
@@ -0,0 +1,77 @@
#pragma once
#include <event2/event.h>
#include <unordered_set>
#include <vector>
#include <string>
#include <memory>
#include <thread>
#include "Client.hh"
#include "ServerState.hh"
class Server {
private:
struct WorkerThread {
Server* server;
int worker_num;
std::unique_ptr<struct event_base, void(*)(struct event_base*)> base;
std::unordered_set<std::unique_ptr<struct evconnlistener, void(*)(struct evconnlistener*)>> listeners;
std::unordered_map<struct bufferevent*, std::shared_ptr<Client>> bev_to_client;
std::thread t;
std::string thread_name;
WorkerThread(Server* server, int worker_num);
void disconnect_client(struct bufferevent* bev);
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_check_for_thread_exit(evutil_socket_t fd, short what, void* ctx);
};
std::atomic<bool> should_exit;
std::vector<WorkerThread> threads;
std::atomic<size_t> client_count;
std::unordered_map<int, std::pair<GameVersion, ServerBehavior>> listen_fd_to_version_and_state;
std::shared_ptr<ServerState> state;
void on_listen_accept(WorkerThread& wt, struct evconnlistener *listener,
evutil_socket_t fd, struct sockaddr *address, int socklen);
void on_listen_error(WorkerThread& wt, struct evconnlistener *listener);
void on_client_input(WorkerThread& wt, struct bufferevent *bev);
void on_client_error(WorkerThread& wt, struct bufferevent *bev, short events);
void check_for_thread_exit(WorkerThread& wt, evutil_socket_t fd, short what);
void receive_and_process_commands(std::shared_ptr<Client> c, struct bufferevent* buf);
void process_client_connect(std::shared_ptr<Client> c);
void process_client_disconnect(std::shared_ptr<Client> c);
void process_client_command(std::shared_ptr<Client> c, const std::string& command);
void run_thread(int thread_id);
public:
Server() = delete;
Server(const Server&) = delete;
Server(Server&&) = delete;
Server(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);
virtual void start();
virtual void schedule_stop();
virtual void wait_for_stop();
};
+175
View File
@@ -0,0 +1,175 @@
#include "ServerState.hh"
#include <memory>
#include "SendCommands.hh"
#include "Text.hh"
using namespace std;
ServerState::ServerState() : next_lobby_id(1), next_game_id(-1) {
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 < 15; x++) {
shared_ptr<Lobby> l(new Lobby());
l->flags |= LobbyFlag::Public | LobbyFlag::Default;
this->add_lobby(l);
}
for (size_t x = 0; x < 5; x++) {
shared_ptr<Lobby> l(new Lobby());
l->flags |= LobbyFlag::Public | LobbyFlag::Default | LobbyFlag::Episode3;
this->add_lobby(l);
}
}
void ServerState::add_client_to_available_lobby(shared_ptr<Client> c) {
rw_guard g(this->lobbies_lock, false);
// nonnegative lobby IDs are public, so start at 0
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) {
rw_guard g(this->lobbies_lock, false);
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);
}
this->send_lobby_join_notifications(new_lobby, c);
}
void ServerState::send_lobby_join_notifications(shared_ptr<Lobby> l,
shared_ptr<Client> joining_client) {
rw_guard g2(l->lock, false);
for (auto& other_client : l->clients) {
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(int64_t lobby_id) {
rw_guard g(this->lobbies_lock, false);
return this->id_to_lobby.at(lobby_id);
}
shared_ptr<Lobby> ServerState::find_lobby(const u16string& name) {
rw_guard g(this->lobbies_lock, false);
return this->name_to_lobby.at(name);
}
vector<shared_ptr<Lobby>> ServerState::all_lobbies() {
rw_guard g(this->lobbies_lock, false);
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) {
if (l->is_game()) {
l->lobby_id = this->next_game_id--;
} else {
l->lobby_id = this->next_lobby_id++;
}
rw_guard g(this->lobbies_lock, true);
if (this->id_to_lobby.count(l->lobby_id)) {
throw logic_error("lobby already exists with the given id");
}
if (this->name_to_lobby.count(l->name)) {
throw invalid_argument("lobby already exists with the given name");
}
this->id_to_lobby.emplace(l->lobby_id, l);
if (l->name[0]) {
this->name_to_lobby.emplace(l->name, l);
}
}
void ServerState::remove_lobby(int64_t lobby_id) {
rw_guard g(this->lobbies_lock, true);
auto it = this->id_to_lobby.find(lobby_id);
if (it == this->id_to_lobby.end()) {
return;
}
if (it->second->name[0]) {
this->name_to_lobby.erase(it->second->name);
}
this->id_to_lobby.erase(it);
}
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");
}
+74
View File
@@ -0,0 +1,74 @@
#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 {
std::string name;
std::unordered_map<std::string, PortConfiguration> port_configuration;
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::unordered_map<uint32_t, std::u16string>> id_to_information_contents;
size_t num_threads;
rw_lock lobbies_lock;
std::map<int64_t, std::shared_ptr<Lobby>> id_to_lobby;
std::unordered_map<std::u16string, std::shared_ptr<Lobby>> name_to_lobby;
std::atomic<int64_t> next_lobby_id;
std::atomic<int64_t> next_game_id;
std::set<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(int64_t lobby_id);
std::shared_ptr<Lobby> find_lobby(const std::u16string& name);
std::vector<std::shared_ptr<Lobby>> all_lobbies();
void add_lobby(std::shared_ptr<Lobby> l);
void remove_lobby(int64_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);
};
+155
View File
@@ -0,0 +1,155 @@
#include "Text.hh"
#include <stdarg.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
int char16cmp(const char16_t* s1, const char16_t* s2, size_t count) {
return char_traits<char16_t>::compare(s1, s2, count);
}
char16_t* char16cpy(char16_t* dest, const char16_t* src, size_t count) {
return char_traits<char16_t>::copy(dest, src, count);
}
size_t char16len(const char16_t* s) {
return char_traits<char16_t>::length(s);
}
// None of these functions truly convert between SJIS and Unicode. They will
// convert English properly (and some other languages as well), but Japanese
// text will screw up horribly
// TODO: fix this shit. this is definitely the worst part of this entire project
void encode_sjis(char* dest, const char16_t* source, size_t max) {
while (*source && (--max)) {
*(dest++) = *(source++);
};
*dest = 0;
}
void decode_sjis(char16_t* dest, const char* source, size_t max) {
while (*source && (--max)) {
*(dest++) = *(source++);
};
*dest = 0;
}
std::string encode_sjis(const char16_t* source) {
string ret;
while (*source) {
ret.push_back(*(source++));
};
return ret;
}
std::u16string decode_sjis(const char* source) {
u16string ret;
while (*source) {
ret.push_back(*(source++));
};
return ret;
}
std::string encode_sjis(const std::u16string& source) {
string ret;
for (char16_t ch : source) {
ret.push_back(ch);
};
return ret;
}
std::u16string decode_sjis(const std::string& source) {
u16string ret;
for (char16_t ch : source) {
ret.push_back(ch);
};
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);
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);
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);
}
+65
View File
@@ -0,0 +1,65 @@
#pragma once
#include <inttypes.h>
#include <stddef.h>
#include <string>
int char16cmp(const char16_t* s1, const char16_t* s2, size_t count);
char16_t* 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>
void add_color_inplace(T* a) {
T* d = a;
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;
}
}
a++;
}
}
+57
View File
@@ -0,0 +1,57 @@
#include "Version.hh"
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";
}
}
+39
View File
@@ -0,0 +1,39 @@
#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,
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);
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+154
View File
@@ -0,0 +1,154 @@
# Types of nonrare items that enemies drop.
# NORMAL MODE
# shield = 01
# armor = 02
# unit = 03
# random = 04
# unknown = 05
# weapon = 06
Drop_Item_00_00 06 # booma
Drop_Item_00_01 06 # gobooma
Drop_Item_00_02 01 # gigobooma
Drop_Item_00_03 02 # savage wolf
Drop_Item_00_04 06 # barbarous wolf
Drop_Item_00_05 02 # rag rappy
Drop_Item_00_06 01 # al rappy
Drop_Item_00_07 06 # mothmant
Drop_Item_00_7A 05 # monest
Drop_Item_00_08 06 # hildebear
Drop_Item_00_09 06 # hildeblue
Drop_Item_00_0A 04 # dragon
Drop_Item_00_0B 06 # evil shark
Drop_Item_00_0C 06 # pal shark
Drop_Item_00_0D 03 # guil shark
Drop_Item_00_0E 02 # poison lily
Drop_Item_00_0F 03 # nar lily
Drop_Item_00_10 06 # grass assassin
Drop_Item_00_11 01 # nano dragon
Drop_Item_00_12 02 # pofuilly slime
Drop_Item_00_13 03 # pouilly slime
Drop_Item_00_14 01 # pan arms
Drop_Item_00_15 03 # migium
Drop_Item_00_16 02 # hidoom
Drop_Item_00_17 04 # de rol le
Drop_Item_00_18 06 # gillchic
Drop_Item_00_19 06 # dubchic
Drop_Item_00_1A 01 # canadine
Drop_Item_00_1B 02 # canane
Drop_Item_00_1C 06 # sinow beat
Drop_Item_00_1D 03 # sinow gold
Drop_Item_00_1E 01 # garanz
Drop_Item_00_1F 04 # vol opt
Drop_Item_00_20 06 # dimenian
Drop_Item_00_21 06 # la dimenian
Drop_Item_00_22 02 # so dimenian
Drop_Item_00_23 06 # claw
Drop_Item_00_24 03 # bulclaw
Drop_Item_00_25 06 # bulk
Drop_Item_00_26 02 # delsaber
Drop_Item_00_27 02 # dark belra
Drop_Item_00_28 01 # dark gunner
Drop_Item_00_29 01 # death dunner
Drop_Item_00_2A 03 # chaos sorceror
Drop_Item_00_2B 03 # chaos bringer
Drop_Item_00_2C 04 # dark falz
Drop_Item_00_2E 06 # dimenian
Drop_Item_00_2F 06 # la dimenian
Drop_Item_00_30 01 # so dimenian
Drop_Item_00_31 02 # rag rappy
Drop_Item_00_32 01 # love rappy
Drop_Item_00_33 04 # hallo rappy
Drop_Item_00_34 04 # saint rappy
Drop_Item_00_35 04 # egg rappy
Drop_Item_00_36 06 # grass assassin
Drop_Item_00_37 02 # poison lily
Drop_Item_00_38 03 # nar lily
Drop_Item_00_39 06 # mothmant
Drop_Item_00_3A 06 # hildebear
Drop_Item_00_3B 06 # hildeblue
Drop_Item_00_3C 02 # dark belra
Drop_Item_00_3D 04 # barba ray
Drop_Item_00_3E 02 # savage wolf
Drop_Item_00_3F 06 # barbarous wolf
Drop_Item_00_40 06 # gillchic
Drop_Item_00_41 06 # dubchic
Drop_Item_00_42 02 # delsaber
Drop_Item_00_43 01 # pan arms
Drop_Item_00_44 03 # migium
Drop_Item_00_45 02 # hidoom
Drop_Item_00_46 01 # garanz
Drop_Item_00_47 03 # chaos sorceror
Drop_Item_00_48 04 # gol dragon
Drop_Item_00_49 06 # merillia
Drop_Item_00_4A 06 # meriltas
Drop_Item_00_4B 06 # ul gibbon
Drop_Item_00_4C 01 # zol gibbon
Drop_Item_00_4D 06 # gee
Drop_Item_00_4E 06 # sinow berill
Drop_Item_00_4F 02 # sinow spigell
Drop_Item_00_50 05 # mericarol
Drop_Item_00_51 02 # merikle
Drop_Item_00_52 01 # mericus
Drop_Item_00_53 03 # gibbles
Drop_Item_00_54 01 # gi gue
Drop_Item_00_55 06 # del lily
Drop_Item_00_56 02 # ill gill
Drop_Item_00_57 03 # epsilon
Drop_Item_00_58 04 # gal gryphon
Drop_Item_00_59 06 # dolmolm
Drop_Item_00_5A 06 # dolmdarl
Drop_Item_00_5B 03 # recobox
Drop_Item_00_5C 06 # recon
Drop_Item_00_5D 01 # deldepth
Drop_Item_00_5E 06 # delbiter
Drop_Item_00_5F 06 # morfos
Drop_Item_00_60 06 # sinow zoa
Drop_Item_00_61 02 # sinow zele
Drop_Item_00_62 04 # olga flow
Drop_Item_00_64 05 # boota
Drop_Item_00_65 05 # ze boota
Drop_Item_00_66 05 # ba boota
Drop_Item_00_67 05 # sand rappy
Drop_Item_00_68 05 # del rappy
Drop_Item_00_69 05 # satellite lizard
Drop_Item_00_6A 05 # yowie
Drop_Item_00_6B 05 # astark
Drop_Item_00_6C 05 # zu
Drop_Item_00_6D 05 # pazuzu
Drop_Item_00_6E 05 # dorphon
Drop_Item_00_6F 05 # dorphon eclair
Drop_Item_00_70 05 # goran
Drop_Item_00_71 05 # pyro goran
Drop_Item_00_72 05 # goran detonator
Drop_Item_00_73 05 # merissa a
Drop_Item_00_74 05 # merissa aa
Drop_Item_00_75 05 # girtablulu
Drop_Item_00_76 05 # saint-million
Drop_Item_00_77 05 # shambertin
Drop_Item_00_78 05 # kondrieu
# HARD MODE
Drop_Item_01_30 02 # so dimenian
Drop_Item_01_56 01 # ill gill
# VERY HARD MODE
Drop_Item_02_02 03 # gigobooma
Drop_Item_02_04 01 # barbarous wolf 08
Drop_Item_02_05 01 # rag rappy 05
Drop_Item_02_31 01 # rag rappy
Drop_Item_02_3F 01 # barbarous wolf
Drop_Item_02_56 02 # ill gill
# ULTIMATE MODE
# no changes
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More