fix cross-version lobby appearance and name colors
This commit is contained in:
+2
-2
@@ -834,7 +834,7 @@ static void server_command_edit(shared_ptr<Client> c, const std::u16string& args
|
||||
} else if (tokens.at(0) == "npc") {
|
||||
if (tokens.at(1) == "none") {
|
||||
p->disp.visual.extra_model = 0;
|
||||
p->disp.visual.v2_flags &= 0xFD;
|
||||
p->disp.visual.validation_flags &= 0xFD;
|
||||
} else {
|
||||
uint8_t npc = npc_for_name(decode_sjis(tokens.at(1)));
|
||||
if (npc == 0xFF) {
|
||||
@@ -842,7 +842,7 @@ static void server_command_edit(shared_ptr<Client> c, const std::u16string& args
|
||||
return;
|
||||
}
|
||||
p->disp.visual.extra_model = npc;
|
||||
p->disp.visual.v2_flags |= 0x02;
|
||||
p->disp.visual.validation_flags |= 0x02;
|
||||
}
|
||||
} else if (tokens.at(0) == "tech") {
|
||||
uint8_t level = stoul(tokens.at(2)) - 1;
|
||||
|
||||
@@ -1258,10 +1258,11 @@ struct S_JoinGame_GC_Ep3_64 : S_JoinGame_GC_64 {
|
||||
// This field is only present if the game (and client) is Episode 3. Similarly
|
||||
// to lobby_data in the base struct, all four of these are always present and
|
||||
// they are filled in in slot positions.
|
||||
struct {
|
||||
struct Ep3PlayerEntry {
|
||||
PlayerInventory inventory;
|
||||
PlayerDispDataDCPCV3 disp;
|
||||
} __packed__ players_ep3[4];
|
||||
} __packed__;
|
||||
parray<Ep3PlayerEntry, 4> players_ep3;
|
||||
} __packed__;
|
||||
|
||||
struct S_JoinGame_XB_64 : S_JoinGame_DC_PC<PlayerLobbyDataXB> {
|
||||
|
||||
+113
-15
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Hash.hh>
|
||||
#include <phosg/Random.hh>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "ItemData.hh"
|
||||
@@ -19,29 +20,126 @@ using namespace std;
|
||||
|
||||
FileContentsCache player_files_cache(300 * 1000 * 1000);
|
||||
|
||||
uint32_t PlayerVisualConfig::compute_name_color_checksum(uint32_t name_color) {
|
||||
uint8_t x = (random_object<uint32_t>() % 0xFF) + 1;
|
||||
uint8_t y = (random_object<uint32_t>() % 0xFF) + 1;
|
||||
// name_color = ABCDEFGHabcdefghIJKLMNOPijklmnop
|
||||
// name_color_checksum = ---------ijklmabcdeIJKLM-------- ^ (xxxxxxxxyyyyyyyyxxxxxxxxyyyyyyyy)
|
||||
uint32_t xbrgx95558 = ((name_color << 15) & 0x007C0000) | ((name_color >> 6) & 0x0003E000) | ((name_color >> 3) & 0x00001F00);
|
||||
uint32_t mask = (x << 24) | (y << 16) | (x << 8) | y;
|
||||
return xbrgx95558 ^ mask;
|
||||
}
|
||||
|
||||
void PlayerVisualConfig::compute_name_color_checksum() {
|
||||
this->name_color_checksum = this->compute_name_color_checksum(this->name_color);
|
||||
}
|
||||
|
||||
void PlayerDispDataDCPCV3::enforce_lobby_join_limits(GameVersion target_version) {
|
||||
struct ClassMaxes {
|
||||
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;
|
||||
};
|
||||
static constexpr ClassMaxes v1_v2_class_maxes[14] = {
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x0007, 0x0100, 0x0100, 0x0100},
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x000A, 0x0100, 0x0100, 0x0100},
|
||||
{0x0000, 0x0009, 0x0000, 0x0005, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x0007, 0x0100, 0x0100, 0x0100},
|
||||
{0x0000, 0x0009, 0x0000, 0x0005, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0009, 0x0000, 0x0005, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x000A, 0x0100, 0x0100, 0x0100},
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x0007, 0x0100, 0x0100, 0x0100},
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x000A, 0x0100, 0x0100, 0x0100},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
};
|
||||
static constexpr ClassMaxes v3_v4_class_maxes[19] = {
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A, 0x0100, 0x0100, 0x0100},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A, 0x0100, 0x0100, 0x0100},
|
||||
{0x0000, 0x0019, 0x0000, 0x0005, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A, 0x0100, 0x0100, 0x0100},
|
||||
{0x0000, 0x0019, 0x0000, 0x0005, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0019, 0x0000, 0x0005, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A, 0x0100, 0x0100, 0x0100},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A, 0x0100, 0x0100, 0x0100},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A, 0x0100, 0x0100, 0x0100},
|
||||
{0x0000, 0x0019, 0x0000, 0x0005, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A, 0x0100, 0x0100, 0x0100},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A, 0x0100, 0x0100, 0x0100},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0001, 0x0100, 0x0100, 0x0100},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0001, 0x0100, 0x0100, 0x0100},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}};
|
||||
|
||||
const ClassMaxes* maxes;
|
||||
if ((target_version == GameVersion::PC) || (target_version == GameVersion::DC)) {
|
||||
// V1/V2 have fewer classes, so we'll substitute some here
|
||||
if (this->visual.char_class == 9) {
|
||||
switch (this->visual.char_class) {
|
||||
case 0: // HUmar
|
||||
case 1: // HUnewearl
|
||||
case 2: // HUcast
|
||||
case 3: // RAmar
|
||||
case 4: // RAcast
|
||||
case 5: // RAcaseal
|
||||
case 6: // FOmarl
|
||||
case 7: // FOnewm
|
||||
case 8: // FOnewearl
|
||||
case 12: // V3 custom 1
|
||||
case 13: // V3 custom 2
|
||||
break;
|
||||
case 9: // HUcaseal
|
||||
this->visual.char_class = 5; // HUcaseal -> RAcaseal
|
||||
} else if (this->visual.char_class == 10) {
|
||||
break;
|
||||
case 10: // FOmar
|
||||
this->visual.char_class = 0; // FOmar -> HUmar
|
||||
} else if (this->visual.char_class == 11) {
|
||||
break;
|
||||
case 11: // RAmarl
|
||||
this->visual.char_class = 1; // RAmarl -> HUnewearl
|
||||
break;
|
||||
case 14: // V2 custom 1 / V3 custom 3
|
||||
case 15: // V2 custom 2 / V3 custom 4
|
||||
case 16: // V2 custom 3 / V3 custom 5
|
||||
case 17: // V2 custom 4 / V3 custom 6
|
||||
case 18: // V2 custom 5 / V3 custom 7
|
||||
this->visual.char_class -= 5;
|
||||
break;
|
||||
default:
|
||||
this->visual.char_class = 0; // Invalid classes -> HUmar
|
||||
}
|
||||
|
||||
// V1/V2 has fewer costumes and android skins, so substitute them here too
|
||||
this->visual.costume %= 9;
|
||||
this->visual.skin %= 9;
|
||||
|
||||
// If the player is somehow still not a valid class, make them appear as the
|
||||
// "ninja" NPC
|
||||
if (this->visual.char_class > 8) {
|
||||
this->visual.extra_model = 0;
|
||||
this->visual.v2_flags |= 2;
|
||||
}
|
||||
maxes = &v1_v2_class_maxes[this->visual.char_class];
|
||||
this->visual.version = 2;
|
||||
|
||||
} else {
|
||||
if (this->visual.char_class >= 19) {
|
||||
this->visual.char_class = 0; // Invalid classes -> HUmar
|
||||
}
|
||||
maxes = &v3_v4_class_maxes[this->visual.char_class];
|
||||
}
|
||||
|
||||
// V1/V2 has fewer costumes and android skins, so substitute them here
|
||||
this->visual.costume %= maxes->costume;
|
||||
this->visual.skin %= maxes->skin;
|
||||
this->visual.face %= maxes->face;
|
||||
this->visual.head %= maxes->head;
|
||||
this->visual.hair %= maxes->hair;
|
||||
this->visual.hair_r %= maxes->hair_r;
|
||||
this->visual.hair_g %= maxes->hair_g;
|
||||
this->visual.hair_b %= maxes->hair_b;
|
||||
|
||||
this->visual.compute_name_color_checksum();
|
||||
this->visual.class_flags = class_flags_for_class(this->visual.char_class);
|
||||
}
|
||||
|
||||
void PlayerDispDataBB::enforce_lobby_join_limits(GameVersion) {
|
||||
@@ -90,10 +188,10 @@ void PlayerDispDataBB::apply_preview(const PlayerDispDataBBPreview& pre) {
|
||||
void PlayerDispDataBB::apply_dressing_room(const PlayerDispDataBBPreview& pre) {
|
||||
this->visual.name_color = pre.visual.name_color;
|
||||
this->visual.extra_model = pre.visual.extra_model;
|
||||
this->visual.unknown_a3 = pre.visual.unknown_a3;
|
||||
this->visual.name_color_checksum = pre.visual.name_color_checksum;
|
||||
this->visual.section_id = pre.visual.section_id;
|
||||
this->visual.char_class = pre.visual.char_class;
|
||||
this->visual.v2_flags = pre.visual.v2_flags;
|
||||
this->visual.validation_flags = pre.visual.validation_flags;
|
||||
this->visual.version = pre.visual.version;
|
||||
this->visual.class_flags = pre.visual.class_flags;
|
||||
this->visual.costume = pre.visual.costume;
|
||||
|
||||
@@ -104,13 +104,21 @@ struct PlayerDispDataBB;
|
||||
struct PlayerVisualConfig {
|
||||
/* 00 */ ptext<char, 0x10> name;
|
||||
/* 10 */ parray<uint8_t, 8> unknown_a2;
|
||||
/* 18 */ le_uint32_t name_color = 0x00000000; // RGBA
|
||||
/* 18 */ le_uint32_t name_color = 0x00000000; // ARGB
|
||||
/* 1C */ uint8_t extra_model = 0;
|
||||
/* 1D */ parray<uint8_t, 0x0F> unused;
|
||||
/* 2C */ le_uint32_t unknown_a3 = 0;
|
||||
// See compute_name_color_checksum for details on how this is computed. This
|
||||
// field is ignored on V3 and BB.
|
||||
/* 2C */ le_uint32_t name_color_checksum = 0;
|
||||
/* 30 */ uint8_t section_id = 0;
|
||||
/* 31 */ uint8_t char_class = 0;
|
||||
/* 32 */ uint8_t v2_flags = 0;
|
||||
// validation_flags specifies that some parts of this structure are not valid
|
||||
// and should be ignored. The bits are:
|
||||
// -----FCS
|
||||
// F = class_flags is incorrect for the character's char_class value
|
||||
// C = char_class is out of range
|
||||
// S = section_id is out of range
|
||||
/* 32 */ uint8_t validation_flags = 0;
|
||||
/* 33 */ uint8_t version = 0;
|
||||
// class_flags specifies features of the character's class. The bits are:
|
||||
// -------- -------- -------- FRHANMfm
|
||||
@@ -129,6 +137,9 @@ struct PlayerVisualConfig {
|
||||
/* 48 */ le_float proportion_x = 0.0;
|
||||
/* 4C */ le_float proportion_y = 0.0;
|
||||
/* 50 */
|
||||
|
||||
static uint32_t compute_name_color_checksum(uint32_t name_color);
|
||||
void compute_name_color_checksum();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerDispDataDCPCV3 {
|
||||
|
||||
+2
-2
@@ -1405,11 +1405,11 @@ std::string disassemble_quest_script(const void* data, size_t size, QuestScriptV
|
||||
lines.emplace_back(string_printf(" %04zX extra_model %02hhX", l->offset + offsetof(PlayerVisualConfig, extra_model), visual.extra_model));
|
||||
string unused = format_data_string(visual.unused.data(), visual.unused.bytes());
|
||||
lines.emplace_back(string_printf(" %04zX unused %s", l->offset + offsetof(PlayerVisualConfig, unused), unused.c_str()));
|
||||
lines.emplace_back(string_printf(" %04zX a3 %08" PRIX32, l->offset + offsetof(PlayerVisualConfig, unknown_a3), visual.unknown_a3.load()));
|
||||
lines.emplace_back(string_printf(" %04zX name_color_checksum %08" PRIX32, l->offset + offsetof(PlayerVisualConfig, name_color_checksum), visual.name_color_checksum.load()));
|
||||
string secid_name = name_for_section_id(visual.section_id);
|
||||
lines.emplace_back(string_printf(" %04zX section_id %02hhX (%s)", l->offset + offsetof(PlayerVisualConfig, section_id), visual.section_id, secid_name.c_str()));
|
||||
lines.emplace_back(string_printf(" %04zX char_class %02hhX (%s)", l->offset + offsetof(PlayerVisualConfig, char_class), visual.char_class, name_for_char_class(visual.char_class)));
|
||||
lines.emplace_back(string_printf(" %04zX v2_flags %02hhX", l->offset + offsetof(PlayerVisualConfig, v2_flags), visual.v2_flags));
|
||||
lines.emplace_back(string_printf(" %04zX validation_flags %02hhX", l->offset + offsetof(PlayerVisualConfig, validation_flags), visual.validation_flags));
|
||||
lines.emplace_back(string_printf(" %04zX version %02hhX", l->offset + offsetof(PlayerVisualConfig, version), visual.version));
|
||||
lines.emplace_back(string_printf(" %04zX class_flags %08" PRIX32, l->offset + offsetof(PlayerVisualConfig, class_flags), visual.class_flags.load()));
|
||||
lines.emplace_back(string_printf(" %04zX costume %04hX", l->offset + offsetof(PlayerVisualConfig, costume), visual.costume.load()));
|
||||
|
||||
@@ -281,12 +281,62 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
|
||||
mask_data, mask_size, sizeof(S_JoinGame_GC_Ep3_64));
|
||||
mask.variations.clear(0);
|
||||
mask.rare_seed = 0;
|
||||
for (size_t offset = sizeof(S_JoinGame_GC_64) +
|
||||
offsetof(S_JoinGame_GC_Ep3_64::Ep3PlayerEntry, disp.visual.name_color_checksum);
|
||||
offset + 4 <= mask_size;
|
||||
offset += sizeof(S_JoinGame_GC_Ep3_64::Ep3PlayerEntry)) {
|
||||
*reinterpret_cast<uint32_t*>(reinterpret_cast<char*>(mask_data) + offset) = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 0xEB:
|
||||
if (version != GameVersion::GC) {
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
case 0x65:
|
||||
case 0x67:
|
||||
case 0x68:
|
||||
if ((version == GameVersion::DC) || (version == GameVersion::GC)) {
|
||||
for (size_t offset = offsetof(S_JoinLobby_DC_GC_65_67_68_Ep3_EB, entries) +
|
||||
offsetof(S_JoinLobby_DC_GC_65_67_68_Ep3_EB::Entry, disp.visual.name_color_checksum);
|
||||
offset + 4 <= mask_size;
|
||||
offset += sizeof(S_JoinLobby_DC_GC_65_67_68_Ep3_EB::Entry)) {
|
||||
*reinterpret_cast<uint32_t*>(reinterpret_cast<char*>(mask_data) + offset) = 0;
|
||||
}
|
||||
} else if (version == GameVersion::XB) {
|
||||
for (size_t offset = offsetof(S_JoinLobby_XB_65_67_68, entries) +
|
||||
offsetof(S_JoinLobby_XB_65_67_68::Entry, disp.visual.name_color_checksum);
|
||||
offset + 4 <= mask_size;
|
||||
offset += sizeof(S_JoinLobby_XB_65_67_68::Entry)) {
|
||||
*reinterpret_cast<uint32_t*>(reinterpret_cast<char*>(mask_data) + offset) = 0;
|
||||
}
|
||||
} else if (version == GameVersion::PC) {
|
||||
for (size_t offset = offsetof(S_JoinLobby_PC_65_67_68, entries) +
|
||||
offsetof(S_JoinLobby_PC_65_67_68::Entry, disp.visual.name_color_checksum);
|
||||
offset + 4 <= mask_size;
|
||||
offset += sizeof(S_JoinLobby_PC_65_67_68::Entry)) {
|
||||
*reinterpret_cast<uint32_t*>(reinterpret_cast<char*>(mask_data) + offset) = 0;
|
||||
}
|
||||
} else if (version == GameVersion::BB) {
|
||||
for (size_t offset = offsetof(S_JoinLobby_BB_65_67_68, entries) +
|
||||
offsetof(S_JoinLobby_BB_65_67_68::Entry, disp.visual.name_color_checksum);
|
||||
offset + 4 <= mask_size;
|
||||
offset += sizeof(S_JoinLobby_BB_65_67_68::Entry)) {
|
||||
*reinterpret_cast<uint32_t*>(reinterpret_cast<char*>(mask_data) + offset) = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 0xE8:
|
||||
if (version == GameVersion::GC) {
|
||||
auto& mask = check_size_t<S_JoinSpectatorTeam_GC_Ep3_E8>(mask_data, mask_size);
|
||||
mask.rare_seed = 0;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
mask.players[z].disp.visual.name_color_checksum = 0;
|
||||
}
|
||||
for (size_t z = 0; z < 8; z++) {
|
||||
mask.spectator_players[z].disp.visual.name_color_checksum = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 0xB1:
|
||||
|
||||
@@ -618,6 +618,7 @@ void send_complete_player_bb(shared_ptr<Client> c) {
|
||||
SC_SyncCharacterSaveFile_BB_00E7 cmd;
|
||||
cmd.inventory = player->inventory;
|
||||
cmd.disp = player->disp;
|
||||
cmd.disp.visual.compute_name_color_checksum();
|
||||
cmd.disp.play_time = 0;
|
||||
cmd.unknown_a1 = 0;
|
||||
cmd.creation_timestamp = 0;
|
||||
@@ -1451,6 +1452,7 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
|
||||
p.inventory.encode_mags(c->version());
|
||||
p.disp = wc_p->disp.to_dcpcv3();
|
||||
remove_language_marker_inplace(p.disp.visual.name);
|
||||
p.disp.enforce_lobby_join_limits(c->version());
|
||||
|
||||
auto& e = cmd.entries[z];
|
||||
e.player_tag = 0x00010000;
|
||||
@@ -1491,6 +1493,7 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
|
||||
p.inventory.encode_mags(c->version());
|
||||
p.disp = entry.disp;
|
||||
remove_language_marker_inplace(p.disp.visual.name);
|
||||
p.disp.enforce_lobby_join_limits(c->version());
|
||||
|
||||
auto& e = cmd.entries[client_id];
|
||||
e.player_tag = 0x00010000;
|
||||
@@ -1522,6 +1525,7 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
|
||||
cmd_p.inventory = other_p->inventory;
|
||||
cmd_p.disp = other_p->disp.to_dcpcv3();
|
||||
remove_language_marker_inplace(cmd_p.disp.visual.name);
|
||||
cmd_p.disp.enforce_lobby_join_limits(c->version());
|
||||
|
||||
cmd_e.player_tag = 0x00010000;
|
||||
cmd_e.guild_card_number = other_c->license->serial_number;
|
||||
@@ -1628,6 +1632,7 @@ void send_join_game(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
cmd.players_ep3[x].inventory = other_p->inventory;
|
||||
cmd.players_ep3[x].inventory.encode_mags(c->version());
|
||||
cmd.players_ep3[x].disp = convert_player_disp_data<PlayerDispDataDCPCV3>(other_p->disp);
|
||||
cmd.players_ep3[x].disp.enforce_lobby_join_limits(c->version());
|
||||
}
|
||||
}
|
||||
send_command_t(c, 0x64, player_count, cmd);
|
||||
|
||||
@@ -793,3 +793,12 @@ const char* name_for_area(Episode episode, uint8_t area) {
|
||||
throw logic_error("invalid episode for drop area");
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t class_flags_for_class(uint8_t char_class) {
|
||||
static constexpr uint8_t flags[12] = {
|
||||
0x25, 0x2A, 0x31, 0x45, 0x51, 0x52, 0x86, 0x89, 0x8A, 0x32, 0x85, 0x46};
|
||||
if (char_class >= 12) {
|
||||
throw runtime_error("invalid character class");
|
||||
}
|
||||
return flags[char_class];
|
||||
}
|
||||
|
||||
@@ -84,3 +84,5 @@ extern const std::unordered_map<std::string, uint8_t> mag_color_for_name;
|
||||
size_t area_limit_for_episode(Episode ep);
|
||||
uint8_t area_for_name(const std::string& name);
|
||||
const char* name_for_area(Episode episode, uint8_t area);
|
||||
|
||||
uint32_t class_flags_for_class(uint8_t char_class);
|
||||
|
||||
Reference in New Issue
Block a user