Implement all the missing parts of the Edit Sheets and Edit Sounds dialogs

- On load, the game now detects graphic sheets and sounds whose IDs are "discontinuous", as well as graphics intended to directly replace preset graphic sheets.
- Edit sheets dialog can now handle "discontinuous" graphics. (The edit sounds dialog already could.)
- Edit sheets dialog prompts user to create a new sheet if there are none already, and also if there are some but not ID 0 (in the latter case they can cancel and still edit the sheets).
- Edit sheets dialog prompts user to convert sheets if the scenario is legacy, rather than doing it silently
- Edit sheets dialog now has "new" and "delete" buttons
- Edit sounds dialog now has functioning "delete" button
This commit is contained in:
2015-06-17 04:29:10 -04:00
parent 580f70f49a
commit 9d74f78df3
9 changed files with 263 additions and 54 deletions

View File

@@ -53,20 +53,10 @@ graphics.</p>
will work). For monster, item, and missile graphics, the slots must have a transparent
background.</p>
<p>Once you have your graphics sheet, there are a few ways to get it into the
scenario. A scenario generally consists of a archive file (specifically, a gzipped
tarball) containing various resource files, so you could put the graphics into the archive
yourself, either by decompressing it and recompressing it or by using a program that
allows you to open an archive without uncompressing it.</p>
<p>However, there's an easier way. Open your scenario in the scenario editor, then go to
the OBoE temporary files folder, where you'll find an uncompressed copy of your scenario.
(On the Mac, this can be found at "~/Library/Application Support/Blades of Exile"; on
Windows, it will be at "%APPDATA%/Blades of Exile".) Simply drop the files into the
graphics subdirectory within the scenario directory (if it doesn't exist, you can create
it), then return to the scenario editor and save the scenario. The scenario will
automatically add the new graphics to the archived copy of the scenario. You may need to
reload the scenario in the editor before it notice them, however.</p>
<p>Once you have your graphics sheet, the easiest way to get it into the scenario is to
select "Edit Custom Graphic Sheets" from the "Scenario" menu and either paste the image in
or click "Import" to load it in from a file. You may also need to click "New" first to
ensure the target sheet actually exists.</p>
<h2>Placing and Using Your Custom Graphics</h2>

View File

@@ -0,0 +1,15 @@
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<?xml-stylesheet href="dialog.xsl" type="text/xsl"?>
<dialog skin='light' defbtn='new'>
<pict type='dlog' num='16' top='6' left='6'/>
<text top='6' left='48' width='252' height='101'>
Before you create the new sheet, you need to decide what its sheet number will be.
Use the default value if you intend to fill the sheet with graphics such as terrains, items, monsters, etc.
However, if you intend to use the sheet for certain special nodes that request a full sheet,
you should probably give it a number of 100 or greater.
</text>
<text top='111' left='48' width='80' height='16'>Sheet number:</text>
<field name='num' top='110' left='138' width='80' height='16'/>
<button name='cancel' type='regular' top='141' left='234'>Cancel</button>
<button name='new' type='regular' top='141' left='169'>Create</button>
</dialog>

View File

@@ -0,0 +1,14 @@
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<?xml-stylesheet href="dialog.xsl" type="text/xsl"?>
<dialog skin='light' defbtn='convert'>
<pict type='dlog' num='16' top='6' left='6'/>
<text top='6' left='48' width='252' height='121'>
This scenario was loaded from a legacy file, and as such its custom graphics have not yet been converted.
They need to be converted before you can edit them in the scenario editor.
This will automatically happen when you save, but if you prefer, you can convert them now.
Be aware that the conversion converts white to transparency,
so it may create unwanted holes if you had terrain or dialog graphics that used pure white.
</text>
<button name='cancel' type='regular' top='131' left='234'>Cancel</button>
<button name='convert' type='large' top='131' left='128'>Convert Now</button>
</dialog>

View File

