Merge pull request #543 from NQNStudios:editor-bounds

Scenedit Quality of life: be helpful when scrolling past boundaries

I've implemented 2 features here:
* When scrolling past the boundaries of the current outdoor section, you will get a yes/no prompt which can load the adjacent section for you at the corresponding center position.
* When scrolling past the boundaries (literal, not the changeable rectangle) of the current town, the editor will ask if you want to jump to the town's entrance in the outdoors. If there are more than one, you can choose.

I did this because I need to be able to find town entrances in the built-in scenarios so I can debug things.
This commit is contained in:
2025-01-23 09:25:36 -05:00
committed by GitHub
12 changed files with 206 additions and 54 deletions

View File

@@ -0,0 +1,11 @@
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<?xml-stylesheet href="dialog.xsl" type="text/xsl"?>
<dialog defbtn='no'>
<button name='no' type='regular' def-key='n' top='39' left='244'>No</button>
<button name='yes' type='regular' def-key='y' top='39' left='178'>Yes</button>
<pict type='dlog' num='11' top='9' left='9'/>
<text top='4' left='51' width='251' height='32'>
Shift to this outdoor section?
</text>
<text name='out-sec' relative='pos-in pos' rel-anchor='prev' top='4' left='0'></text>
</dialog>

View File

@@ -0,0 +1,11 @@
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<?xml-stylesheet href="dialog.xsl" type="text/xsl"?>
<dialog defbtn='no'>
<button name='no' type='regular' def-key='n' top='39' left='244'>No</button>
<button name='yes' type='regular' def-key='y' top='39' left='178'>Yes</button>
<pict type='dlog' num='11' top='9' left='9'/>
<text top='4' left='51' width='251' height='32'>
Shift to this town's entrance in this outdoor section?
</text>
<text name='out-sec' relative='pos-in pos' rel-anchor='prev' top='4' left='0'></text>
</dialog>

View File

@@ -52,7 +52,8 @@ public:
cDialog* operator->();
/// Show the dialog.
/// @param selectedIndex The index of the string that should be initially selected when the dialog is shown.
/// @return The index of the newly selected string; if the user cancelled, this will be equal to selectedIndex.
/// @return The index of the newly selected string; if the user cancelled, this will be equal to the initial
/// selectedIndex you provide. (So, pass -1 or something to signify that cancelling means the result is invalid.)
/// If initialized from an iterator range, this will be relative to begin.
size_t show(size_t selectedIndex);
};

View File

@@ -440,33 +440,27 @@ static void handle_scenario_args() {
}
// Try to put the party in an outdoor section from which you can enter the town --
// so when you leave, you'll hopefully be in the right place.
bool found_entrance = false;
for(int x = 0; x < univ.scenario.outdoors.width(); ++x){
for(int y = 0; y < univ.scenario.outdoors.height(); ++y){
for(spec_loc_t& entrance : univ.scenario.outdoors[x][y]->city_locs){
if(entrance.spec == *scen_arg_town){
// Very janky but I don't know how else to make it properly load the right sections and set i_w_c
while(univ.party.outdoor_corner.x > x){
shift_universe_left();
}
while(univ.party.outdoor_corner.x < x){
shift_universe_right();
}
while(univ.party.outdoor_corner.y > y){
shift_universe_up();
}
while(univ.party.outdoor_corner.y < y){
shift_universe_down();
}
outd_move_party(local_to_global(entrance), true);
found_entrance = true;
break;
}
}
if(found_entrance) break;
auto town_entrances = univ.scenario.find_town_entrances(*scen_arg_town);
if(!town_entrances.empty()){
// When there are multiple entrances, this part of the code shouldn't matter,
// but also won't hurt.
town_entrance_t first_entrance_found = town_entrances[0];
int x = first_entrance_found.out_sec.x;
int y = first_entrance_found.out_sec.y;
// Very janky but I don't know how else to make it properly load the right sections and set i_w_c
while(univ.party.outdoor_corner.x > x){
shift_universe_left();
}
if(found_entrance) break;
while(univ.party.outdoor_corner.x < x){
shift_universe_right();
}
while(univ.party.outdoor_corner.y > y){
shift_universe_up();
}
while(univ.party.outdoor_corner.y < y){
shift_universe_down();
}
outd_move_party(local_to_global(first_entrance_found.loc), true);
}
short town_entrance = 0;

View File

@@ -577,3 +577,17 @@ void cScenario::readFrom(const cTagFile& file){
}
}
}
std::vector<town_entrance_t> cScenario::find_town_entrances(int town_num) {
std::vector<town_entrance_t> matching_entrances;
for(int x = 0; x < outdoors.width(); ++x){
for(int y = 0; y < outdoors.height(); ++y){
for(spec_loc_t& entrance : outdoors[x][y]->city_locs){
if(town_num == -1 || entrance.spec == town_num){
matching_entrances.push_back({{x, y}, {entrance.x, entrance.y}, static_cast<int>(entrance.spec)});
}
}
}
}
return matching_entrances;
}

