initial commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.DS_Store
|
||||
*.o
|
||||
newserv
|
||||
+739
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
|
||||
|
||||
struct BattleParams {
|
||||
uint16_t atp; // attack power
|
||||
uint16_t psv; // perseverance (intelligence?)
|
||||
uint16_t evp; // evasion
|
||||
uint16_t hp; // hit points
|
||||
uint16_t dfp; // defense
|
||||
uint16_t ata; // accuracy
|
||||
uint16_t lck; // luck
|
||||
uint8_t unknown[14];
|
||||
uint32_t experience;
|
||||
uint32_t difficulty;
|
||||
};
|
||||
|
||||
struct BattleParamTable {
|
||||
BattleParams entries[2][3][4][0x60]; // online/offline, episode, difficulty, monster type
|
||||
|
||||
BattleParamTable(const char* filename_prefix);
|
||||
|
||||
const BattleParams& get(bool solo, uint8_t episode, uint8_t difficulty,
|
||||
uint8_t monster_type) const;
|
||||
const BattleParams* get_subtable(bool solo, uint8_t episode,
|
||||
uint8_t difficulty) const;
|
||||
};
|
||||
|
||||
|
||||
|
||||
struct BattleParamIndex {
|
||||
BattleParamTable table_for_episode[3];
|
||||
};
|
||||
|
||||
// an enemy entry as loaded by the game
|
||||
struct PSOEnemy {
|
||||
uint16_t unused;
|
||||
uint8_t hit_flags;
|
||||
uint8_t last_hit;
|
||||
uint32_t experience;
|
||||
uint32_t rt_index;
|
||||
|
||||
PSOEnemy();
|
||||
PSOEnemy(uint32_t experience, uint32_t rt_index);
|
||||
};
|
||||
|
||||
std::vector<PSOEnemy> load_map(const char* filename, uint8_t episode,
|
||||
uint8_t difficulty, const BattleParams* bp, bool alt_enemies);
|
||||
@@ -0,0 +1,9 @@
|
||||
#include "Menu.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
MenuItem::MenuItem(uint32_t item_id, const u16string& name,
|
||||
const u16string& description, uint32_t flags) : item_id(item_id), name(name),
|
||||
description(description), flags(flags) { }
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
|
||||
|
||||
enum MenuItemFlag {
|
||||
InvisibleOnDC = 0x01,
|
||||
InvisibleOnPC = 0x02,
|
||||
InvisibleOnGC = 0x04,
|
||||
InvisibleOnGCEpisode3 = 0x08,
|
||||
InvisibleOnBB = 0x10,
|
||||
RequiresMessageBoxes = 0x20,
|
||||
};
|
||||
|
||||
struct MenuItem {
|
||||
uint32_t item_id;
|
||||
std::u16string name;
|
||||
std::u16string description;
|
||||
uint32_t flags;
|
||||
|
||||
MenuItem(uint32_t item_id, const std::u16string& name,
|
||||
const std::u16string& description, uint32_t flags);
|
||||
};
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "Client.hh"
|
||||
#include "ServerState.hh"
|
||||
|
||||
|
||||
void process_connect(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c);
|
||||
void process_disconnect(std::shared_ptr<ServerState> s,
|
||||
std::shared_ptr<Client> c);
|
||||
void process_command(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c,
|
||||
uint16_t command, uint32_t flag, uint16_t size, const void* data);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#include "PSOProtocol.hh"
|
||||
#include "Client.hh"
|
||||
#include "Lobby.hh"
|
||||
#include "Client.hh"
|
||||
#include "ServerState.hh"
|
||||
|
||||
|
||||
void check_size(uint16_t size, uint16_t min_size, uint16_t max_size = 0);
|
||||
|
||||
void process_subcommand(std::shared_ptr<ServerState> s,
|
||||
std::shared_ptr<Lobby> l, std::shared_ptr<Client> c, uint8_t command,
|
||||
uint8_t flag, const PSOSubcommand* sub, size_t count);
|
||||
+2109
File diff suppressed because it is too large
Load Diff
+162
@@ -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);
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
+154
@@ -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
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user