rewrite map loader
This commit is contained in:
+18
-17
@@ -11,27 +11,26 @@ using namespace std;
|
||||
|
||||
FileContentsCache::FileContentsCache(uint64_t ttl_usecs) : ttl_usecs(ttl_usecs) { }
|
||||
|
||||
FileContentsCache::File::File(const string& name, shared_ptr<const string> contents,
|
||||
uint64_t load_time) : name(name), contents(contents), load_time(load_time) { }
|
||||
FileContentsCache::File::File(
|
||||
const string& name,
|
||||
string&& data,
|
||||
uint64_t load_time)
|
||||
: name(name), data(move(data)), load_time(load_time) { }
|
||||
|
||||
shared_ptr<const string> FileContentsCache::replace(
|
||||
shared_ptr<const FileContentsCache::File> FileContentsCache::replace(
|
||||
const string& name, string&& data, uint64_t t) {
|
||||
if (t == 0) {
|
||||
t = now();
|
||||
}
|
||||
shared_ptr<const string> contents(new string(move(data)));
|
||||
auto emplace_ret = this->name_to_file.emplace(
|
||||
piecewise_construct,
|
||||
forward_as_tuple(name),
|
||||
forward_as_tuple(name, contents, t));
|
||||
shared_ptr<File> new_file(new File(name, move(data), t));
|
||||
auto emplace_ret = this->name_to_file.emplace(name, new_file);
|
||||
if (!emplace_ret.second) {
|
||||
emplace_ret.first->second.contents = contents;
|
||||
emplace_ret.first->second.load_time = t;
|
||||
emplace_ret.first->second = new_file;
|
||||
}
|
||||
return contents;
|
||||
return new_file;
|
||||
}
|
||||
|
||||
shared_ptr<const string> FileContentsCache::replace(
|
||||
shared_ptr<const FileContentsCache::File> FileContentsCache::replace(
|
||||
const string& name, const void* data, size_t size, uint64_t t) {
|
||||
string s(reinterpret_cast<const char*>(data), size);
|
||||
return this->replace(name, move(s), t);
|
||||
@@ -45,14 +44,16 @@ FileContentsCache::GetResult FileContentsCache::get_or_load(const char* name) {
|
||||
return this->get_or_load(string(name));
|
||||
}
|
||||
|
||||
shared_ptr<const string> FileContentsCache::get_or_throw(const std::string& name) {
|
||||
shared_ptr<const FileContentsCache::File> FileContentsCache::get_or_throw(
|
||||
const std::string& name) {
|
||||
auto throw_fn = +[](const std::string&) -> string {
|
||||
throw out_of_range("file missing from cache");
|
||||
};
|
||||
return this->get(name, throw_fn).data;
|
||||
return this->get(name, throw_fn).file;
|
||||
}
|
||||
|
||||
shared_ptr<const string> FileContentsCache::get_or_throw(const char* name) {
|
||||
shared_ptr<const FileContentsCache::File> FileContentsCache::get_or_throw(
|
||||
const char* name) {
|
||||
return this->get_or_throw(string(name));
|
||||
}
|
||||
|
||||
@@ -61,8 +62,8 @@ FileContentsCache::GetResult FileContentsCache::get(const std::string& name,
|
||||
uint64_t t = now();
|
||||
try {
|
||||
auto& entry = this->name_to_file.at(name);
|
||||
if (this->ttl_usecs && (t - entry.load_time < this->ttl_usecs)) {
|
||||
return {entry.contents, false};
|
||||
if (this->ttl_usecs && (t - entry->load_time < this->ttl_usecs)) {
|
||||
return {entry, false};
|
||||
}
|
||||
} catch (const out_of_range& e) { }
|
||||
return {this->replace(name, generate(name)), true};
|
||||
|
||||
+19
-21
@@ -11,15 +11,14 @@ using namespace std;
|
||||
|
||||
|
||||
class FileContentsCache {
|
||||
private:
|
||||
public:
|
||||
struct File {
|
||||
std::string name;
|
||||
std::shared_ptr<const std::string> contents;
|
||||
std::string data;
|
||||
uint64_t load_time;
|
||||
|
||||
File() = delete;
|
||||
File(const std::string& name, std::shared_ptr<const std::string> contents,
|
||||
uint64_t load_time);
|
||||
File(const std::string& name, std::string&& contents, uint64_t load_time);
|
||||
File(const File&) = delete;
|
||||
File(File&&) = delete;
|
||||
File& operator=(const File&) = delete;
|
||||
@@ -27,7 +26,6 @@ private:
|
||||
~File() = default;
|
||||
};
|
||||
|
||||
public:
|
||||
explicit FileContentsCache(uint64_t ttl_usecs);
|
||||
FileContentsCache(const FileContentsCache&) = delete;
|
||||
FileContentsCache(FileContentsCache&&) = delete;
|
||||
@@ -40,20 +38,20 @@ public:
|
||||
return this->name_to_file.erase(key);
|
||||
}
|
||||
|
||||
std::shared_ptr<const std::string> replace(
|
||||
std::shared_ptr<const File> replace(
|
||||
const std::string& name, std::string&& data, uint64_t t = 0);
|
||||
std::shared_ptr<const std::string> replace(
|
||||
std::shared_ptr<const File> replace(
|
||||
const std::string& name, const void* data, size_t size, uint64_t t = 0);
|
||||
|
||||
struct GetResult {
|
||||
std::shared_ptr<const std::string> data;
|
||||
std::shared_ptr<const File> file;
|
||||
bool generate_called;
|
||||
};
|
||||
|
||||
GetResult get_or_load(const std::string& name);
|
||||
GetResult get_or_load(const char* name);
|
||||
std::shared_ptr<const string> get_or_throw(const std::string& name);
|
||||
std::shared_ptr<const string> get_or_throw(const char* name);
|
||||
std::shared_ptr<const File> get_or_throw(const std::string& name);
|
||||
std::shared_ptr<const File> get_or_throw(const char* name);
|
||||
|
||||
GetResult get(
|
||||
const std::string& name, std::function<std::string(const std::string&)> generate);
|
||||
@@ -63,36 +61,36 @@ public:
|
||||
template <typename T>
|
||||
struct GetObjResult {
|
||||
const T& obj;
|
||||
std::shared_ptr<const std::string> data;
|
||||
std::shared_ptr<const File> data;
|
||||
bool generate_called;
|
||||
};
|
||||
|
||||
template <typename T, typename NameT>
|
||||
GetObjResult<T> get_obj_or_load(NameT name) {
|
||||
auto res = this->get_or_load(name);
|
||||
if (res.data->size() != sizeof(T)) {
|
||||
if (res.file->data.size() != sizeof(T)) {
|
||||
throw runtime_error("cached string size is incorrect");
|
||||
}
|
||||
return {*reinterpret_cast<const T*>(res.data->data()), res.data, res.generate_called};
|
||||
return {*reinterpret_cast<const T*>(res.file->data.data()), res.file, res.generate_called};
|
||||
}
|
||||
template <typename T, typename NameT>
|
||||
GetObjResult<T> get_obj_or_throw(NameT name) {
|
||||
auto res = this->get_or_throw(name);
|
||||
if (res->size() != sizeof(T)) {
|
||||
if (res.file->data.size() != sizeof(T)) {
|
||||
throw runtime_error("cached string size is incorrect");
|
||||
}
|
||||
return {*reinterpret_cast<const T*>(res.data->data()), res.data, res.generate_called};
|
||||
return {*reinterpret_cast<const T*>(res.file->data.data()), res.file, res.generate_called};
|
||||
}
|
||||
template <typename T, typename NameT>
|
||||
GetObjResult<T> get_obj(NameT name, std::function<T(const std::string&)> generate) {
|
||||
uint64_t t = now();
|
||||
try {
|
||||
auto& entry = this->name_to_file.at(name);
|
||||
if (entry.contents->size() != sizeof(T)) {
|
||||
auto& f = this->name_to_file.at(name);
|
||||
if (f->data.size() != sizeof(T)) {
|
||||
throw runtime_error("cached string size is incorrect");
|
||||
}
|
||||
if (this->ttl_usecs && (t - entry.load_time < this->ttl_usecs)) {
|
||||
return {*reinterpret_cast<const T*>(entry.contents->data()), entry.contents, false};
|
||||
if (this->ttl_usecs && (t - f->load_time < this->ttl_usecs)) {
|
||||
return {*reinterpret_cast<const T*>(f->data.data()), f, false};
|
||||
}
|
||||
} catch (const out_of_range& e) { }
|
||||
T value = generate(name);
|
||||
@@ -103,10 +101,10 @@ public:
|
||||
template <typename T, typename NameT>
|
||||
GetObjResult<T> replace_obj(NameT name, const T& value) {
|
||||
auto cached_value = this->replace(name, &value, sizeof(value));
|
||||
return {*reinterpret_cast<const T*>(cached_value->data()), cached_value, false};
|
||||
return {*reinterpret_cast<const T*>(cached_value->data.data()), cached_value, false};
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, File> name_to_file;
|
||||
std::unordered_map<std::string, std::shared_ptr<File>> name_to_file;
|
||||
uint64_t ttl_usecs;
|
||||
};
|
||||
|
||||
@@ -32,9 +32,9 @@ string CompiledFunctionCode::generate_client_command(
|
||||
const string& suffix) const {
|
||||
S_ExecuteCode_Footer_GC_B2 footer;
|
||||
footer.num_relocations = this->relocation_deltas.size();
|
||||
footer.unused1.clear();
|
||||
footer.unused1.clear(0);
|
||||
footer.entrypoint_addr_offset = this->entrypoint_offset_offset;
|
||||
footer.unused2.clear();
|
||||
footer.unused2.clear(0);
|
||||
|
||||
StringWriter w;
|
||||
if (!label_writes.empty()) {
|
||||
|
||||
+1
-1
@@ -475,7 +475,7 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
|
||||
config_log.info("Loading battle parameters");
|
||||
state->battle_params.reset(new BattleParamTable("system/blueburst/BattleParamEntry"));
|
||||
state->battle_params.reset(new BattleParamsIndex("system/blueburst/BattleParamEntry"));
|
||||
|
||||
config_log.info("Loading level table");
|
||||
state->level_table.reset(new LevelTable("system/blueburst/PlyLevelTbl.prs", true));
|
||||
|
||||
+48
-38
@@ -10,27 +10,35 @@ using namespace std;
|
||||
|
||||
|
||||
|
||||
static void load_battle_param_file(const string& filename, BattleParams* entries) {
|
||||
scoped_fd fd(filename, O_RDONLY);
|
||||
readx(fd, entries, 0x60 * sizeof(BattleParams));
|
||||
BattleParamsIndex::BattleParamsIndex(const char* prefix) {
|
||||
for (uint8_t is_solo = 0; is_solo < 2; is_solo++) {
|
||||
for (uint8_t episode = 0; episode < 3; episode++) {
|
||||
string filename = prefix;
|
||||
if (episode == 1) {
|
||||
filename += "_lab";
|
||||
} else if (episode == 2) {
|
||||
filename += "_ep4";
|
||||
}
|
||||
if (!is_solo) {
|
||||
filename += "_on";
|
||||
}
|
||||
filename += ".dat";
|
||||
|
||||
this->entries[is_solo][episode][0].reset(new TableT());
|
||||
this->entries[is_solo][episode][1].reset(new TableT());
|
||||
this->entries[is_solo][episode][2].reset(new TableT());
|
||||
this->entries[is_solo][episode][3].reset(new TableT());
|
||||
|
||||
scoped_fd fd(filename, O_RDONLY);
|
||||
readx(fd, this->entries[is_solo][episode][0].get(), sizeof(TableT));
|
||||
readx(fd, this->entries[is_solo][episode][1].get(), sizeof(TableT));
|
||||
readx(fd, this->entries[is_solo][episode][2].get(), sizeof(TableT));
|
||||
readx(fd, this->entries[is_solo][episode][3].get(), sizeof(TableT));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
const BattleParams& BattleParamsIndex::get(bool solo, uint8_t episode,
|
||||
uint8_t difficulty, uint8_t monster_type) const {
|
||||
if (episode > 3) {
|
||||
throw invalid_argument("incorrect episode");
|
||||
@@ -41,18 +49,19 @@ const BattleParams& BattleParamTable::get(bool solo, uint8_t episode,
|
||||
if (monster_type > 0x60) {
|
||||
throw invalid_argument("incorrect monster type");
|
||||
}
|
||||
return this->entries[!!solo][episode][difficulty][monster_type];
|
||||
return (*this->entries[!!solo][episode][difficulty])[monster_type];
|
||||
}
|
||||
|
||||
const BattleParams* BattleParamTable::get_subtable(bool solo, uint8_t episode,
|
||||
uint8_t difficulty) const {
|
||||
shared_ptr<const BattleParamsIndex::TableT>
|
||||
BattleParamsIndex::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];
|
||||
return this->entries[!!solo][episode][difficulty];
|
||||
}
|
||||
|
||||
|
||||
@@ -92,9 +101,19 @@ struct EnemyEntry {
|
||||
|
||||
static uint64_t next_enemy_id = 1;
|
||||
|
||||
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> parse_map(
|
||||
uint8_t episode,
|
||||
uint8_t difficulty,
|
||||
shared_ptr<const BattleParamsIndex::TableT> battle_params_table,
|
||||
const void* data,
|
||||
size_t size,
|
||||
bool alt_enemies) {
|
||||
|
||||
const auto* map = reinterpret_cast<const EnemyEntry*>(data);
|
||||
size_t entry_count = size / sizeof(EnemyEntry);
|
||||
if (size != entry_count * sizeof(EnemyEntry)) {
|
||||
throw runtime_error("data size is not a multiple of entry size");
|
||||
}
|
||||
|
||||
vector<PSOEnemy> enemies;
|
||||
auto create_clones = [&](size_t count) {
|
||||
@@ -103,6 +122,7 @@ static vector<PSOEnemy> parse_map(uint8_t episode, uint8_t difficulty,
|
||||
}
|
||||
};
|
||||
|
||||
const auto& battle_params = *battle_params_table;
|
||||
for (size_t y = 0; y < entry_count; y++) {
|
||||
const auto& e = map[y];
|
||||
size_t num_clones = e.num_clones;
|
||||
@@ -254,7 +274,7 @@ static vector<PSOEnemy> parse_map(uint8_t episode, uint8_t difficulty,
|
||||
case 0xC0: // Dragon or Gal Gryphon
|
||||
if (episode == 1) {
|
||||
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x12].experience, 44);
|
||||
} else if (episode == 0x02) {
|
||||
} else if (episode == 2) {
|
||||
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x1E].experience, 77);
|
||||
}
|
||||
break;
|
||||
@@ -343,7 +363,7 @@ static vector<PSOEnemy> parse_map(uint8_t episode, uint8_t difficulty,
|
||||
}
|
||||
break;
|
||||
case 0xE0: // Epsilon, Sinow Zoa and Zele
|
||||
if ((episode == 0x02) && (alt_enemies)) {
|
||||
if ((episode == 2) && (alt_enemies)) {
|
||||
enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x23].experience, 84);
|
||||
create_clones(4);
|
||||
} else {
|
||||
@@ -413,13 +433,3 @@ static vector<PSOEnemy> parse_map(uint8_t episode, uint8_t difficulty,
|
||||
|
||||
return enemies;
|
||||
}
|
||||
|
||||
vector<PSOEnemy> load_map(const std::string& filename, uint8_t episode,
|
||||
uint8_t difficulty, const BattleParams* battle_params, bool alt_enemies) {
|
||||
static FileContentsCache map_file_cache(300 * 1000 * 1000);
|
||||
shared_ptr<const string> data = map_file_cache.get_or_load(filename).data;
|
||||
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);
|
||||
}
|
||||
|
||||
+20
-13
@@ -6,6 +6,8 @@
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include "Text.hh"
|
||||
|
||||
|
||||
|
||||
struct BattleParams {
|
||||
@@ -21,24 +23,24 @@ struct BattleParams {
|
||||
le_uint32_t difficulty;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct BattleParamTable {
|
||||
BattleParams entries[2][3][4][0x60]; // online/offline, episode, difficulty, monster type
|
||||
class BattleParamsIndex {
|
||||
public:
|
||||
using TableT = parray<BattleParams, 0x60>;
|
||||
|
||||
BattleParamTable(const char* filename_prefix);
|
||||
BattleParamsIndex(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;
|
||||
} __attribute__((packed));
|
||||
std::shared_ptr<const TableT> get_subtable(
|
||||
bool solo, uint8_t episode, uint8_t difficulty) const;
|
||||
|
||||
private:
|
||||
// online/offline, episode, difficulty
|
||||
std::shared_ptr<TableT> entries[2][3][4];
|
||||
};
|
||||
|
||||
|
||||
|
||||
struct BattleParamIndex {
|
||||
BattleParamTable table_for_episode[3];
|
||||
} __attribute__((packed));
|
||||
|
||||
// an enemy entry as loaded by the game
|
||||
struct PSOEnemy {
|
||||
uint64_t id;
|
||||
uint16_t source_type;
|
||||
@@ -53,5 +55,10 @@ struct PSOEnemy {
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
std::vector<PSOEnemy> load_map(const std::string& filename, uint8_t episode,
|
||||
uint8_t difficulty, const BattleParams* bp, bool alt_enemies);
|
||||
std::vector<PSOEnemy> parse_map(
|
||||
uint8_t episode,
|
||||
uint8_t difficulty,
|
||||
std::shared_ptr<const BattleParamsIndex::TableT> battle_params,
|
||||
const void* data,
|
||||
size_t size,
|
||||
bool alt_enemies);
|
||||
|
||||
+9
-9
@@ -285,9 +285,9 @@ GuildCardBB::GuildCardBB() noexcept
|
||||
|
||||
void GuildCardBB::clear() {
|
||||
this->guild_card_number = 0;
|
||||
this->name.clear();
|
||||
this->team_name.clear();
|
||||
this->description.clear();
|
||||
this->name.clear(0);
|
||||
this->team_name.clear(0);
|
||||
this->description.clear(0);
|
||||
this->present = 0;
|
||||
this->language = 0;
|
||||
this->section_id = 0;
|
||||
@@ -296,7 +296,7 @@ void GuildCardBB::clear() {
|
||||
|
||||
void GuildCardEntryBB::clear() {
|
||||
this->data.clear();
|
||||
this->unknown_a1.clear();
|
||||
this->unknown_a1.clear(0);
|
||||
}
|
||||
|
||||
uint32_t GuildCardFileBB::checksum() const {
|
||||
@@ -495,7 +495,7 @@ void ClientGameData::import_player(const PSOPlayerDataV3& gc) {
|
||||
if (gc.auto_reply_enabled) {
|
||||
player->auto_reply = gc.auto_reply;
|
||||
} else {
|
||||
player->auto_reply.clear();
|
||||
player->auto_reply.clear(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,7 +509,7 @@ void ClientGameData::import_player(const PSOPlayerDataBB& bb) {
|
||||
if (bb.auto_reply_enabled) {
|
||||
player->auto_reply = bb.auto_reply;
|
||||
} else {
|
||||
player->auto_reply.clear();
|
||||
player->auto_reply.clear(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,7 +520,7 @@ PlayerBB ClientGameData::export_player_bb() {
|
||||
PlayerBB ret;
|
||||
ret.inventory = player->inventory;
|
||||
ret.disp = player->disp;
|
||||
ret.unknown.clear();
|
||||
ret.unknown.clear(0);
|
||||
ret.option_flags = account->option_flags;
|
||||
ret.quest_data1 = player->quest_data1;
|
||||
ret.bank = player->bank;
|
||||
@@ -537,10 +537,10 @@ PlayerBB ClientGameData::export_player_bb() {
|
||||
ret.shortcuts = account->shortcuts;
|
||||
ret.auto_reply = player->auto_reply;
|
||||
ret.info_board = player->info_board;
|
||||
ret.unknown5.clear();
|
||||
ret.unknown5.clear(0);
|
||||
ret.challenge_data = player->challenge_data;
|
||||
ret.tech_menu_config = player->tech_menu_config;
|
||||
ret.unknown6.clear();
|
||||
ret.unknown6.clear(0);
|
||||
ret.quest_data2 = player->quest_data2;
|
||||
ret.key_config = account->key_config;
|
||||
return ret;
|
||||
|
||||
+35
-58
@@ -1424,9 +1424,9 @@ void process_gba_file_request(shared_ptr<ServerState>, shared_ptr<Client> c,
|
||||
strip_trailing_zeroes(filename);
|
||||
|
||||
static FileContentsCache gba_file_cache(300 * 1000 * 1000);
|
||||
auto contents = gba_file_cache.get_or_load("system/gba/" + filename).data;
|
||||
auto f = gba_file_cache.get_or_load("system/gba/" + filename).file;
|
||||
|
||||
send_quest_file(c, "", filename, *contents, QuestFileType::GBA_DEMO);
|
||||
send_quest_file(c, "", filename, f->data, QuestFileType::GBA_DEMO);
|
||||
}
|
||||
|
||||
|
||||
@@ -1656,7 +1656,7 @@ void process_client_checksum_bb(shared_ptr<ServerState>, shared_ptr<Client> c,
|
||||
for (size_t z = 0; z < max_count; z++) {
|
||||
if (!gcf.entries[z].data.present) {
|
||||
gcf.entries[z].data = new_gc;
|
||||
gcf.entries[z].unknown_a1.clear();
|
||||
gcf.entries[z].unknown_a1.clear(0);
|
||||
c->log.info("Added guild card %" PRIu32 " at position %zu",
|
||||
new_gc.guild_card_number.load(), z);
|
||||
break;
|
||||
@@ -1997,7 +1997,7 @@ void process_set_auto_reply_t(shared_ptr<ServerState>, shared_ptr<Client> c,
|
||||
void process_disable_auto_reply(shared_ptr<ServerState>, shared_ptr<Client> c,
|
||||
uint16_t, uint32_t, const string& data) { // C8
|
||||
check_size_v(data.size(), 0);
|
||||
c->game_data.player()->auto_reply.clear();
|
||||
c->game_data.player()->auto_reply.clear(0);
|
||||
}
|
||||
|
||||
void process_set_blocked_senders_list(shared_ptr<ServerState>, shared_ptr<Client> c,
|
||||
@@ -2021,22 +2021,6 @@ shared_ptr<Lobby> create_game_generic(shared_ptr<ServerState> s,
|
||||
const std::u16string& password, uint8_t episode, uint8_t difficulty,
|
||||
uint8_t battle, uint8_t challenge, uint8_t solo) {
|
||||
|
||||
static const uint32_t variation_maxes_online[3][0x20] = {
|
||||
{1, 1, 1, 5, 1, 5, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2,
|
||||
3, 2, 3, 2, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
|
||||
{1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 3, 1, 3, 1, 3,
|
||||
2, 2, 1, 3, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1},
|
||||
{1, 1, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 1, 1, 3,
|
||||
3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}};
|
||||
|
||||
static const uint32_t variation_maxes_solo[3][0x20] = {
|
||||
{1, 1, 1, 3, 1, 3, 3, 1, 3, 1, 3, 1, 3, 2, 3, 2,
|
||||
3, 2, 3, 2, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
|
||||
{1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 3, 1, 3, 1, 3,
|
||||
2, 2, 1, 3, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1},
|
||||
{1, 1, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 1, 1, 3,
|
||||
3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}};
|
||||
|
||||
// A player's actual level is their displayed level - 1, so the minimums for
|
||||
// Episode 1 (for example) are actually 1, 20, 40, 80.
|
||||
static const uint32_t default_minimum_levels[3][4] = {
|
||||
@@ -2102,7 +2086,6 @@ shared_ptr<Lobby> create_game_generic(shared_ptr<ServerState> s,
|
||||
game->min_level = min_level;
|
||||
game->max_level = 0xFFFFFFFF;
|
||||
|
||||
const uint32_t* variation_maxes = nullptr;
|
||||
if (game->version == GameVersion::BB) {
|
||||
// TODO: cache these somewhere so we don't read the file every time, lolz
|
||||
game->rare_item_set.reset(new RareItemSet("system/blueburst/ItemRT.rel",
|
||||
@@ -2113,34 +2096,38 @@ shared_ptr<Lobby> create_game_generic(shared_ptr<ServerState> s,
|
||||
}
|
||||
game->next_game_item_id = 0x00810000;
|
||||
|
||||
const auto* bp_subtable = s->battle_params->get_subtable(game->mode == 3,
|
||||
auto bp_subtable = s->battle_params->get_subtable(game->mode == 3,
|
||||
game->episode - 1, game->difficulty);
|
||||
|
||||
const char* type_chars = (game->mode == 3) ? "sm" : "m";
|
||||
if (episode > 0 && episode < 4) {
|
||||
variation_maxes = (game->mode == 3) ? variation_maxes_solo[episode - 1] : variation_maxes_online[episode - 1];
|
||||
}
|
||||
generate_variations(
|
||||
game->variations, game->random, game->episode, game->mode == 3);
|
||||
|
||||
for (size_t x = 0; x < 0x10; x++) {
|
||||
for (const char* type = type_chars; *type; type++) {
|
||||
auto filename = string_printf(
|
||||
"system/blueburst/map/%c%hhX%zX%" PRIX32 "%" PRIX32 ".dat",
|
||||
*type, game->episode, x,
|
||||
game->variations.data()[x * 2].load(),
|
||||
game->variations.data()[(x * 2) + 1].load());
|
||||
try {
|
||||
auto enemies = load_map(filename.c_str(), game->episode,
|
||||
game->difficulty, bp_subtable, false);
|
||||
game->enemies.insert(game->enemies.end(), enemies.begin(), enemies.end());
|
||||
c->log.info("Loaded map %s (%zu entries)", filename.c_str(), enemies.size());
|
||||
for (size_t z = 0; z < enemies.size(); z++) {
|
||||
string e_str = enemies[z].str();
|
||||
static_game_data_log.info("(Entry %zu) %s", z, e_str.c_str());
|
||||
}
|
||||
break;
|
||||
} catch (const exception& e) {
|
||||
c->log.warning("Failed to load map %s: %s", filename.c_str(), e.what());
|
||||
try {
|
||||
auto file = map_data_for_variation(
|
||||
game->episode,
|
||||
game->mode == 3,
|
||||
x,
|
||||
game->variations[x * 2 + 0],
|
||||
game->variations[x * 2 + 1]);
|
||||
auto area_enemies = parse_map(
|
||||
game->episode,
|
||||
game->difficulty,
|
||||
bp_subtable,
|
||||
file->data.data(),
|
||||
file->data.size(),
|
||||
false);
|
||||
game->enemies.insert(
|
||||
game->enemies.end(),
|
||||
area_enemies.begin(),
|
||||
area_enemies.end());
|
||||
c->log.info("Loaded map for area %zu (%zu entries)", x, area_enemies.size());
|
||||
for (size_t z = 0; z < area_enemies.size(); z++) {
|
||||
string e_str = area_enemies[z].str();
|
||||
static_game_data_log.info("(Entry %zu) %s", z, e_str.c_str());
|
||||
}
|
||||
} catch (const exception& e) {
|
||||
c->log.warning("Failed to load map for area %zu: %s", x, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2149,22 +2136,12 @@ shared_ptr<Lobby> create_game_generic(shared_ptr<ServerState> s,
|
||||
}
|
||||
c->log.info("Loaded maps contain %zu entries overall", game->enemies.size());
|
||||
|
||||
} else {
|
||||
// In non-BB games, just set the variations (we don't track items/enemies/
|
||||
// etc.)
|
||||
if (episode > 0 && episode < 4 && !is_ep3) {
|
||||
variation_maxes = variation_maxes_online[episode - 1];
|
||||
}
|
||||
}
|
||||
} else if (is_ep3) {
|
||||
game->variations.clear(0);
|
||||
|
||||
if (variation_maxes) {
|
||||
for (size_t x = 0; x < 0x20; x++) {
|
||||
game->variations.data()[x] = (*game->random)() % variation_maxes[x];
|
||||
}
|
||||
} else {
|
||||
for (size_t x = 0; x < 0x20; x++) {
|
||||
game->variations.data()[x] = 0;
|
||||
}
|
||||
// In non-BB non-Ep3 games, just set the variations (we don't track enemies)
|
||||
generate_variations(game->variations, game->random, game->episode, false);
|
||||
}
|
||||
|
||||
s->change_client_lobby(c, game);
|
||||
|
||||
+10
-10
@@ -376,16 +376,16 @@ void send_stream_file_index_bb(shared_ptr<Client> c) {
|
||||
string key = "system/blueburst/" + filename;
|
||||
auto cache_res = bb_stream_files_cache.get_or_load(key);
|
||||
auto& e = entries.emplace_back();
|
||||
e.size = cache_res.data->size();
|
||||
e.size = cache_res.file->data.size();
|
||||
// Computing the checksum can be slow, so we cache it along with the file
|
||||
// data. If the cache result was just populated, then it may be different,
|
||||
// so we always recompute the checksum in that case.
|
||||
if (cache_res.generate_called) {
|
||||
e.checksum = crc32(cache_res.data->data(), e.size);
|
||||
e.checksum = crc32(cache_res.file->data.data(), e.size);
|
||||
bb_stream_files_cache.replace_obj<uint32_t>(key + ".crc32", e.checksum);
|
||||
} else {
|
||||
auto compute_checksum = [&](const string&) -> uint32_t {
|
||||
return crc32(cache_res.data->data(), e.size);
|
||||
return crc32(cache_res.file->data.data(), e.size);
|
||||
};
|
||||
e.checksum = bb_stream_files_cache.get_obj<uint32_t>(key + ".crc32", compute_checksum).obj;
|
||||
}
|
||||
@@ -400,26 +400,26 @@ void send_stream_file_chunk_bb(shared_ptr<Client> c, uint32_t chunk_index) {
|
||||
auto cache_result = bb_stream_files_cache.get("<BB stream file>", +[](const string&) -> string {
|
||||
size_t bytes = 0;
|
||||
for (const auto& name : stream_file_entries) {
|
||||
bytes += bb_stream_files_cache.get_or_load("system/blueburst/" + name).data->size();
|
||||
bytes += bb_stream_files_cache.get_or_load("system/blueburst/" + name).file->data.size();
|
||||
}
|
||||
|
||||
string ret;
|
||||
ret.reserve(bytes);
|
||||
for (const auto& name : stream_file_entries) {
|
||||
ret += *bb_stream_files_cache.get_or_load("system/blueburst/" + name).data;
|
||||
ret += bb_stream_files_cache.get_or_load("system/blueburst/" + name).file->data;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
auto contents = cache_result.data;
|
||||
const auto& contents = cache_result.file->data;
|
||||
|
||||
S_StreamFileChunk_BB_02EB chunk_cmd;
|
||||
chunk_cmd.chunk_index = chunk_index;
|
||||
size_t offset = sizeof(chunk_cmd.data) * chunk_index;
|
||||
if (offset > contents->size()) {
|
||||
if (offset > contents.size()) {
|
||||
throw runtime_error("client requested chunk beyond end of stream file");
|
||||
}
|
||||
size_t bytes = min<size_t>(contents->size() - offset, sizeof(chunk_cmd.data));
|
||||
memcpy(chunk_cmd.data, contents->data() + offset, bytes);
|
||||
size_t bytes = min<size_t>(contents.size() - offset, sizeof(chunk_cmd.data));
|
||||
memcpy(chunk_cmd.data, contents.data() + offset, bytes);
|
||||
|
||||
size_t cmd_size = offsetof(S_StreamFileChunk_BB_02EB, data) + bytes;
|
||||
cmd_size = (cmd_size + 3) & ~3;
|
||||
@@ -1484,7 +1484,7 @@ void send_quest_open_file_t(
|
||||
default:
|
||||
throw logic_error("invalid quest file type");
|
||||
}
|
||||
cmd.unused.clear();
|
||||
cmd.unused.clear(0);
|
||||
cmd.file_size = file_size;
|
||||
cmd.filename = filename.c_str();
|
||||
send_command_t(c, command_num, 0x00, cmd);
|
||||
|
||||
+1
-1
@@ -163,7 +163,7 @@ Proxy commands (these will only work when exactly one client is connected):\n\
|
||||
shared_ptr<LicenseManager> lm(new LicenseManager("system/licenses.nsi"));
|
||||
this->state->license_manager = lm;
|
||||
} else if (type == "battle-params") {
|
||||
shared_ptr<BattleParamTable> bpt(new BattleParamTable("system/blueburst/BattleParamEntry"));
|
||||
shared_ptr<BattleParamsIndex> bpt(new BattleParamsIndex("system/blueburst/BattleParamEntry"));
|
||||
this->state->battle_params = bpt;
|
||||
} else if (type == "level-table") {
|
||||
shared_ptr<LevelTable> lt(new LevelTable("system/blueburst/PlyLevelTbl.prs", true));
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ struct ServerState {
|
||||
std::shared_ptr<const Ep3DataIndex> ep3_data_index;
|
||||
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 BattleParamsIndex> battle_params;
|
||||
std::shared_ptr<const CommonItemData> common_item_data;
|
||||
|
||||
std::shared_ptr<LicenseManager> license_manager;
|
||||
|
||||
@@ -2,10 +2,216 @@
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "FileContentsCache.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
struct AreaMapFileIndex {
|
||||
const char* name_token;
|
||||
std::vector<uint32_t> variation1_values;
|
||||
std::vector<uint32_t> variation2_values;
|
||||
|
||||
AreaMapFileIndex(
|
||||
const char* name_token,
|
||||
std::vector<uint32_t> variation1_values,
|
||||
std::vector<uint32_t> variation2_values)
|
||||
: name_token(name_token),
|
||||
variation1_values(variation1_values),
|
||||
variation2_values(variation2_values) { }
|
||||
};
|
||||
|
||||
// These are indexed as [episode][is_solo][area]
|
||||
// (Note that Lobby::episode is 1-3, so we actually use episode - 1)
|
||||
static const std::vector<std::vector<std::vector<AreaMapFileIndex>>> map_file_info = {
|
||||
{ // Episode 1
|
||||
{ // Non-solo
|
||||
{"city00", {}, {0}},
|
||||
{"forest01", {}, {0, 1, 2, 3, 4}},
|
||||
{"forest02", {}, {0, 1, 2, 3, 4}},
|
||||
{"cave01", {0, 1, 2}, {0, 1}},
|
||||
{"cave02", {0, 1, 2}, {0, 1}},
|
||||
{"cave03", {0, 1, 2}, {0, 1}},
|
||||
{"machine01", {0, 1, 2}, {0, 1}},
|
||||
{"machine02", {0, 1, 2}, {0, 1}},
|
||||
{"ancient01", {0, 1, 2}, {0, 1}},
|
||||
{"ancient02", {0, 1, 2}, {0, 1}},
|
||||
{"ancient03", {0, 1, 2}, {0, 1}},
|
||||
{"boss01", {}, {}},
|
||||
{"boss02", {}, {}},
|
||||
{"boss03", {}, {}},
|
||||
{"boss04", {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
},
|
||||
{ // Solo
|
||||
{"city00", {}, {0}},
|
||||
{"forest01", {}, {0, 2, 4}},
|
||||
{"forest02", {}, {0, 3, 4}},
|
||||
{"cave01", {0, 1, 2}, {0}},
|
||||
{"cave02", {0, 1, 2}, {0}},
|
||||
{"cave03", {0, 1, 2}, {0}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
},
|
||||
},
|
||||
{ // Episode 2
|
||||
{ // Non-solo
|
||||
{"labo00", {}, {0}},
|
||||
{"ruins01", {0, 1}, {0}},
|
||||
{"ruins02", {0, 1}, {0}},
|
||||
{"space01", {0, 1}, {0}},
|
||||
{"space02", {0, 1}, {0}},
|
||||
{"jungle01", {}, {0, 1, 2}},
|
||||
{"jungle02", {}, {0, 1, 2}},
|
||||
{"jungle03", {}, {0, 1, 2}},
|
||||
{"jungle04", {0, 1}, {0, 1}},
|
||||
{"jungle05", {}, {0, 1, 2}},
|
||||
{"seabed01", {0, 1}, {0, 1}},
|
||||
{"seabed02", {0, 1}, {0, 1}},
|
||||
{"boss05", {}, {}},
|
||||
{"boss06", {}, {}},
|
||||
{"boss07", {}, {}},
|
||||
{"boss08", {}, {}},
|
||||
},
|
||||
{ // Solo
|
||||
{"labo00", {}, {0}},
|
||||
{"ruins01", {0, 1}, {0}},
|
||||
{"ruins02", {0, 1}, {0}},
|
||||
{"space01", {0, 1}, {0}},
|
||||
{"space02", {0, 1}, {0}},
|
||||
{"jungle01", {}, {0, 1, 2}},
|
||||
{"jungle02", {}, {0, 1, 2}},
|
||||
{"jungle03", {}, {0, 1, 2}},
|
||||
{"jungle04", {0, 1}, {0, 1}},
|
||||
{"jungle05", {}, {0, 1, 2}},
|
||||
{"seabed01", {0, 1}, {0}},
|
||||
{"seabed02", {0, 1}, {0}},
|
||||
{"boss05", {}, {}},
|
||||
{"boss06", {}, {}},
|
||||
{"boss07", {}, {}},
|
||||
{"boss08", {}, {}},
|
||||
},
|
||||
},
|
||||
{ // Episode 4
|
||||
{ // Non-solo
|
||||
{"city02", {0}, {0}},
|
||||
{"wilds01", {0}, {0, 1, 2}},
|
||||
{"wilds01", {1}, {0, 1, 2}},
|
||||
{"wilds01", {2}, {0, 1, 2}},
|
||||
{"wilds01", {3}, {0, 1, 2}},
|
||||
{"crater01", {0}, {0, 1, 2}},
|
||||
{"desert01", {0, 1, 2}, {0}},
|
||||
{"desert02", {0}, {0, 1, 2}},
|
||||
{"desert03", {0, 1, 2}, {0}},
|
||||
{"boss09", {0}, {0}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
},
|
||||
{ // Solo
|
||||
{"city02", {0}, {0}},
|
||||
{"wilds01", {0}, {0, 1, 2}},
|
||||
{"wilds01", {1}, {0, 1, 2}},
|
||||
{"wilds01", {2}, {0, 1, 2}},
|
||||
{"wilds01", {3}, {0, 1, 2}},
|
||||
{"crater01", {0}, {0, 1, 2}},
|
||||
{"desert01", {0, 1, 2}, {0}},
|
||||
{"desert02", {0}, {0, 1, 2}},
|
||||
{"desert03", {0, 1, 2}, {0}},
|
||||
{"boss09", {0}, {0}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
{nullptr, {}, {}},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
void generate_variations(
|
||||
parray<le_uint32_t, 0x20>& variations,
|
||||
std::shared_ptr<std::mt19937> random,
|
||||
uint8_t episode,
|
||||
bool is_solo) {
|
||||
const auto& ep_index = map_file_info.at(episode - 1);
|
||||
for (size_t z = 0; z < 0x10; z++) {
|
||||
const AreaMapFileIndex* a = nullptr;
|
||||
if (is_solo) {
|
||||
a = &ep_index.at(true).at(z);
|
||||
}
|
||||
if (!a || !a->name_token) {
|
||||
a = &ep_index.at(false).at(z);
|
||||
}
|
||||
if (!a->name_token) {
|
||||
variations[z * 2 + 0] = 0;
|
||||
variations[z * 2 + 1] = 0;
|
||||
} else {
|
||||
variations[z * 2 + 0] = (a->variation1_values.size() < 2) ? 0 :
|
||||
((*random)() % a->variation1_values.size());
|
||||
variations[z * 2 + 1] = (a->variation2_values.size() < 2) ? 0 :
|
||||
((*random)() % a->variation2_values.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<const FileContentsCache::File> map_data_for_variation(
|
||||
uint8_t episode, bool is_solo, uint8_t area, uint32_t var1, uint32_t var2) {
|
||||
static FileContentsCache cache(300 * 1000 * 1000);
|
||||
|
||||
// Map filenames are like map_<name_token>[_VV][_VV][_off]<e|o>[_s].dat
|
||||
// name_token comes from AreaMapFileIndex
|
||||
// _VV are the values from the variation<1|2>_values vector (in contrast to
|
||||
// the values sent in the 64 command, which are INDEXES INTO THAT VECTOR)
|
||||
// _off or _s are used for solo mode (try both - city uses _s whereas levels
|
||||
// use _off apparently)
|
||||
// e|o specifies what kind of data: e = enemies, o = objects
|
||||
const auto& ep_index = map_file_info.at(episode - 1);
|
||||
const AreaMapFileIndex* a = nullptr;
|
||||
if (is_solo) {
|
||||
a = &ep_index.at(true).at(area);
|
||||
}
|
||||
if (!a || !a->name_token) {
|
||||
a = &ep_index.at(false).at(area);
|
||||
}
|
||||
if (!a->name_token) {
|
||||
throw out_of_range("no map data for area");
|
||||
}
|
||||
|
||||
string filename = "system/blueburst/map/map_";
|
||||
filename += a->name_token;
|
||||
if (!a->variation1_values.empty()) {
|
||||
filename += string_printf("_%02" PRIX32, a->variation1_values.at(var1));
|
||||
}
|
||||
if (!a->variation2_values.empty()) {
|
||||
filename += string_printf("_%02" PRIX32, a->variation2_values.at(var2));
|
||||
}
|
||||
if (is_solo) {
|
||||
// Try both _offe.dat and e_s.dat suffixes
|
||||
try {
|
||||
return cache.get_or_load(filename + "_offe.dat").file;
|
||||
} catch (const cannot_open_file&) {
|
||||
return cache.get_or_load(filename + "e_s.dat").file;
|
||||
}
|
||||
} else {
|
||||
return cache.get_or_load(filename + "e.dat").file;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const vector<string> section_id_to_name({
|
||||
"Viridia", "Greennill", "Skyly", "Bluefull", "Purplenum", "Pinkal", "Redria",
|
||||
"Oran", "Yellowboze", "Whitill"});
|
||||
|
||||
@@ -3,11 +3,22 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#include <unordered_map>
|
||||
#include <random>
|
||||
|
||||
#include "FileContentsCache.hh"
|
||||
#include "Player.hh"
|
||||
|
||||
|
||||
|
||||
void generate_variations(
|
||||
parray<le_uint32_t, 0x20>& variations,
|
||||
std::shared_ptr<std::mt19937> random,
|
||||
uint8_t episode,
|
||||
bool is_solo);
|
||||
std::shared_ptr<const FileContentsCache::File> map_data_for_variation(
|
||||
uint8_t episode, bool is_solo, uint8_t area, uint32_t var1, uint32_t var2);
|
||||
void load_map_files();
|
||||
|
||||
size_t stack_size_for_item(uint8_t data0, uint8_t data1);
|
||||
size_t stack_size_for_item(const ItemData& item);
|
||||
|
||||
|
||||
+7
-3
@@ -192,9 +192,13 @@ template <typename ItemT, size_t Count>
|
||||
struct parray {
|
||||
ItemT items[Count];
|
||||
|
||||
template <typename ArgT = ItemT> requires (std::is_arithmetic<ItemT>::value)
|
||||
parray() {
|
||||
this->clear();
|
||||
this->clear(0);
|
||||
}
|
||||
template <typename ArgT = ItemT> requires (!std::is_arithmetic<ItemT>::value)
|
||||
parray() { }
|
||||
|
||||
parray(const parray& other) {
|
||||
this->operator=(other);
|
||||
}
|
||||
@@ -281,7 +285,7 @@ struct parray {
|
||||
return !this->operator==(s);
|
||||
}
|
||||
|
||||
void clear(ItemT v = 0) {
|
||||
void clear(ItemT v) {
|
||||
for (size_t x = 0; x < Count; x++) {
|
||||
this->items[x] = v;
|
||||
}
|
||||
@@ -312,7 +316,7 @@ struct parray {
|
||||
template <typename CharT, size_t Count>
|
||||
struct ptext : parray<CharT, Count> {
|
||||
ptext() {
|
||||
this->clear();
|
||||
this->clear(0);
|
||||
}
|
||||
ptext(const ptext& other) : parray<CharT, Count>(other) { }
|
||||
ptext(ptext&& s) = delete;
|
||||
|
||||
Reference in New Issue
Block a user