@@ -0,0 +1,12 @@
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<?xml-stylesheet href="dialog.xsl" type="text/xsl"?>
<dialog skin='light' defbtn='new'>
<pict type='dlog' num='16' top='6' left='6'/>
<text top='6' left='48' width='252' height='51'>
You don't yet have any custom sheets in this scenario!
Would you like to create a new empty sheet?
You can then paste your graphics in or import them from a file.
</text>
<button name='cancel' type='regular' top='61' left='234'>Cancel</button>
<button name='new' type='large' top='61' left='128'>Create New</button>
</dialog>

View File

@@ -0,0 +1,14 @@
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<?xml-stylesheet href="dialog.xsl" type="text/xsl"?>
<dialog skin='light' defbtn='new'>
<pict type='dlog' num='16' top='6' left='6'/>
<text top='6' left='48' width='252' height='91'>
Though this scenario does currently have some custom sheets,
they will not be loaded for normal graphics and can only be used in special nodes that request a full sheet.
This is because there is no sheet #0.
Would you like to create a blank sheet #0?
You can then paste in your graphics or import from a file.
</text>
<button name='cancel' type='regular' top='101' left='234'>Cancel</button>
<button name='new' type='large' top='101' left='128'>Create New</button>
</dialog>

View File

@@ -0,0 +1,13 @@
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<?xml-stylesheet href="dialog.xsl" type="text/xsl"?>
<dialog skin='light' defbtn='cancel'>
<pict type='dlog' num='16' top='6' left='6'/>
<text top='6' left='48' width='252' height='91'>
Warning: By deleting this sheet, graphics on subsequent sheets will no longer be recognized by the game.
You can delete the sheet anyway, or you can move the subsequent sheets back one, overwriting this sheet.
Note that, either way, anything referencing graphics on these sheets will have to be updated.
</text>
<button name='cancel' type='regular' top='101' left='234'>Cancel</button>
<button name='move' type='large' top='101' left='63'>Move Sheets</button>
<button name='del' type='regular' top='101' left='169'>Delete</button>
</dialog>

View File

