JSON-based controller descriptions
[lsnes.git] / src / core / project.cpp
blob38cbaaf153888b3436305a33d0511549f503379e
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/string.hpp"
15 #include <fstream>
16 #include <dirent.h>
17 #if defined(_WIN32) || defined(_WIN64) || defined(TEST_WIN32_CODE)
18 #define FUCKED_SYSTEM
19 #include <windows.h>
20 #endif
22 void do_flush_slotinfo();
24 namespace
26 project_info* active_project = NULL;
28 void concatenate(std::vector<char>& data, const std::vector<char>& app)
30 size_t dsize = data.size();
31 data.resize(dsize + app.size());
32 std::copy(app.begin(), app.end(), data.begin() + dsize);
35 std::vector<char> base_decode(const std::string& str)
37 std::vector<char> r;
38 size_t len = str.length();
39 size_t i;
40 uint32_t a, b, c, d, e, v;
41 for(i = 0; i + 5 <= len; i += 5) {
42 a = str[i + 0] - 33;
43 b = str[i + 1] - 33;
44 c = str[i + 2] - 33;
45 d = str[i + 3] - 33;
46 e = str[i + 4] - 33;
47 v = 52200625 * a + 614125 * b + 7225 * c + 85 * d + e;
48 r.push_back(v);
49 r.push_back(v >> 8);
50 r.push_back(v >> 16);
51 r.push_back(v >> 24);
53 a = b = c = d = e = 0;
54 if(i + 0 < len) e = str[len - 1] - 33;
55 if(i + 1 < len) d = str[len - 2] - 33;
56 if(i + 2 < len) c = str[len - 3] - 33;
57 if(i + 3 < len) b = str[len - 4] - 33;
58 v = 614125 * b + 7225 * c + 85 * d + e;
59 if(i + 1 < len) r.push_back(v);
60 if(i + 2 < len) r.push_back(v >> 8);
61 if(i + 3 < len) r.push_back(v >> 16);
62 return r;
65 template<int x> void blockcode(std::ostringstream& s, const std::vector<char>& data, size_t& ptr,
66 size_t& chars)
68 uint32_t a = 0, b = 0, c = 0, d = 0, v;
69 if(x >= 4) a = static_cast<uint8_t>(data[ptr++]);
70 if(x >= 3) b = static_cast<uint8_t>(data[ptr++]);
71 if(x >= 2) c = static_cast<uint8_t>(data[ptr++]);
72 if(x >= 1) d = static_cast<uint8_t>(data[ptr++]);
73 v = 16777216 * d + 65536 * c + 256 * b + a;
74 v >>= (32 - 8 * x);
75 if(x >= 4) s << static_cast<char>(v / 52200625 % 85 + 33);
76 if(x >= 3) s << static_cast<char>(v / 614125 % 85 + 33);
77 if(x >= 2) s << static_cast<char>(v / 7225 % 85 + 33);
78 if(x >= 1) s << static_cast<char>(v / 85 % 85 + 33);
79 if(x >= 1) s << static_cast<char>(v % 85 + 33);
80 chars -= (x + 1);
83 std::pair<std::string, size_t> base_encode(const std::vector<char>& data, size_t ptr, size_t chars)
85 std::ostringstream s;
86 while(chars >= 5 && ptr + 4 <= data.size())
87 blockcode<4>(s, data, ptr, chars);
88 if(chars >= 4 && ptr + 3 <= data.size())
89 blockcode<3>(s, data, ptr, chars);
90 if(chars >= 3 && ptr + 2 <= data.size())
91 blockcode<2>(s, data, ptr, chars);
92 if(chars >= 2 && ptr + 1 <= data.size())
93 blockcode<1>(s, data, ptr, chars);
94 return std::make_pair(s.str(), ptr);
96 std::string eq_escape(const std::string& str)
98 std::ostringstream s;
99 size_t len = str.length();
100 for(size_t i = 0; i < len; i++) {
101 if(str[i] == '\\')
102 s << "\\\\";
103 else if(str[i] == '=')
104 s << "\\e";
105 else
106 s << str[i];
108 return s.str();
111 std::string eq_unescape(const std::string& str)
113 std::ostringstream s;
114 size_t len = str.length();
115 bool escape = false;
116 for(size_t i = 0; i < len; i++) {
117 if(escape) {
118 if(str[i] == 'e')
119 s << "=";
120 else
121 s << str[i];
122 escape = false;
123 } else {
124 if(str[i] == '\\')
125 escape = true;
126 else
127 s << str[i];
130 return s.str();
133 void save_binary(std::ostream& s, const std::string& key, const std::vector<char>& value)
135 size_t ptr = 0;
136 while(ptr < value.size()) {
137 auto v = base_encode(value, ptr, 60);
138 s << key << "=" << v.first << std::endl;
139 ptr = v.second;
143 void project_write(std::ostream& s, project_info& p)
145 s << p.name << std::endl;
146 s << "rom=" << p.rom << std::endl;
147 if(p.last_save != "")
148 s << "last-save=" << p.last_save << std::endl;
149 s << "directory=" << p.directory << std::endl;
150 s << "prefix=" << p.prefix << std::endl;
151 for(auto i : p.luascripts)
152 s << "luascript=" << i << std::endl;
153 s << "gametype=" << p.gametype << std::endl;
154 s << "coreversion=" << p.coreversion << std::endl;
155 if(p.gamename != "")
156 s << "gamename=" << p.gamename << std::endl;
157 s << "projectid=" << p.projectid << std::endl;
158 s << "time=" << p.movie_rtc_second << ":" << p.movie_rtc_subsecond << std::endl;
159 for(auto i : p.authors)
160 s << "author=" << i.first << "|" << i.second << std::endl;
161 for(unsigned i = 0; i < ROM_SLOT_COUNT; i++) {
162 if(p.romimg_sha256[i] != "") {
163 if(i)
164 s << "slotsha" << static_cast<char>(96 + i) << "=" << p.romimg_sha256[i]
165 << std::endl;
166 else
167 s << "romsha=" << p.romimg_sha256[i] << std::endl;
169 if(p.romxml_sha256[i] != "") {
170 if(i)
171 s << "slotxml" << static_cast<char>(96 + i) << "=" << p.romxml_sha256[i]
172 << std::endl;
173 else
174 s << "romxml=" << p.romxml_sha256[i] << std::endl;
176 if(p.namehint[i] != "") {
177 if(i)
178 s << "slothint" << static_cast<char>(96 + i) << "=" << p.namehint[i]
179 << std::endl;
180 else
181 s << "romhint=" << p.namehint[i] << std::endl;
183 if(p.roms[i] != "") {
184 if(i)
185 s << "slotrom" << static_cast<char>(96 + i) << "=" << p.roms[i]
186 << std::endl;
187 else
188 s << "romrom=" << p.roms[i] << std::endl;
191 for(auto i : p.settings)
192 s << "setting." << i.first << "=" << i.second << std::endl;
193 for(auto i : p.watches)
194 s << "watch." << eq_escape(i.first) << "=" << i.second << std::endl;
195 for(auto i : p.macros)
196 s << "macro." + i.first << "=" << i.second.serialize() << std::endl;
197 for(auto i : p.movie_sram)
198 save_binary(s, "sram." + i.first, i.second);
199 if(p.anchor_savestate.size())
200 save_binary(s, "anchor", p.anchor_savestate);
203 void fill_stub_movie(struct moviefile& m, struct project_info& p, struct core_type& coretype)
205 //Create a dummy movie.
206 m.lazy_project_create = false;
207 m.start_paused = true;
208 m.movie_rtc_second = p.movie_rtc_second;
209 m.movie_rtc_subsecond = p.movie_rtc_subsecond;
210 m.save_frame = 0;
211 m.lagged_frames = 0;
212 m.anchor_savestate = p.anchor_savestate;
213 m.movie_sram = p.movie_sram;
214 m.authors = p.authors;
215 for(unsigned i = 0; i < ROM_SLOT_COUNT; i++) {
216 m.romimg_sha256[i] = p.romimg_sha256[i];
217 m.romxml_sha256[i] = p.romxml_sha256[i];
218 m.namehint[i] = p.namehint[i];
220 m.projectid = p.projectid;
221 m.coreversion = p.coreversion;
222 m.gamename = p.gamename;
223 m.settings = p.settings;
224 auto ctrldata = coretype.controllerconfig(m.settings);
225 port_type_set& ports = port_type_set::make(ctrldata.ports, ctrldata.portindex());
226 m.input.clear(ports);
227 try {
228 m.gametype = &coretype.lookup_sysregion(p.gametype);
229 } catch(std::bad_alloc& e) {
230 throw;
231 } catch(std::exception& e) {
232 throw std::runtime_error("Illegal game type '" + p.gametype + "'");
236 std::string project_getname(const std::string& id)
238 std::string file = get_config_path() + "/" + id + ".prj";
239 std::ifstream f(file);
240 if(!f)
241 throw std::runtime_error("Can't open project file");
242 std::string name;
243 std::getline(f, name);
244 if(!f)
245 throw std::runtime_error("Can't read project name");
246 return name;
250 project_info& project_load(const std::string& id)
252 std::string file = get_config_path() + "/" + id + ".prj";
253 std::ifstream f(file);
254 if(!f)
255 throw std::runtime_error("Can't open project file");
256 project_info& pi = *new project_info();
257 pi.id = id;
258 //First line is always project name.
259 std::getline(f, pi.name);
260 if(!f || pi.name == "") {
261 delete &pi;
262 throw std::runtime_error("Can't read project file");
264 while(f) {
265 std::string tmp;
266 std::getline(f, tmp);
267 regex_results r;
268 if(r = regex("rom=(.+)", tmp))
269 pi.rom = r[1];
270 else if(r = regex("last-save=(.+)", tmp))
271 pi.last_save = r[1];
272 else if(r = regex("directory=(.+)", tmp))
273 pi.directory = r[1];
274 else if(r = regex("prefix=(.+)", tmp))
275 pi.prefix = r[1];
276 else if(r = regex("luascript=(.+)", tmp))
277 pi.luascripts.push_back(r[1]);
278 else if(r = regex("gametype=(.+)", tmp))
279 pi.gametype = r[1];
280 else if(r = regex("coreversion=(.+)", tmp))
281 pi.coreversion = r[1];
282 else if(r = regex("gamename=(.+)", tmp))
283 pi.gamename = r[1];
284 else if(r = regex("projectid=(.+)", tmp))
285 pi.projectid = r[1];
286 else if(r = regex("projectid=([0-9]+):([0-9]+)", tmp)) {
287 pi.movie_rtc_second = parse_value<int64_t>(r[1]);
288 pi.movie_rtc_subsecond = parse_value<int64_t>(r[2]);
289 } else if(r = regex("author=(.*)\\|(.*)", tmp))
290 pi.authors.push_back(std::make_pair(r[1], r[2]));
291 else if(r = regex("author=(.+)", tmp))
292 pi.authors.push_back(std::make_pair(r[1], ""));
293 else if(r = regex("romsha=([0-9a-f]+)", tmp))
294 pi.romimg_sha256[0] = r[1];
295 else if(r = regex("slotsha([a-z])=([0-9a-f]+)", tmp))
296 pi.romimg_sha256[r[1][0] - 96] = r[2];
297 else if(r = regex("romxml=([0-9a-f]+)", tmp))
298 pi.romxml_sha256[0] = r[1];
299 else if(r = regex("slotxml([a-z])=([0-9a-f]+)", tmp))
300 pi.romxml_sha256[r[1][0] - 96] = r[2];
301 else if(r = regex("romhint=(.*)", tmp))
302 pi.namehint[0] = r[1];
303 else if(r = regex("slothint([a-z])=(.*)", tmp))
304 pi.namehint[r[1][0] - 96] = r[2];
305 else if(r = regex("romrom=(.*)", tmp))
306 pi.roms[0] = r[1];
307 else if(r = regex("slotrom([a-z])=(.*)", tmp))
308 pi.roms[r[1][0] - 96] = r[2];
309 else if(r = regex("setting.([^=]+)=(.*)", tmp))
310 pi.settings[r[1]] = r[2];
311 else if(r = regex("watch.([^=]+)=(.*)", tmp))
312 pi.watches[eq_unescape(r[1])] = r[2];
313 else if(r = regex("sram.([^=]+)=(.*)", tmp))
314 concatenate(pi.movie_sram[r[1]], base_decode(r[2]));
315 else if(r = regex("macro.([^=]+)=(.*)", tmp))
316 try {
317 pi.macros[r[1]] = JSON::node(r[2]);
318 } catch(std::exception& e) {
319 messages << "Unable to load macro '" << r[1] << "': " << e.what() << std::endl;
321 else if(r = regex("anchor=(.*)", tmp))
322 concatenate(pi.anchor_savestate, base_decode(r[1]));
323 else if(r = regex("time=([0-9]+):([0-9]+)", tmp)) {
324 pi.movie_rtc_second = parse_value<int64_t>(r[1]);
325 pi.movie_rtc_subsecond = parse_value<int64_t>(r[2]);
328 return pi;
331 void project_flush(project_info* p)
333 if(!p)
334 return;
335 std::string file = get_config_path() + "/" + p->id + ".prj";
336 std::string tmpfile = get_config_path() + "/" + p->id + ".prj.tmp";
337 std::string bakfile = get_config_path() + "/" + p->id + ".prj.bak";
338 std::ofstream f(tmpfile);
339 if(!f)
340 throw std::runtime_error("Can't write project file");
341 project_write(f, *p);
342 if(!f)
343 throw std::runtime_error("Can't write project file");
344 f.close();
346 std::ifstream f2(file);
347 if(f2) {
348 std::ofstream f3(bakfile);
349 if(!f3)
350 throw std::runtime_error("Can't backup project file");
351 while(f2) {
352 std::string tmp;
353 std::getline(f2, tmp);
354 f3 << tmp << std::endl;
356 f2.close();
357 f3.close();
359 #if defined(_WIN32) || defined(_WIN64) || defined(TEST_WIN32_CODE)
360 if(MoveFileEx(tmpfile.c_str(), file.c_str(), MOVEFILE_REPLACE_EXISTING) < 0)
361 #else
362 if(rename(tmpfile.c_str(), file.c_str()) < 0)
363 #endif
364 throw std::runtime_error("Can't replace project file");
367 project_info* project_get()
369 return active_project;
372 bool project_set(project_info* p, bool current)
374 if(!p) {
375 if(active_project)
376 voicesub_unload_collection();
377 active_project = p;
378 notify_core_change();
379 return true;
382 loaded_rom newrom;
383 moviefile newmovie;
384 bool switched = false;
385 std::set<core_sysregion*> sysregs;
386 try {
387 if(current)
388 goto skip_rom_movie;
390 sysregs = core_sysregion::find_matching(p->gametype);
391 if(sysregs.empty())
392 throw std::runtime_error("No core supports '" + p->gametype + "'");
394 //First, try to load the ROM and the last movie file into RAM...
395 if(p->rom != "") {
396 newrom = loaded_rom(p->rom, p->coreversion);
397 } else {
398 core_type* ctype = NULL;
399 for(auto i : sysregs) {
400 ctype = &i->get_type();
401 if(ctype->get_core_identifier() == p->coreversion)
402 break;
404 newrom = loaded_rom(p->roms, ctype->get_core_identifier(), ctype->get_iname(), "");
406 if(newrom.rtype->get_core_identifier() != p->coreversion) {
407 messages << "Warning: Can't find matching core, using " << newrom.rtype->get_core_identifier()
408 << std::endl;
410 if(p->last_save != "")
411 try {
412 newmovie = moviefile(p->last_save, *newrom.rtype);
413 } catch(std::exception& e) {
414 messages << "Warning: Can't load last save: " << e.what() << std::endl;
415 fill_stub_movie(newmovie, *p, *newrom.rtype);
417 else {
418 fill_stub_movie(newmovie, *p, *newrom.rtype);
420 //Okay, loaded, load into core.
421 newrom.load(p->settings, p->movie_rtc_second, p->movie_rtc_subsecond);
422 our_rom = newrom;
423 do_load_state(newmovie, LOAD_STATE_DEFAULT);
424 skip_rom_movie:
425 active_project = p;
426 switched = true;
427 for(auto i : get_watches())
428 if(p->watches.count(i))
429 set_watchexpr_for(i, p->watches[i]);
430 else
431 set_watchexpr_for(i, "");
432 voicesub_load_collection(p->directory + "/" + p->prefix + ".lsvs");
433 lsnes_cmd.invoke("reset-lua");
434 for(auto i : p->luascripts)
435 lsnes_cmd.invoke("run-lua " + i);
436 load_project_macros(controls, *active_project);
437 } catch(std::exception& e) {
438 messages << "Can't switch projects: " << e.what() << std::endl;
440 if(switched) {
441 do_flush_slotinfo();
442 update_movie_state();
443 notify_core_change();
445 return switched;
448 std::map<std::string, std::string> project_enumerate()
450 std::set<std::string> projects;
451 std::map<std::string, std::string> projects2;
453 projects = enumerate_directory(get_config_path(), ".*\\.prj");
454 for(auto i : projects) {
455 std::string id = i;
456 size_t split;
457 #ifdef FUCKED_SYSTEM
458 split = id.find_last_of("\\/");
459 #else
460 split = id.find_last_of("/");
461 #endif
462 if(split < id.length())
463 id = id.substr(split + 1);
464 id = id.substr(0, id.length() - 4);
465 try {
466 projects2[id] = project_getname(id);
467 } catch(...) {
468 messages << "Failed to load name for ID '" << id << "'" << std::endl;
471 return projects2;
474 std::string project_moviepath()
476 if(active_project)
477 return active_project->directory;
478 else
479 return lsnes_vset["moviepath"].str();
482 std::string project_otherpath()
484 if(active_project)
485 return active_project->directory;
486 else
487 return ".";
490 std::string project_savestate_ext()
492 return active_project ? "lss" : "lsmv";
495 void project_copy_watches(project_info& p)
497 for(auto i : get_watches())
498 p.watches[i] = get_watchexpr_for(i);
501 void project_copy_macros(project_info& p, controller_state& s)
503 for(auto i : s.enumerate_macro())
504 p.macros[i] = s.get_macro(i).serialize();