Fix loading memory watches on project change
[lsnes.git] / src / core / project.cpp
blob4fa95d0fa279f33596e2f2a668f13ca5df1514e2
1 #include "core/command.hpp"
2 #include "core/controller.hpp"
3 #include "core/dispatch.hpp"
4 #include "core/inthread.hpp"
5 #include "core/project.hpp"
6 #include "core/misc.hpp"
7 #include "core/mainloop.hpp"
8 #include "core/memorywatch.hpp"
9 #include "core/moviefile.hpp"
10 #include "core/moviedata.hpp"
11 #include "core/settings.hpp"
12 #include "core/window.hpp"
13 #include "library/directory.hpp"
14 #include "library/minmax.hpp"
15 #include "library/string.hpp"
16 #include <fstream>
17 #include <dirent.h>
18 #if defined(_WIN32) || defined(_WIN64) || defined(TEST_WIN32_CODE)
19 #define FUCKED_SYSTEM
20 #include <windows.h>
21 #endif
23 void do_flush_slotinfo();
25 namespace
27 project_info* active_project = NULL;
29 void concatenate(std::vector<char>& data, const std::vector<char>& app)
31 size_t dsize = data.size();
32 data.resize(dsize + app.size());
33 std::copy(app.begin(), app.end(), data.begin() + dsize);
36 std::vector<char> base_decode(const std::string& str)
38 std::vector<char> r;
39 size_t len = str.length();
40 size_t i;
41 uint32_t a, b, c, d, e, v;
42 for(i = 0; i + 5 <= len; i += 5) {
43 a = str[i + 0] - 33;
44 b = str[i + 1] - 33;
45 c = str[i + 2] - 33;
46 d = str[i + 3] - 33;
47 e = str[i + 4] - 33;
48 v = 52200625 * a + 614125 * b + 7225 * c + 85 * d + e;
49 r.push_back(v);
50 r.push_back(v >> 8);
51 r.push_back(v >> 16);
52 r.push_back(v >> 24);
54 a = b = c = d = e = 0;
55 if(i + 0 < len) e = str[len - 1] - 33;
56 if(i + 1 < len) d = str[len - 2] - 33;
57 if(i + 2 < len) c = str[len - 3] - 33;
58 if(i + 3 < len) b = str[len - 4] - 33;
59 v = 614125 * b + 7225 * c + 85 * d + e;
60 if(i + 1 < len) r.push_back(v);
61 if(i + 2 < len) r.push_back(v >> 8);
62 if(i + 3 < len) r.push_back(v >> 16);
63 return r;
66 template<int x> void blockcode(std::ostringstream& s, const std::vector<char>& data, size_t& ptr,
67 size_t& chars)
69 uint32_t a = 0, b = 0, c = 0, d = 0, v;
70 if(x >= 4) a = static_cast<uint8_t>(data[ptr++]);
71 if(x >= 3) b = static_cast<uint8_t>(data[ptr++]);
72 if(x >= 2) c = static_cast<uint8_t>(data[ptr++]);
73 if(x >= 1) d = static_cast<uint8_t>(data[ptr++]);
74 v = 16777216 * d + 65536 * c + 256 * b + a;
75 v >>= (32 - 8 * x);
76 if(x >= 4) s << static_cast<char>(v / 52200625 % 85 + 33);
77 if(x >= 3) s << static_cast<char>(v / 614125 % 85 + 33);
78 if(x >= 2) s << static_cast<char>(v / 7225 % 85 + 33);
79 if(x >= 1) s << static_cast<char>(v / 85 % 85 + 33);
80 if(x >= 1) s << static_cast<char>(v % 85 + 33);
81 chars -= (x + 1);
84 std::pair<std::string, size_t> base_encode(const std::vector<char>& data, size_t ptr, size_t chars)
86 std::ostringstream s;
87 while(chars >= 5 && ptr + 4 <= data.size())
88 blockcode<4>(s, data, ptr, chars);
89 if(chars >= 4 && ptr + 3 <= data.size())
90 blockcode<3>(s, data, ptr, chars);
91 if(chars >= 3 && ptr + 2 <= data.size())
92 blockcode<2>(s, data, ptr, chars);
93 if(chars >= 2 && ptr + 1 <= data.size())
94 blockcode<1>(s, data, ptr, chars);
95 return std::make_pair(s.str(), ptr);
97 std::string eq_escape(const std::string& str)
99 std::ostringstream s;
100 size_t len = str.length();
101 for(size_t i = 0; i < len; i++) {
102 if(str[i] == '\\')
103 s << "\\\\";
104 else if(str[i] == '=')
105 s << "\\e";
106 else
107 s << str[i];
109 return s.str();
112 std::string eq_unescape(const std::string& str)
114 std::ostringstream s;
115 size_t len = str.length();
116 bool escape = false;
117 for(size_t i = 0; i < len; i++) {
118 if(escape) {
119 if(str[i] == 'e')
120 s << "=";
121 else
122 s << str[i];
123 escape = false;
124 } else {
125 if(str[i] == '\\')
126 escape = true;
127 else
128 s << str[i];
131 return s.str();
134 void save_binary(std::ostream& s, const std::string& key, const std::vector<char>& value)
136 size_t ptr = 0;
137 while(ptr < value.size()) {
138 auto v = base_encode(value, ptr, 60);
139 s << key << "=" << v.first << std::endl;
140 ptr = v.second;
144 void fill_stub_movie(struct moviefile& m, struct project_info& p, struct core_type& coretype)
146 //Create a dummy movie.
147 m.lazy_project_create = false;
148 m.start_paused = true;
149 m.movie_rtc_second = p.movie_rtc_second;
150 m.movie_rtc_subsecond = p.movie_rtc_subsecond;
151 m.save_frame = 0;
152 m.lagged_frames = 0;
153 m.anchor_savestate = p.anchor_savestate;
154 m.movie_sram = p.movie_sram;
155 m.authors = p.authors;
156 for(unsigned i = 0; i < ROM_SLOT_COUNT; i++) {
157 m.romimg_sha256[i] = p.romimg_sha256[i];
158 m.romxml_sha256[i] = p.romxml_sha256[i];
159 m.namehint[i] = p.namehint[i];
161 m.projectid = p.projectid;
162 m.coreversion = p.coreversion;
163 m.gamename = p.gamename;
164 m.settings = p.settings;
165 auto ctrldata = coretype.controllerconfig(m.settings);
166 port_type_set& ports = port_type_set::make(ctrldata.ports, ctrldata.portindex());
167 m.create_default_branch(ports);
168 try {
169 m.gametype = &coretype.lookup_sysregion(p.gametype);
170 } catch(std::bad_alloc& e) {
171 throw;
172 } catch(std::exception& e) {
173 throw std::runtime_error("Illegal game type '" + p.gametype + "'");
177 std::string project_getname(const std::string& id)
179 std::string file = get_config_path() + "/" + id + ".prj";
180 std::ifstream f(file);
181 if(!f)
182 throw std::runtime_error("Can't open project file");
183 std::string name;
184 std::getline(f, name);
185 if(!f)
186 throw std::runtime_error("Can't read project name");
187 return name;
191 project_info& project_load(const std::string& id)
193 std::string file = get_config_path() + "/" + id + ".prj";
194 std::ifstream f(file);
195 if(!f)
196 throw std::runtime_error("Can't open project file");
197 project_info& pi = *new project_info();
198 pi.id = id;
199 pi.movie_rtc_second = 1000000000;
200 pi.movie_rtc_subsecond = 0;
201 pi.active_branch = 0;
202 pi.next_branch = 0;
203 pi.filename = file;
204 //First line is always project name.
205 std::getline(f, pi.name);
206 if(!f || pi.name == "") {
207 delete &pi;
208 throw std::runtime_error("Can't read project file");
210 while(f) {
211 std::string tmp;
212 std::getline(f, tmp);
213 regex_results r;
214 if(r = regex("rom=(.+)", tmp))
215 pi.rom = r[1];
216 else if(r = regex("last-save=(.+)", tmp))
217 pi.last_save = r[1];
218 else if(r = regex("directory=(.+)", tmp))
219 pi.directory = r[1];
220 else if(r = regex("prefix=(.+)", tmp))
221 pi.prefix = r[1];
222 else if(r = regex("luascript=(.+)", tmp))
223 pi.luascripts.push_back(r[1]);
224 else if(r = regex("gametype=(.+)", tmp))
225 pi.gametype = r[1];
226 else if(r = regex("coreversion=(.+)", tmp))
227 pi.coreversion = r[1];
228 else if(r = regex("gamename=(.+)", tmp))
229 pi.gamename = r[1];
230 else if(r = regex("projectid=(.+)", tmp))
231 pi.projectid = r[1];
232 else if(r = regex("projectid=([0-9]+):([0-9]+)", tmp)) {
233 pi.movie_rtc_second = parse_value<int64_t>(r[1]);
234 pi.movie_rtc_subsecond = parse_value<int64_t>(r[2]);
235 } else if(r = regex("author=(.*)\\|(.*)", tmp))
236 pi.authors.push_back(std::make_pair(r[1], r[2]));
237 else if(r = regex("author=(.+)", tmp))
238 pi.authors.push_back(std::make_pair(r[1], ""));
239 else if(r = regex("romsha=([0-9a-f]+)", tmp))
240 pi.romimg_sha256[0] = r[1];
241 else if(r = regex("slotsha([a-z])=([0-9a-f]+)", tmp))
242 pi.romimg_sha256[r[1][0] - 96] = r[2];
243 else if(r = regex("romxml=([0-9a-f]+)", tmp))
244 pi.romxml_sha256[0] = r[1];
245 else if(r = regex("slotxml([a-z])=([0-9a-f]+)", tmp))
246 pi.romxml_sha256[r[1][0] - 96] = r[2];
247 else if(r = regex("romhint=(.*)", tmp))
248 pi.namehint[0] = r[1];
249 else if(r = regex("slothint([a-z])=(.*)", tmp))
250 pi.namehint[r[1][0] - 96] = r[2];
251 else if(r = regex("romrom=(.*)", tmp))
252 pi.roms[0] = r[1];
253 else if(r = regex("slotrom([a-z])=(.*)", tmp))
254 pi.roms[r[1][0] - 96] = r[2];
255 else if(r = regex("setting.([^=]+)=(.*)", tmp))
256 pi.settings[r[1]] = r[2];
257 else if(r = regex("watch.([^=]+)=(.*)", tmp))
258 pi.watches[eq_unescape(r[1])] = r[2];
259 else if(r = regex("sram.([^=]+)=(.*)", tmp))
260 concatenate(pi.movie_sram[r[1]], base_decode(r[2]));
261 else if(r = regex("macro.([^=]+)=(.*)", tmp))
262 try {
263 pi.macros[r[1]] = JSON::node(r[2]);
264 } catch(std::exception& e) {
265 messages << "Unable to load macro '" << r[1] << "': " << e.what() << std::endl;
267 else if(r = regex("anchor=(.*)", tmp))
268 concatenate(pi.anchor_savestate, base_decode(r[1]));
269 else if(r = regex("time=([0-9]+):([0-9]+)", tmp)) {
270 pi.movie_rtc_second = parse_value<int64_t>(r[1]);
271 pi.movie_rtc_subsecond = parse_value<int64_t>(r[2]);
272 } else if(r = regex("branch([1-9][0-9]*)parent=([0-9]+)", tmp)) {
273 uint64_t bid = parse_value<int64_t>(r[1]);
274 uint64_t pbid = parse_value<int64_t>(r[2]);
275 if(!pi.branches.count(bid))
276 pi.branches[bid].name = "(Unnamed branch)";
277 pi.branches[bid].pbid = pbid;
278 } else if(r = regex("branch([1-9][0-9]*)name=(.*)", tmp)) {
279 uint64_t bid = parse_value<int64_t>(r[1]);
280 if(!pi.branches.count(bid))
281 pi.branches[bid].pbid = 0;
282 pi.branches[bid].name = r[2];
283 } else if(r = regex("branchcurrent=([0-9]+)", tmp)) {
284 pi.active_branch = parse_value<int64_t>(r[1]);
285 } else if(r = regex("branchnext=([0-9]+)", tmp)) {
286 pi.next_branch = parse_value<int64_t>(r[1]);
289 for(auto& i : pi.branches) {
290 uint64_t j = i.first;
291 uint64_t m = j;
292 while(j) {
293 j = pi.branches[j].pbid;
294 m = min(m, j);
295 if(j == i.first) {
296 //Cyclic dependency!
297 messages << "Warning: Cyclic slot branch dependency, reparenting '" <<
298 pi.branches[m].name << "' to be child of root..." << std::endl;
299 pi.branches[j].pbid = 0;
300 break;
304 if(pi.active_branch && !pi.branches.count(pi.active_branch)) {
305 messages << "Warning: Current slot branch does not exist, using root..." << std::endl;
306 pi.active_branch = 0;
308 return pi;
311 project_info* project_get()
313 return active_project;
316 bool project_set(project_info* p, bool current)
318 if(!p) {
319 if(active_project)
320 voicesub_unload_collection();
321 active_project = p;
322 notify_core_change();
323 notify_branch_change();
324 return true;
327 loaded_rom newrom;
328 moviefile* newmovie = NULL;
329 bool switched = false;
330 std::set<core_sysregion*> sysregs;
331 bool used = false;
332 try {
333 if(current)
334 goto skip_rom_movie;
336 sysregs = core_sysregion::find_matching(p->gametype);
337 if(sysregs.empty())
338 throw std::runtime_error("No core supports '" + p->gametype + "'");
340 //First, try to load the ROM and the last movie file into RAM...
341 if(p->rom != "") {
342 newrom = loaded_rom(p->rom, p->coreversion);
343 } else {
344 core_type* ctype = NULL;
345 for(auto i : sysregs) {
346 ctype = &i->get_type();
347 if(ctype->get_core_identifier() == p->coreversion)
348 break;
350 newrom = loaded_rom(p->roms, ctype->get_core_identifier(), ctype->get_iname(), "");
352 if(newrom.rtype->get_core_identifier() != p->coreversion) {
353 messages << "Warning: Can't find matching core, using " << newrom.rtype->get_core_identifier()
354 << std::endl;
356 if(p->last_save != "")
357 try {
358 newmovie = new moviefile(p->last_save, *newrom.rtype);
359 } catch(std::exception& e) {
360 messages << "Warning: Can't load last save: " << e.what() << std::endl;
361 newmovie = new moviefile();
362 fill_stub_movie(*newmovie, *p, *newrom.rtype);
364 else {
365 newmovie = new moviefile();
366 fill_stub_movie(*newmovie, *p, *newrom.rtype);
368 //Okay, loaded, load into core.
369 newrom.load(p->settings, p->movie_rtc_second, p->movie_rtc_subsecond);
370 our_rom = newrom;
371 do_load_state(*newmovie, LOAD_STATE_DEFAULT, used);
372 skip_rom_movie:
373 active_project = p;
374 switched = true;
375 //Calculate union of old and new.
376 std::set<std::string> _watches = lsnes_memorywatch.enumerate();
377 for(auto i : p->watches) _watches.insert(i.first);
379 for(auto i : _watches)
380 try {
381 if(p->watches.count(i))
382 lsnes_memorywatch.set(i, p->watches[i]);
383 else
384 lsnes_memorywatch.clear(i);
385 } catch(std::exception& e) {
386 messages << "Can't set/clear watch '" << i << "': " << e.what() << std::endl;
388 voicesub_load_collection(p->directory + "/" + p->prefix + ".lsvs");
389 lsnes_cmd.invoke("reset-lua");
390 for(auto i : p->luascripts)
391 lsnes_cmd.invoke("run-lua " + i);
392 load_project_macros(controls, *active_project);
393 } catch(std::exception& e) {
394 if(newmovie && !used)
395 delete newmovie;
396 messages << "Can't switch projects: " << e.what() << std::endl;
398 if(switched) {
399 do_flush_slotinfo();
400 update_movie_state();
401 notify_core_change();
402 notify_branch_change();
404 return switched;
407 std::map<std::string, std::string> project_enumerate()
409 std::set<std::string> projects;
410 std::map<std::string, std::string> projects2;
412 projects = enumerate_directory(get_config_path(), ".*\\.prj");
413 for(auto i : projects) {
414 std::string id = i;
415 size_t split;
416 #ifdef FUCKED_SYSTEM
417 split = id.find_last_of("\\/");
418 #else
419 split = id.find_last_of("/");
420 #endif
421 if(split < id.length())
422 id = id.substr(split + 1);
423 id = id.substr(0, id.length() - 4);
424 try {
425 projects2[id] = project_getname(id);
426 } catch(...) {
427 messages << "Failed to load name for ID '" << id << "'" << std::endl;
430 return projects2;
433 std::string project_moviepath()
435 if(active_project)
436 return active_project->directory;
437 else
438 return lsnes_vset["moviepath"].str();
441 std::string project_otherpath()
443 if(active_project)
444 return active_project->directory;
445 else
446 return ".";
449 std::string project_savestate_ext()
451 return active_project ? "lss" : "lsmv";
454 void project_copy_watches(project_info& p)
456 for(auto i : lsnes_memorywatch.enumerate()) {
457 try {
458 p.watches[i] = lsnes_memorywatch.get_string(i);
459 } catch(std::exception& e) {
460 messages << "Can't read memory watch '" << i << "': " << e.what() << std::endl;
465 void project_copy_macros(project_info& p, controller_state& s)
467 for(auto i : s.enumerate_macro())
468 p.macros[i] = s.get_macro(i).serialize();
471 uint64_t project_info::get_parent_branch(uint64_t bid)
473 if(!bid)
474 return 0;
475 if(!branches.count(bid))
476 throw std::runtime_error("Invalid branch ID");
477 return branches[bid].pbid;
480 void project_info::set_current_branch(uint64_t bid)
482 if(bid && !branches.count(bid))
483 throw std::runtime_error("Invalid branch ID");
484 active_branch = bid;
485 notify_branch_change();
486 messages << "Set current slot branch to " << get_branch_string() << std::endl;
489 const std::string& project_info::get_branch_name(uint64_t bid)
491 static std::string rootname = "(root)";
492 if(!bid)
493 return rootname;
494 if(!branches.count(bid))
495 throw std::runtime_error("Invalid branch ID");
496 return branches[bid].name;
499 void project_info::set_branch_name(uint64_t bid, const std::string& name)
501 if(!bid)
502 throw std::runtime_error("Root branch name can't be set");
503 if(!branches.count(bid))
504 throw std::runtime_error("Invalid branch ID");
505 branches[bid].name = name;
506 notify_branch_change();
509 void project_info::set_parent_branch(uint64_t bid, uint64_t pbid)
511 if(!bid)
512 throw std::runtime_error("Root branch never has parent");
513 if(!branches.count(bid))
514 throw std::runtime_error("Invalid branch ID");
515 if(pbid && !branches.count(pbid))
516 throw std::runtime_error("Invalid parent branch ID");
517 for(auto& i : branches) {
518 uint64_t j = i.first;
519 while(j) {
520 j = (j == bid) ? pbid : branches[j].pbid;
521 if(j == i.first)
522 throw std::runtime_error("Reparenting would create a circular dependency");
525 branches[bid].pbid = pbid;
526 notify_branch_change();
529 std::set<uint64_t> project_info::branch_children(uint64_t bid)
531 if(bid && !branches.count(bid))
532 throw std::runtime_error("Invalid branch ID");
533 std::set<uint64_t> r;
534 for(auto& i : branches)
535 if(i.second.pbid == bid)
536 r.insert(i.first);
537 return r;
540 uint64_t project_info::create_branch(uint64_t pbid, const std::string& name)
542 if(pbid && !branches.count(pbid))
543 throw std::runtime_error("Invalid parent branch ID");
544 uint64_t assign_bid = next_branch;
545 uint64_t last_bid = (branches.empty() ? 1 : branches.rbegin()->first + 1);
546 assign_bid = max(assign_bid, last_bid);
547 branches[assign_bid].name = name;
548 branches[assign_bid].pbid = pbid;
549 next_branch = assign_bid + 1;
550 notify_branch_change();
551 return assign_bid;
554 void project_info::delete_branch(uint64_t bid)
556 if(!bid)
557 throw std::runtime_error("Root branch can not be deleted");
558 if(bid == active_branch)
559 throw std::runtime_error("Current branch can't be deleted");
560 if(!branches.count(bid))
561 throw std::runtime_error("Invalid branch ID");
562 for(auto& i : branches)
563 if(i.second.pbid == bid)
564 throw std::runtime_error("Can't delete branch with children");
565 branches.erase(bid);
566 notify_branch_change();
569 std::string project_info::get_branch_string()
571 std::string r;
572 uint64_t j = active_branch;
573 if(!j)
574 return "(root)";
575 while(j) {
576 if(r == "")
577 r = get_branch_name(j);
578 else
579 r = get_branch_name(j) + "→" + r;
580 j = get_parent_branch(j);
582 return r;
585 void project_info::flush()
587 std::string file = get_config_path() + "/" + id + ".prj";
588 std::string tmpfile = get_config_path() + "/" + id + ".prj.tmp";
589 std::string bakfile = get_config_path() + "/" + id + ".prj.bak";
590 std::ofstream f(tmpfile);
591 if(!f)
592 throw std::runtime_error("Can't write project file");
593 write(f);
594 if(!f)
595 throw std::runtime_error("Can't write project file");
596 f.close();
598 std::ifstream f2(file);
599 if(f2) {
600 std::ofstream f3(bakfile);
601 if(!f3)
602 throw std::runtime_error("Can't backup project file");
603 while(f2) {
604 std::string tmp;
605 std::getline(f2, tmp);
606 f3 << tmp << std::endl;
608 f2.close();
609 f3.close();
611 #if defined(_WIN32) || defined(_WIN64) || defined(TEST_WIN32_CODE)
612 if(MoveFileEx(tmpfile.c_str(), file.c_str(), MOVEFILE_REPLACE_EXISTING) < 0)
613 #else
614 if(rename(tmpfile.c_str(), file.c_str()) < 0)
615 #endif
616 throw std::runtime_error("Can't replace project file");
619 void project_info::write(std::ostream& s)
621 s << name << std::endl;
622 s << "rom=" << rom << std::endl;
623 if(last_save != "")
624 s << "last-save=" << last_save << std::endl;
625 s << "directory=" << directory << std::endl;
626 s << "prefix=" << prefix << std::endl;
627 for(auto i : luascripts)
628 s << "luascript=" << i << std::endl;
629 s << "gametype=" << gametype << std::endl;
630 s << "coreversion=" << coreversion << std::endl;
631 if(gamename != "")
632 s << "gamename=" << gamename << std::endl;
633 s << "projectid=" << projectid << std::endl;
634 s << "time=" << movie_rtc_second << ":" << movie_rtc_subsecond << std::endl;
635 for(auto i : authors)
636 s << "author=" << i.first << "|" << i.second << std::endl;
637 for(unsigned i = 0; i < ROM_SLOT_COUNT; i++) {
638 if(romimg_sha256[i] != "") {
639 if(i)
640 s << "slotsha" << static_cast<char>(96 + i) << "=" << romimg_sha256[i] << std::endl;
641 else
642 s << "romsha=" << romimg_sha256[i] << std::endl;
644 if(romxml_sha256[i] != "") {
645 if(i)
646 s << "slotxml" << static_cast<char>(96 + i) << "=" << romxml_sha256[i] << std::endl;
647 else
648 s << "romxml=" << romxml_sha256[i] << std::endl;
650 if(namehint[i] != "") {
651 if(i)
652 s << "slothint" << static_cast<char>(96 + i) << "=" << namehint[i] << std::endl;
653 else
654 s << "romhint=" << namehint[i] << std::endl;
656 if(roms[i] != "") {
657 if(i)
658 s << "slotrom" << static_cast<char>(96 + i) << "=" << roms[i] << std::endl;
659 else
660 s << "romrom=" << roms[i] << std::endl;
663 for(auto i : settings)
664 s << "setting." << i.first << "=" << i.second << std::endl;
665 for(auto i : watches)
666 s << "watch." << eq_escape(i.first) << "=" << i.second << std::endl;
667 for(auto i : macros)
668 s << "macro." + i.first << "=" << i.second.serialize() << std::endl;
669 for(auto i : movie_sram)
670 save_binary(s, "sram." + i.first, i.second);
671 if(anchor_savestate.size())
672 save_binary(s, "anchor", anchor_savestate);
673 for(auto& i : branches) {
674 s << "branch" << i.first << "parent=" << i.second.pbid << std::endl;
675 s << "branch" << i.first << "name=" << i.second.name << std::endl;
677 s << "branchcurrent=" << active_branch << std::endl;
678 s << "branchnext=" << next_branch << std::endl;
681 namespace
683 void recursive_list_branch(uint64_t bid, std::set<unsigned>& dset, unsigned depth, bool last_of)
685 if(!active_project) {
686 messages << "Not in project context." << std::endl;
687 return;
689 std::set<uint64_t> children = active_project->branch_children(bid);
690 std::string prefix;
691 for(unsigned i = 0; i + 1 < depth; i++)
692 prefix += (dset.count(i) ? "\u2502" : " ");
693 prefix += (dset.count(depth - 1) ? (last_of ? "\u2514" : "\u251c") : " ");
694 if(last_of) dset.erase(depth - 1);
695 messages << prefix
696 << ((bid == active_project->get_current_branch()) ? "*" : "")
697 << bid << ":" << active_project->get_branch_name(bid) << std::endl;
698 dset.insert(depth);
699 size_t c = 0;
700 for(auto i : children) {
701 bool last = (++c == children.size());
702 recursive_list_branch(i, dset, depth + 1, last);
704 dset.erase(depth);
707 command::fnptr<> list_branches(lsnes_cmd, "list-branches", "List all slot branches",
708 "Syntax: list-branches\nList all slot branches.\n",
709 []() throw(std::bad_alloc, std::runtime_error) {
710 std::set<unsigned> dset;
711 recursive_list_branch(0, dset, 0, false);
714 command::fnptr<const std::string&> create_branch(lsnes_cmd, "create-branch", "Create a new slot branch",
715 "Syntax: create-branch <parentid> <name>\nCreate new branch named <name> under <parentid>.\n",
716 [](const std::string& args) throw(std::bad_alloc, std::runtime_error) {
717 regex_results r = regex("([0-9]+)[ \t]+(.*)", args);
718 if(!r) {
719 messages << "Syntax: create-branch <parentid> <name>" << std::endl;
720 return;
722 try {
723 uint64_t pbid = parse_value<uint64_t>(r[1]);
724 if(!active_project)
725 throw std::runtime_error("Not in project context");
726 uint64_t bid = active_project->create_branch(pbid, r[2]);
727 messages << "Created branch #" << bid << std::endl;
728 active_project->flush();
729 } catch(std::exception& e) {
730 messages << "Can't create new branch: " << e.what() << std::endl;
734 command::fnptr<const std::string&> delete_branch(lsnes_cmd, "delete-branch", "Delete a slot branch",
735 "Syntax: delete-branch <id>\nDelete slot branch with id <id>.\n",
736 [](const std::string& args) throw(std::bad_alloc, std::runtime_error) {
737 regex_results r = regex("([0-9]+)[ \t]*", args);
738 if(!r) {
739 messages << "Syntax: delete-branch <id>" << std::endl;
740 return;
742 try {
743 uint64_t bid = parse_value<uint64_t>(r[1]);
744 if(!active_project)
745 throw std::runtime_error("Not in project context");
746 active_project->delete_branch(bid);
747 messages << "Deleted branch #" << bid << std::endl;
748 active_project->flush();
749 } catch(std::exception& e) {
750 messages << "Can't delete branch: " << e.what() << std::endl;
754 command::fnptr<const std::string&> set_branch(lsnes_cmd, "set-branch", "Set current slot branch",
755 "Syntax: set-branch <id>\nSet current branch to <id>.\n",
756 [](const std::string& args) throw(std::bad_alloc, std::runtime_error) {
757 regex_results r = regex("([0-9]+)[ \t]*", args);
758 if(!r) {
759 messages << "Syntax: set-branch <id>" << std::endl;
760 return;
762 try {
763 uint64_t bid = parse_value<uint64_t>(r[1]);
764 if(!active_project)
765 throw std::runtime_error("Not in project context");
766 active_project->set_current_branch(bid);
767 messages << "Set current branch to #" << bid << std::endl;
768 active_project->flush();
769 update_movie_state();
770 } catch(std::exception& e) {
771 messages << "Can't set branch: " << e.what() << std::endl;
775 command::fnptr<const std::string&> reparent_branch(lsnes_cmd, "reparent-branch", "Reparent a slot branch",
776 "Syntax: reparent-branch <id> <newpid>\nReparent branch <id> to be child of <newpid>.\n",
777 [](const std::string& args) throw(std::bad_alloc, std::runtime_error) {
778 regex_results r = regex("([0-9]+)[ \t]+([0-9]+)[ \t]*", args);
779 if(!r) {
780 messages << "Syntax: reparent-branch <id> <newpid>" << std::endl;
781 return;
783 try {
784 uint64_t bid = parse_value<uint64_t>(r[1]);
785 uint64_t pbid = parse_value<uint64_t>(r[2]);
786 if(!active_project)
787 throw std::runtime_error("Not in project context");
788 active_project->set_parent_branch(bid, pbid);
789 messages << "Reparented branch #" << bid << std::endl;
790 active_project->flush();
791 update_movie_state();
792 } catch(std::exception& e) {
793 messages << "Can't reparent branch: " << e.what() << std::endl;
797 command::fnptr<const std::string&> rename_branch(lsnes_cmd, "rename-branch", "Rename a slot branch",
798 "Syntax: rename-branch <id> <name>\nRename branch <id> to <name>.\n",
799 [](const std::string& args) throw(std::bad_alloc, std::runtime_error) {
800 regex_results r = regex("([0-9]+)[ \t]+(.*)", args);
801 if(!r) {
802 messages << "Syntax: rename-branch <id> <name>" << std::endl;
803 return;
805 try {
806 uint64_t bid = parse_value<uint64_t>(r[1]);
807 if(!active_project)
808 throw std::runtime_error("Not in project context");
809 active_project->set_branch_name(bid, r[2]);
810 messages << "Renamed branch #" << bid << std::endl;
811 active_project->flush();
812 update_movie_state();
813 } catch(std::exception& e) {
814 messages << "Can't rename branch: " << e.what() << std::endl;