@@ -493,6 +493,7 @@ void cPict::recalcRect() {
break;
case PIC_FULL:
case PIC_CUSTOM_FULL:
if(drawScaled) break;
auto sheet = getSheet(SHEET_FULL, picNum);
sf::Vector2u sz = sheet->getSize();
bounds.width() = sz.x;

View File

@@ -3308,19 +3308,51 @@ void edit_custom_pics_types() {
pic_dlg.run();
}
void set_dlg_custom_sheet(cDialog& me, size_t sheet) {
static void set_dlg_custom_sheet(cDialog& me, size_t sheet) {
me["num"].setTextToNum(sheet);
dynamic_cast<cPict&>(me["sheet"]).setPict(sheet, PIC_FULL);
}
extern fs::path tempDir;
void edit_custom_sheets() {
// TODO: What about "disconnected" sheets? eg, we have sheets 0-6, but also sheet 100.
int max_pic = -1;
std::vector<int> all_pics;
fs::path pic_dir = tempDir/"scenario/graphics";
if(!fs::exists(pic_dir)) fs::create_directories(pic_dir);
for(fs::directory_iterator iter(pic_dir); iter != fs::directory_iterator(); iter++) {
std::string fname = iter->path().filename().string().c_str();
int dot = fname.find_last_of('.');
if(fname.substr(0,5) == "sheet" && fname.substr(dot) == ".png" && std::all_of(fname.begin()+5, fname.begin()+dot, isdigit)) {
int this_pic = boost::lexical_cast<int>(fname.substr(5,dot-5));
max_pic = max(max_pic, this_pic);
all_pics.push_back(this_pic);
}
}
// First, make sure we even have custom graphics! Also make sure they're not legacy format.
if(spec_scen_g.numSheets < 1) {
bool must_init_spec_g = false;
if(max_pic < 0) {
if(cChoiceDlog("have-no-pics", {"cancel", "new"}).show() == "cancel")
return;
must_init_spec_g = true;
} else if(max_pic >= 0 && spec_scen_g.numSheets < 1) {
if(cChoiceDlog("have-only-full-pics", {"cancel", "new"}).show() == "new")
must_init_spec_g = true;
} else if(spec_scen_g.is_old) {
if(cChoiceDlog("convert-pics-now", {"cancel", "convert"}).show() == "cancel")
return;
spec_scen_g.convert_sheets();
}
if(must_init_spec_g) {
spec_scen_g.clear();
spec_scen_g.sheets = new sf::Texture[1];
spec_scen_g.numSheets = 1;
spec_scen_g.sheets[0].create(280, 360);
spec_scen_g.sheets[0].copyToImage().saveToFile((pic_dir/"sheet0.png").string().c_str());
all_pics.insert(all_pics.begin(), 0);
}
set_cursor(watch_curs);
// Get image data from the sheets in memory
@@ -3336,21 +3368,35 @@ void edit_custom_sheets() {
cDialog pic_dlg("graphic-sheets");
pic_dlg["cancel"].attachClickHandler(std::bind(&cDialog::toast, _1, false));
pic_dlg["okay"].attachClickHandler(std::bind(&cDialog::toast, _1, true));
pic_dlg["copy"].attachClickHandler([&sheets,&cur](cDialog&, std::string, eKeyMod) -> bool {
pic_dlg["copy"].attachClickHandler([&sheets,&cur,&all_pics,&pic_dir](cDialog&, std::string, eKeyMod) -> bool {
if(cur >= spec_scen_g.numSheets) {
fs::path fromPath = pic_dir/("sheet" + std::to_string(all_pics[cur]) + ".png");
sf::Image img;
img.loadFromFile(fromPath.string().c_str());
set_clipboard_img(img);
return true;
}
set_clipboard_img(sheets[cur]);
return true;
});
pic_dlg["paste"].attachClickHandler([&sheets,&cur](cDialog&, std::string, eKeyMod) -> bool {
pic_dlg["paste"].attachClickHandler([&sheets,&cur,&all_pics,&pic_dir](cDialog&, std::string, eKeyMod) -> bool {
auto img = get_clipboard_img();
if(img == nullptr) {
beep();
return true;
}
if(cur >= spec_scen_g.numSheets) {
std::string resName = "sheet" + std::to_string(all_pics[cur]);
fs::path toPath = pic_dir/(resName + ".png");
img->saveToFile(toPath.string().c_str());
ResMgr::free<ImageRsrc>(resName);
return true;
}
sheets[cur] = *img;
spec_scen_g.replace_sheet(cur, *img);
return true;
});
pic_dlg["open"].attachClickHandler([&sheets,&cur](cDialog&, std::string, eKeyMod) -> bool {
pic_dlg["open"].attachClickHandler([&sheets,&cur,&all_pics,&pic_dir](cDialog&, std::string, eKeyMod) -> bool {
fs::path fpath = nav_get_rsrc({"png", "bmp", "jpg", "jpeg", "gif", "psd"});
if(fpath.empty()) return true;
sf::Image img;
@@ -3358,39 +3404,133 @@ void edit_custom_sheets() {
beep();
return true;
}
if(cur >= spec_scen_g.numSheets) {
std::string resName = "sheet" + std::to_string(all_pics[cur]);
fs::path toPath = pic_dir/(resName + ".png");
img.saveToFile(toPath.string().c_str());
ResMgr::free<ImageRsrc>(resName);
return true;
}
sheets[cur] = img;
spec_scen_g.replace_sheet(cur, img);
return true;
});
pic_dlg["save"].attachClickHandler([&sheets,&cur](cDialog&, std::string, eKeyMod) -> bool {
pic_dlg["save"].attachClickHandler([&sheets,&cur,&all_pics,&pic_dir](cDialog&, std::string, eKeyMod) -> bool {
fs::path fpath = nav_put_rsrc({"png", "bmp", "jpg", "jpeg"});
if(fpath.empty()) return true;
if(cur >= spec_scen_g.numSheets) {
fs::path fromPath = pic_dir/("sheet" + std::to_string(all_pics[cur]) + ".png");
sf::Image img;
img.loadFromFile(fromPath.string().c_str());
img.saveToFile(fpath.string().c_str());
return true;
}
sheets[cur].saveToFile(fpath.string().c_str());
return true;
});
pic_dlg["new"].attachClickHandler([&sheets,&cur,&all_pics,&pic_dir](cDialog& me, std::string, eKeyMod) -> bool {
cChoiceDlog pickNum("add-new-sheet", {"cancel", "new"}, &me);
pickNum->getControl("num").setTextToNum(spec_scen_g.numSheets);
if(pickNum.show() == "cancel") return true;
int newSheet = pickNum->getControl("num").getTextAsNum();
fs::path sheetPath = pic_dir/("sheet" + std::to_string(newSheet) + ".png");
if(newSheet == spec_scen_g.numSheets) {
sf::Texture* wasSheets = spec_scen_g.sheets;
spec_scen_g.sheets = new sf::Texture[spec_scen_g.numSheets + 1];
std::copy_n(wasSheets, spec_scen_g.numSheets, spec_scen_g.sheets);
spec_scen_g.sheets[newSheet].create(280,360);
spec_scen_g.sheets[newSheet].copyToImage().saveToFile(sheetPath.string().c_str());
spec_scen_g.numSheets++;
auto iter = all_pics.insert(std::upper_bound(all_pics.begin(), all_pics.end(), newSheet), newSheet);
cur = iter - all_pics.begin();
} else {
auto iter = std::lower_bound(all_pics.begin(), all_pics.end(), newSheet);
if(*iter == newSheet) {
giveError("Sorry, but that sheet already exists! Try creating a sheet with a different number.", "Sheet number: " + std::to_string(newSheet), &me);
return true;
}
iter = all_pics.insert(iter, newSheet);
cur = iter - all_pics.begin();
sf::Image img;
img.create(280, 360);
img.saveToFile(sheetPath.string().c_str());
}
me["left"].show();
me["right"].show();
set_dlg_custom_sheet(me, all_pics[cur]);
return true;
});
pic_dlg["del"].attachClickHandler([&sheets,&cur,&all_pics,&pic_dir](cDialog& me, std::string, eKeyMod) -> bool {
int which_pic = all_pics[cur];
if(which_pic < spec_scen_g.numSheets) {
std::string choice = "del";
if(which_pic < spec_scen_g.numSheets - 1)
choice = cChoiceDlog("must-delete-in-order", {"cancel", "del", "move"}, &me).show();
if(choice == "cancel") return true;
sf::Texture* wasSheets = spec_scen_g.sheets;
if(choice == "move") {
spec_scen_g.sheets = new sf::Texture[spec_scen_g.numSheets-1];
std::copy_n(wasSheets, which_pic, spec_scen_g.sheets);
std::copy(wasSheets + which_pic + 1, wasSheets + spec_scen_g.numSheets, spec_scen_g.sheets + which_pic);
spec_scen_g.numSheets--;
for(; which_pic < spec_scen_g.numSheets; which_pic++) {
fs::path from = pic_dir/("sheet" + std::to_string(which_pic + 1) + ".png");
fs::path to = pic_dir/("sheet" + std::to_string(which_pic) + ".png");
if(!fs::exists(from)) continue; // Just in case
fs::remove(to);
fs::rename(from, to);
ResMgr::free<ImageRsrc>("sheet" + std::to_string(which_pic));
}
auto end = std::find(all_pics.begin() + cur, all_pics.end(), which_pic - 1);
if(end != all_pics.end())
all_pics.erase(end);
else {
// This shouldn't be reached
std::cerr << "Whoops! Somehow failed to remove the index of the deleted sheet!" << std::endl;
}
} else if(choice == "del") {
all_pics.erase(all_pics.begin() + cur);
spec_scen_g.numSheets = which_pic;
spec_scen_g.sheets = new sf::Texture[which_pic];
std::copy_n(wasSheets, which_pic, spec_scen_g.sheets);
ResMgr::free<ImageRsrc>("sheet" + std::to_string(which_pic));
}
delete[] wasSheets;
}
fs::path fpath = pic_dir/("sheet" + std::to_string(which_pic) + ".png");
if(fs::exists(fpath)) fs::remove(fpath);
if(all_pics.size() == 1) {
me["left"].hide();
me["right"].hide();
} else if(all_pics.empty()) {
cStrDlog("You've just deleted the last custom graphics sheet, so this dialog will now close. If you want to add more sheets, you can of course reopen the dialog.", "", "Last Sheet Deleted", 16, PIC_DLOG).show();
me.toast(true);
return true;
}
if(cur > 0) cur--;
set_dlg_custom_sheet(me, all_pics[cur]);
return true;
});
// TODO: These buttons not implemented yet
pic_dlg["new"].hide();
pic_dlg["del"].hide();
if(spec_scen_g.numSheets == 1) {
if(all_pics.size() == 1) {
pic_dlg["left"].hide();
pic_dlg["right"].hide();
} else pic_dlg.attachClickHandlers([&sheets,&cur](cDialog& me, std::string dir, eKeyMod) -> bool {
}
pic_dlg.attachClickHandlers([&sheets,&cur,&all_pics](cDialog& me, std::string dir, eKeyMod) -> bool {
if(dir == "left") {
if(cur == 0)
cur = spec_scen_g.numSheets - 1;
cur = all_pics.size() - 1;
else cur--;
} else if(dir == "right") {
cur++;
if(cur >= spec_scen_g.numSheets)
if(cur >= all_pics.size())
cur = 0;
} else return true;
set_dlg_custom_sheet(me, cur);
set_dlg_custom_sheet(me, all_pics[cur]);
return true;
}, {"left", "right"});
set_dlg_custom_sheet(pic_dlg, cur);
set_dlg_custom_sheet(pic_dlg, all_pics[cur]);
shut_down_menus(5); // So that cmd+O, cmd+N, cmd+S can work
pic_dlg.run();
@@ -3408,8 +3548,7 @@ void edit_custom_sheets() {
else shut_down_menus(3);
}
extern fs::path tempDir;
static bool edit_custom_sound_action(cDialog& me, std::string action, int curPage, int& max_snd) {
static bool edit_custom_sound_action(cDialog& me, std::string action, std::vector<std::string>& snd_names, int curPage, int& max_snd) {
size_t a_len = action.length();
int which_snd = (curPage + 1) * 100 + (action[a_len-1] - '0');
action.erase(action.end() - 1);
@@ -3423,7 +3562,10 @@ static bool edit_custom_sound_action(cDialog& me, std::string action, int curPag
if(action == "play") {
play_sound(-which_snd);
} else if(action == "del") {
// TODO: Implement this action
if(which_snd - 100 < snd_names.size())
snd_names[which_snd - 100].clear();
fs::remove(sndfile);
me["name" + std::to_string(which_snd % 10)].setText("");
} else if(action == "open") {
fs::path fpath = nav_get_rsrc({"wav"});
if(fpath.empty()) return true;
@@ -3448,7 +3590,7 @@ static bool edit_custom_sound_action(cDialog& me, std::string action, int curPag
return true;
}
static void fill_custom_sounds_page(cDialog& me, const std::vector<std::string>& snd_names, int& curPage, int& max_snd, bool firstTime) {
static void fill_custom_sounds_page(cDialog& me, std::vector<std::string>& snd_names, int& curPage, int& max_snd, bool firstTime) {
for(int i = 0; i < 10; i++) {
int which_snd = (curPage + 1) * 100 + i;
std::string id = std::to_string(i);
@@ -3458,7 +3600,7 @@ static void fill_custom_sounds_page(cDialog& me, const std::vector<std::string>&
"play" + id, "del" + id,
"open" + id, "save" + id,
};
me.attachClickHandlers(std::bind(edit_custom_sound_action, _1, _2, std::ref(curPage), std::ref(max_snd)), buttons);
me.attachClickHandlers(std::bind(edit_custom_sound_action, _1, _2, std::ref(snd_names), std::ref(curPage), std::ref(max_snd)), buttons);
}
me["num" + id].setTextToNum(which_snd);
if(which_snd - 100 < snd_names.size())
@@ -3496,8 +3638,8 @@ void edit_custom_sounds() {
}
int curPage = 0;
fill_custom_sounds_page(snd_dlg, scenario.snd_names, curPage, max_snd, true);
auto snd_names = scenario.snd_names;
fill_custom_sounds_page(snd_dlg, snd_names, curPage, max_snd, true);
if(max_snd < 110) {
snd_dlg["left"].hide();

View File

@@ -1881,29 +1881,37 @@ bool load_scenario_v2(fs::path file_to_load, cScenario& scenario) {
int num_graphic_sheets = 0;
if(is_packed) {
fs::remove_all(tempDir/"scenario");
int i = 0;
std::string fname;
while(fname = "scenario/graphics/sheet" + std::to_string(i) + ".png", pack.hasFile(fname)) {
std::bitset<65536> have_pic = {0};
for(auto& file : pack) {
std::string fname = file.filename;
int dot = fname.find_last_of('.');
if(fname.substr(0,23) == "scenario/graphics/sheet") {
if(fname.substr(dot,4) != ".png") continue;
if(!std::all_of(fname.begin() + 23, fname.begin() + dot, isdigit)) continue;
int i = boost::lexical_cast<int>(fname.substr(23, dot - 23));
if(i >= 65536) continue;
have_pic[i] = true;
} else if(fname.substr(0,18) == "scenario/graphics/") {
if(fname.substr(dot,4) != ".png") continue;
// This would be an override sheet, one that replaces one of the preset sheets.
// Or at least, we're going to assume it is. If it's not, there's no harm done
// (except possibly storing a sheet that will never be used).
// TODO: A way to edit these sheets in the scenario editor?
} else if(fname.substr(0,19) == "scenario/sounds/SND") {
if(fname.substr(dot,4) != ".wav") continue;
if(!std::all_of(fname.begin() + 19, fname.begin() + dot, isdigit)) continue;
} else continue;
fs::path path = tempDir/fname;
fs::create_directories(path.parent_path());
std::istream& graphic = pack.getFile(fname);
std::istream& f = pack.getFile(fname);
std::ofstream fout(path.string().c_str(), std::ios::binary);
fout << graphic.rdbuf();
fout << f.rdbuf();
fout.close();
i++;
}
// This is a bit of trickery to get it to only count the first consecutive range of sheets
while(have_pic[num_graphic_sheets])
num_graphic_sheets++;
ResMgr::pushPath<ImageRsrc>(tempDir/"scenario"/"graphics");
num_graphic_sheets = i;
i = 100;
while(fname = "scenario/sounds/SND" + std::to_string(i) + ".wav", pack.hasFile(fname)) {
fs::path path = tempDir/fname;
fs::create_directories(path.parent_path());
std::istream& snd = pack.getFile(fname);
std::ofstream fout(path.string().c_str(), std::ios::binary);
fout << snd.rdbuf();
fout.close();
i++;
}
ResMgr::pushPath<SoundRsrc>(tempDir/"scenario"/"sounds");
} else {
if(fs::is_directory(file_to_load/"graphics"))