Fix fields array not matching the size of the town

The fields array was fixed at 64x64, which is fine for all towns
supported in legacy BoE.

However, we intend to support even larger towns in the future,
and also it seems silly to hold so much extra space for a smaller town.

So now, the fields array is a 2D vector that matches the size of the terrain vector.
The setup array is similarly a list of 2D vectors.

This radically changes the format used to store the setup array in a saved game.
Older saves won't crash the game, but fields will be messed up or missing.
Resetting towns is recommended.
This commit is contained in:
2023-01-21 00:47:35 -05:00
parent c7c8f3fa77
commit a430abbd50
9 changed files with 82 additions and 71 deletions

View File

@@ -347,17 +347,10 @@ bool load_party_v2(fs::path file_to_load, cUniverse& real_univ){
showError("Loading Blades of Exile save file failed.");
return false;
}
uint16_t magic;
fin.read((char*)&magic, 2);
fin.read((char*)&univ.party.setup, sizeof(univ.party.setup));
if(magic == 0x0E0B) { // should be 0x0B0E!
for(auto& i : univ.party.setup) {
for(auto& j : i) {
for(auto& k : j) {
flip_short(reinterpret_cast<int16_t*>(&k));
}
}
}
file.readFrom(fin);
auto& page = file[0];
for(size_t i = 0; i < univ.party.setup.size(); i++) {
page[std::to_string(i)].extract(univ.party.setup[i]);
}
}
@@ -461,9 +454,13 @@ bool save_party(fs::path dest_file, const cUniverse& univ) {
{
std::ostream& fout = partyOut.newFile("save/setup.dat");
static uint16_t magic = 0x0B0E;
fout.write((char*)&magic, 2);
fout.write((char*)&univ.party.setup, sizeof(univ.party.setup));
auto& page = file.add();
page.add("OBOE");
for(size_t i = 0; i < univ.party.setup.size(); i++) {
page[std::to_string(i)].encode(univ.party.setup[i]);
}
file.writeTo(fout);
file.clear();
}
if(univ.party.town_num < 200) {

View File

@@ -274,10 +274,6 @@ void start_outdoor_combat(cOutdoors::cCreature encounter,location where,short nu
// Basically, in outdoor combat, we create kind of a 48x48 town for
// the combat to take place in
for(short i = 0; i < 48; i++)
for(short j = 0; j < 48; j++) {
univ.town.fields[i][j] = 0;
}
univ.town.prep_arena();
univ.town->in_town_rect = town_rect;

View File

@@ -715,8 +715,6 @@ bool combat_move_monster(short which,location destination) {
location find_clear_spot(location from_where,short mode) {
location loc,store_loc;
short num_tries = 0,r1;
// Here 254 indicates the low byte of the town fields, minus explored spaces (which is lowest bit).
unsigned long blocking_fields = SPECIAL_SPOT | OBJECT_CRATE | OBJECT_BARREL | OBJECT_BLOCK | FIELD_QUICKFIRE | 254;
while(num_tries < 75) {
num_tries++;
@@ -729,7 +727,7 @@ location find_clear_spot(location from_where,short mode) {
&& can_see_light(from_where,loc,combat_obscurity) == 0
&& (!is_combat() || univ.target_there(loc,TARG_PC) == nullptr)
&& (!(is_town()) || (loc != univ.party.town_loc))
&& (!(univ.town.fields[loc.x][loc.y] & blocking_fields))) {
&& (!univ.town.is_summon_safe(loc.x, loc.y))) {
if((mode == 0) || ((mode == 1) && (adjacent(from_where,loc))))
return loc;
else store_loc = loc;

View File

@@ -75,7 +75,6 @@ void start_town_mode(short which_town, short entry_dir) {
short at_which_save_slot,former_town;
bool monsters_loaded = false,town_toast = false;
location loc;
unsigned short temp;
bool play_town_sound = false;
if(town_force < 200)
@@ -230,15 +229,9 @@ void start_town_mode(short which_town, short entry_dir) {
}
}
for(short j = 0; j < univ.town->max_dim; j++)
for(short k = 0; k < univ.town->max_dim; k++) { // now load in saved setup,
// except that pushable things restore to orig locs
// TODO: THIS IS A TEMPORARY HACK TO GET i VALUE
int i = std::find_if(univ.party.creature_save.begin(), univ.party.creature_save.end(), [&pop](cPopulation& p) {return &p == &pop;}) - univ.party.creature_save.begin();
temp = univ.party.setup[i][j][k] << 8;
temp &= ~(OBJECT_CRATE | OBJECT_BARREL | OBJECT_BLOCK);
univ.town.fields[j][k] |= temp;
}
// TODO: THIS IS A TEMPORARY HACK TO GET i VALUE
int i = std::find_if(univ.party.creature_save.begin(), univ.party.creature_save.end(), [&pop](cPopulation& p) {return &p == &pop;}) - univ.party.creature_save.begin();
univ.town.update_fields(univ.party.setup[i]);
}
if(!monsters_loaded) {
@@ -547,16 +540,13 @@ location end_town_mode(short switching_level,location destination) { // returns
pop = univ.town.monst;
// TODO: THIS IS A TEMPORARY HACK TO GET i VALUE
int i = std::find_if(univ.party.creature_save.begin(), univ.party.creature_save.end(), [&pop](cPopulation& p) {return &p == &pop;}) - univ.party.creature_save.begin();
for(short j = 0; j < univ.town->max_dim; j++)
for(short k = 0; k < univ.town->max_dim; k++)
univ.party.setup[i][j][k] = (univ.town.fields[j][k] & 0xff00) >> 8;
univ.town.save_setup(univ.party.setup[i]);
data_saved = true;
break;
}
if(!data_saved) {
univ.party.creature_save[univ.party.at_which_save_slot] = univ.town.monst;
for(short j = 0; j < univ.town->max_dim; j++)
for(short k = 0; k < univ.town->max_dim; k++)
univ.party.setup[univ.party.at_which_save_slot][j][k] = (univ.town.fields[j][k] & 0xff00) >> 8;
univ.town.save_setup(univ.party.setup[univ.party.at_which_save_slot]);
univ.party.at_which_save_slot = (univ.party.at_which_save_slot == 3) ? 0 : univ.party.at_which_save_slot + 1;
}
@@ -930,7 +920,6 @@ void create_out_combat_terrain(short ter_type,short num_walls,bool is_road) {
for(short i = 0; i < 48; i++)
for(short j = 0; j < 48; j++) {
univ.town.fields[i][j] = 0;
if((j <= 8) || (j >= 35) || (i <= 8) || (i >= 35))
univ.town->terrain(i,j) = 90;
else univ.town->terrain(i,j) = ter_base[arena];

View File

@@ -69,6 +69,12 @@ public:
assign_from_container(list);
return *this;
}
void fill(Type val) {
row_ref& me = *this;
for(int i = 0; i < ref.width(); i++) {
me[i] = val;
}
}
// Seems like defining a move assignment operator deletes the copy constructor. Don't want that.
row_ref(const row_ref&) = default;
};
@@ -117,6 +123,12 @@ public:
assign_from_container(list);
return *this;
}
void fill(Type val) {
col_ref& me = *this;
for(int i = 0; i < ref.width(); i++) {
me[i] = val;
}
}
// Seems like defining a move assignment operator deletes the copy constructor. Don't want that.
col_ref(const col_ref&) = default;
};
@@ -244,6 +256,13 @@ public:
bool empty() const {
return data.empty();
}
void fill(Type val) {
for(size_t x = 0; x < w; x++) {
for(size_t y = 0; y < h; y++) {
(*this)[x][y] = val;
}
}
}
vector2d() : w(0), h(0) {}
vector2d(size_t w, size_t h) : vector2d() {
resize(w,h);

View File

@@ -92,6 +92,7 @@ cParty::cParty(const cParty& other)
, total_xp_gained(other.total_xp_gained)
, total_dam_taken(other.total_dam_taken)
, scen_name(other.scen_name)
, setup(other.setup)
, stored_items(other.stored_items)
, summons(other.summons)
, scen_won(other.scen_won)
@@ -100,7 +101,6 @@ cParty::cParty(const cParty& other)
, pointers(other.pointers)
{
memcpy(stuff_done, other.stuff_done, sizeof(stuff_done));
memcpy(setup, other.setup, sizeof(setup));
for(int i = 0; i < 6; i++) {
adven[i].reset(new cPlayer(*this, *other.adven[i]));
}
@@ -162,6 +162,7 @@ void cParty::swap(cParty& other) {
std::swap(total_dam_taken, other.total_dam_taken);
std::swap(scen_name, other.scen_name);
std::swap(adven, other.adven);
std::swap(setup, other.setup);
std::swap(stored_items, other.stored_items);
std::swap(summons, other.summons);
std::swap(scen_won, other.scen_won);
@@ -173,9 +174,6 @@ void cParty::swap(cParty& other) {
memcpy(stuff_done, other.stuff_done, sizeof(stuff_done));
memcpy(other.stuff_done, temp_sdf, sizeof(stuff_done));
unsigned short temp_setup[4][64][64];
memcpy(temp_setup, setup, sizeof(setup));
memcpy(setup, other.setup, sizeof(setup));
memcpy(other.setup, temp_setup, sizeof(setup));
for(size_t i = 0; i < adven.size(); i++) {
std::swap(adven[i], other.adven[i]);
}

View File

@@ -129,7 +129,7 @@ public:
private:
std::array<std::unique_ptr<cPlayer>,6> adven;
public:
unsigned short setup[4][64][64]; // formerly setup_save_type
std::array<vector2d<unsigned short>, 4> setup; // formerly setup_save_type
std::array<std::vector<cItem>,3> stored_items; // formerly stored_items_list_type
std::vector<cMonster> summons; // an array of monsters which can be summoned by the party's items yet don't originate from this scenario

View File

@@ -108,10 +108,8 @@ cTown& cCurTown::operator * (){
void cCurTown::place_preset_fields() {
// Initialize barriers, etc. Note non-sfx gets forgotten if this is a town recently visited.
for(int i = 0; i < 64; i++)
for(int j = 0; j < 64; j++) {
fields[i][j] = 0;
}
fields.resize(record()->max_dim, record()->max_dim);
fields.fill(0);
for(size_t i = 0; i < record()->preset_fields.size(); i++) {
switch(record()->preset_fields[i].type){
case OBJECT_BLOCK:
@@ -176,6 +174,26 @@ void cCurTown::place_preset_fields() {
}
}
void cCurTown::update_fields(const vector2d<unsigned short>& setup) {
for(short i = 0; i < record()->max_dim && i < setup.width(); i++) {
for(short j = 0; j < record()->max_dim && j < setup.height(); j++) {
// except that pushable things restore to orig locs
unsigned short temp = setup[i][j] << 8;
temp &= ~(OBJECT_CRATE | OBJECT_BARREL | OBJECT_BLOCK);
univ.town.fields[i][j] |= temp;
}
}
}
void cCurTown::save_setup(vector2d<unsigned short>& setup) const {
setup.resize(record()->max_dim, record()->max_dim);
for(short i = 0; i < record()->max_dim; i++) {
for(short j = 0; j < record()->max_dim; j++) {
setup[i][j] = fields[i][j] >> 8;
}
}
}
cSpeech& cCurTown::cur_talk() {
// Make sure we actually have a valid speech stored
return univ.scenario.towns[cur_talk_loaded]->talking;
@@ -190,6 +208,8 @@ bool cCurTown::prep_talk(short which) {
void cCurTown::prep_arena() {
if(arena != nullptr) delete arena;
arena = new cTown(univ.scenario, AREA_MEDIUM);
fields.resize(AREA_MEDIUM, AREA_MEDIUM);
fields.fill(0);
}
cCurTown::~cCurTown() {
@@ -201,6 +221,13 @@ cTown*const cCurTown::record() const {
return univ.scenario.towns[univ.party.town_num];
}
bool cCurTown::is_summon_safe(short x, short y) const {
if(x > record()->max_dim || y > record()->max_dim) return false;
// Here 254 indicates the low byte of the town fields, minus explored spaces (which is lowest bit).
static const unsigned long blocking_fields = SPECIAL_SPOT | OBJECT_CRATE | OBJECT_BARREL | OBJECT_BLOCK | FIELD_QUICKFIRE | 254;
return fields[x][y] & blocking_fields;
}
bool cCurTown::is_explored(short x, short y) const{
if(x > record()->max_dim || y > record()->max_dim) return false;
return fields[x][y] & SPECIAL_EXPLORED;
@@ -817,14 +844,7 @@ void cCurTown::writeTo(cTagFile& file) const {
}
}
auto& fields_page = file.add();
vector2d<as_hex<unsigned long>> fields_tmp;
fields_tmp.resize(64, 64);
for(size_t x = 0; x < 64; x++) {
for(size_t y = 0; y < 64; y++) {
fields_tmp[x][y] = fields[x][y];
}
}
fields_page["FIELDS"].encode(fields_tmp);
fields_page["FIELDS"].encode(fields);
fields_page["TERRAIN"].encode(record()->terrain);
// TODO: Do we need to save special_spot?
}
@@ -837,13 +857,11 @@ void cCurTown::readFrom(const cTagFile& file){
monst.hostile = page.contains("HOSTILE");
page["AT"] >> univ.party.town_loc.x >> univ.party.town_loc.y;
} else if(page.getFirstKey() == "FIELDS" || page.getFirstKey() == "TERRAIN") {
vector2d<as_hex<unsigned long>> fields_tmp;
page["FIELDS"].extract(fields_tmp);
page["FIELDS"].extract(fields);
page["TERRAIN"].extract(record()->terrain);
fields_tmp.resize(64, 64);
for(size_t x = 0; x < 64; x++) {
for(size_t y = 0; y < 64; y++) {
fields[x][y] = fields_tmp[x][y];
fields.resize(record()->max_dim, record()->max_dim);
for(size_t x = 0; x < record()->max_dim; x++) {
for(size_t y = 0; y < record()->max_dim; y++) {
if(is_quickfire(x, y)) {
quickfire_present = true;
}
@@ -876,9 +894,6 @@ void cCurTown::readFrom(const cTagFile& file){
cCurTown::cCurTown(cUniverse& univ) : univ(univ) {
arena = nullptr;
univ.party.town_num = 200;
for(int i = 0; i < 64; i++)
for(int j = 0; j < 64; j++)
fields[i][j] = 0L;
}
cCurOut::cCurOut(cUniverse& univ) : univ(univ) {}
@@ -973,7 +988,7 @@ void cCurTown::copy(const cCurTown& other) {
difficulty = other.difficulty;
monst = other.monst;
items = other.items;
memcpy(fields, other.fields, sizeof(fields));
fields = other.fields;
}
void cCurTown::swap(cCurTown& other) {
@@ -983,10 +998,7 @@ void cCurTown::swap(cCurTown& other) {
std::swap(difficulty, other.difficulty);
monst.swap(other.monst);
std::swap(items, other.items);
unsigned long temp[64][64];
memcpy(temp, other.fields, sizeof(fields));
memcpy(other.fields, fields, sizeof(fields));
memcpy(fields, temp, sizeof(fields));
fields.swap(other.fields);
}
void cUniverse::check_monst(cMonster& monst) {

View File

@@ -39,6 +39,7 @@ class cCurTown {
cUniverse& univ;
cTown* arena;
cTown*const record() const;
vector2d<unsigned long> fields;
public:
bool quickfire_present = false, belt_present = false;
// formerly current_town_type
@@ -47,8 +48,6 @@ public:
std::vector<cItem> items; // formerly town_item_list type
unsigned long fields[64][64];
void import_legacy(legacy::current_town_type& old);
void import_legacy(legacy::town_item_list& old);
void import_legacy(unsigned char(& old_sfx)[64][64], unsigned char(& old_misc_i)[64][64]);
@@ -62,6 +61,9 @@ public:
bool prep_talk(short which); // Prepare for loading specified speech, returning true if already loaded
void prep_arena(); // Set up for a combat arena
void place_preset_fields();
void update_fields(const vector2d<unsigned short>& setup);
void save_setup(vector2d<unsigned short>& setup) const;
bool is_summon_safe(short x, short y) const;
bool is_explored(short x, short y) const;
bool is_force_wall(short x, short y) const;