View File

@@ -39,6 +39,13 @@ struct scenario_header_flags {
enum eContentRating {G, PG, R, NC17};
// Used for finding town entrances in the outdoors
struct town_entrance_t {
location out_sec;
location loc;
int town;
};
class cScenario {
public:
class cItemStorage {
@@ -117,6 +124,10 @@ public:
cItem return_treasure(int loot, bool allow_junk_treasure = false) const;
cItem pull_item_of_type(unsigned int loot_max,short min_val,short max_val,const std::vector<eItemType>& types,bool allow_junk_treasure=false) const;
// Debugging/Editing helper: find town entrances in the outdoors. When town_num is specified, only return entrances
// to the town with that number
std::vector<town_entrance_t> find_town_entrances(int town_num = -1);
void reset_version();
explicit cScenario();
~cScenario();

View File

@@ -5,6 +5,8 @@
#include <array>
#include <string>
#include <stack>
#include <vector>
#include <boost/lexical_cast.hpp>
#include "scen.global.hpp"
#include "scenario/scenario.hpp"
#include "gfx/render_shapes.hpp"
@@ -23,6 +25,7 @@
#include "tools/cursors.hpp"
#include "dialogxml/widgets/scrollbar.hpp"
#include "dialogxml/dialogs/strdlog.hpp"
#include "dialogxml/dialogs/strchoice.hpp"
#include "dialogxml/dialogs/choicedlog.hpp"
#ifndef MSBUILD_GITREV
#include "tools/gitrev.hpp"
@@ -236,10 +239,7 @@ static bool handle_lb_action(location the_point) {
file_to_load = nav_get_scenario();
if(!file_to_load.empty() && load_scenario(file_to_load, scenario)) {
set_current_town(scenario.last_town_edited);
cur_out = scenario.last_out_edited;
current_terrain = scenario.outdoors[cur_out.x][cur_out.y];
overall_mode = MODE_MAIN_SCREEN;
set_up_main_screen();
set_current_out(scenario.last_out_edited);
} else if(!file_to_load.empty())
// If we tried to load but failed, the scenario record is messed up, so boot to start screen.
set_up_start_screen();
@@ -277,9 +277,7 @@ static bool handle_lb_action(location the_point) {
case LB_LOAD_OUT:
spot_hit = pick_out(cur_out, scenario);
if(spot_hit != cur_out) {
cur_out = spot_hit;
current_terrain = scenario.outdoors[cur_out.x][cur_out.y];
set_up_main_screen();
set_current_out(spot_hit);
}
break;
case LB_EDIT_OUT:
@@ -1153,34 +1151,38 @@ static bool handle_terrain_action(location the_point, bool ctrl_hit) {
return true;
}
bool need_redraw = false;
if((the_point.in(border_rect[0])) & (cen_y > (editing_town ? 4 : 3))) {
cen_y--;
if((the_point.in(border_rect[0]))) {
if(ctrl_hit)
cen_y = ((editing_town) ? 4 : 3);
else
handle_editor_screen_shift(0, -1);
need_redraw = true;
mouse_button_held = true;
}
if((the_point.in(border_rect[1])) & (cen_x > (editing_town ? 4 : 3))) {
cen_x--;
if((the_point.in(border_rect[1]))) {
if(ctrl_hit)
cen_x = ((editing_town) ? 4 : 3);
else
handle_editor_screen_shift(-1, 0);
need_redraw = true;
mouse_button_held = true;
}
auto max_dim = cur_area->max_dim - 5;
// This allows you to see a strip of terrain from the adjacent sector when editing outdoors
if(!editing_town) max_dim++;
if((the_point.in(border_rect[2])) && (cen_y < max_dim)) {
cen_y++;
if((the_point.in(border_rect[2]))) {
if(ctrl_hit)
cen_y = max_dim;
else
handle_editor_screen_shift(0, 1);
need_redraw = true;
mouse_button_held = true;
}
if((the_point.in(border_rect[3])) && (cen_x < max_dim)) {
cen_x++;
if((the_point.in(border_rect[3]))) {
if(ctrl_hit)
cen_x = max_dim;
else
handle_editor_screen_shift(1, 0);
need_redraw = true;
mouse_button_held = true;
}
@@ -1781,13 +1783,119 @@ void handle_keystroke(sf::Event event) {
mouse_button_held = false;
}
bool handle_outdoor_sec_shift(int dx, int dy){
if(editing_town) return false;
int new_x = cur_out.x + dx;
int new_y = cur_out.y + dy;
if(new_x < 0) return true;
if(new_x >= scenario.outdoors.width()) return true;
if(new_y < 0) return true;
if(new_y >= scenario.outdoors.height()) return true;
cChoiceDlog shift_prompt("shift-outdoor-section", {"yes", "no"});
location new_out_sec = { new_x, new_y };
shift_prompt->getControl("out-sec").setText(boost::lexical_cast<std::string>(new_out_sec));
if(shift_prompt.show() == "yes"){
int last_cen_x = cen_x;
int last_cen_y = cen_y;
set_current_out(new_out_sec);
// match the terrain view to where we were
start_out_edit();
if(dx < 0) {
cen_x = get_current_area()->max_dim - 4;
}else if(dx > 0){
cen_x = 3;
}else{
cen_x = last_cen_x;
}
if(dy < 0){
cen_y = get_current_area()->max_dim - 4;
}else if(dy > 0){
cen_y = 3;
}else{
cen_y = last_cen_y;
}
redraw_screen();
}
return true;
}
void handle_editor_screen_shift(int dx, int dy) {
int min = (editing_town ? 4 : 3);
int max = get_current_area()->max_dim - 5;
if(!editing_town) max++;
bool out_of_bounds = false;
if(cen_x + dx < min){
// In outdoors, prompt whether to swap to the next section west
if(handle_outdoor_sec_shift(-1, 0)) return;
out_of_bounds = true;
}else if(cen_x + dx > max){
// In outdoors, prompt whether to swap to the next section east
if(handle_outdoor_sec_shift(1, 0)) return;
out_of_bounds = true;
}else if(cen_y + dy < min){
// In outdoors, prompt whether to swap to the next section north
if(handle_outdoor_sec_shift(0, -1)) return;
out_of_bounds = true;
}else if(cen_y + dy > max){
// In outdoors, prompt whether to swap to the next section south
if(handle_outdoor_sec_shift(0, 1)) return;
out_of_bounds = true;
}
if(out_of_bounds){
// In town, prompt whether to go back to outdoor entrance location
std::vector<town_entrance_t> town_entrances = scenario.find_town_entrances(cur_town);
if(town_entrances.size() == 1){
town_entrance_t only_entrance = town_entrances[0];
cChoiceDlog shift_prompt("shift-town-entrance", {"yes", "no"});
shift_prompt->getControl("out-sec").setText(boost::lexical_cast<std::string>(only_entrance.out_sec));
if(shift_prompt.show() == "yes"){
set_current_out(only_entrance.out_sec);
start_out_edit();
cen_x = only_entrance.loc.x;
cen_y = only_entrance.loc.y;
redraw_screen();
return;
}
}else if(town_entrances.size() > 1){
std::vector<std::string> entrance_strings;
for(town_entrance_t entrance : town_entrances){
std::ostringstream sstr;
sstr << "Entrance in section " << entrance.out_sec << " at " << entrance.loc;
entrance_strings.push_back(sstr.str());
}
cStringChoice dlog(entrance_strings, "Shift to one of this town's entrances in the outdoors?");
size_t choice = dlog.show(-1);
if(choice >= 0 && choice < town_entrances.size()){
town_entrance_t entrance = town_entrances[choice];
set_current_out(entrance.out_sec);
start_out_edit();
cen_x = entrance.loc.x;
cen_y = entrance.loc.y;
redraw_screen();
return;
}
}
}
cen_x = minmax(min, max, cen_x + dx);
cen_y = minmax(min, max, cen_y + dy);
}
void handle_scroll(const sf::Event& event) {
location pos { translate_mouse_coordinates({event.mouseMove.x,event.mouseMove.y}) };
int amount = event.mouseWheel.delta;
if(overall_mode < MODE_MAIN_SCREEN && pos.in(terrain_rect)) {
if(kb.isCtrlPressed())
cen_x = minmax(4, town->max_dim - 5, cen_x - amount);
else cen_y = minmax(4, town->max_dim - 5, cen_y - amount);
handle_editor_screen_shift(-amount, 0);
else handle_editor_screen_shift(0, -amount);
draw_terrain();
place_location();
}
}

View File

@@ -8,6 +8,7 @@ void flash_rect(rectangle to_flash);
void swap_terrain();
void set_new_terrain(ter_num_t selected_terrain);
void handle_keystroke(sf::Event event);
void handle_editor_screen_shift(int dx, int dy);
void handle_scroll(const sf::Event& event);
void get_wandering_monst();
void get_town_info();

View File

@@ -52,11 +52,9 @@ void set_up_apple_events() {
if(load_scenario(fileName, scenario)) {
set_current_town(scenario.last_town_edited);
cur_out = scenario.last_out_edited;
current_terrain = scenario.outdoors[cur_out.x][cur_out.y];
change_made = false;
ae_loading = true;
set_up_main_screen();
set_current_out(scenario.last_out_edited);
}
return TRUE;
}

View File

@@ -304,11 +304,9 @@ static void process_args(int argc, char* argv[]) {
if(!file.empty()) {
if(load_scenario(file, scenario)) {
set_current_town(scenario.last_town_edited);
cur_out = scenario.last_out_edited;
current_terrain = scenario.outdoors[cur_out.x][cur_out.y];
change_made = false;
ae_loading = true;
set_up_main_screen();
set_current_out(scenario.last_out_edited);
} else {
std::cout << "Failed to load scenario: " << file << std::endl;
}
@@ -449,11 +447,8 @@ void handle_menu_choice(eMenu item_hit) {
if(!file_to_load.empty() && load_scenario(file_to_load, scenario)) {
cur_town = scenario.last_town_edited;
town = scenario.towns[cur_town];
cur_out = scenario.last_out_edited;
current_terrain = scenario.outdoors[cur_out.x][cur_out.y];
overall_mode = MODE_MAIN_SCREEN;
change_made = false;
set_up_main_screen();
set_current_out(scenario.last_out_edited);
} else if(!file_to_load.empty())
set_up_start_screen(); // Failed to load file, dump to start
undo_list.clear();

View File

@@ -1296,6 +1296,13 @@ void set_current_town(int to) {
scenario.last_town_edited = cur_town;
}
void set_current_out(location out_sec) {
cur_out = out_sec;
scenario.last_out_edited = cur_out;
current_terrain = scenario.outdoors[cur_out.x][cur_out.y];
set_up_main_screen();
}
aNewTown::aNewTown(cTown* t)
: cAction("add town")
, theTown(t)

View File

@@ -23,3 +23,4 @@ void edit_placed_item(short which_i);
void delete_last_town();
void edit_town_wand();
void set_current_town(int to);
void set_current_out(location out_sec);