You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

491 lines
11KB

  1. #include <algorithm>
  2. #include <osdialog.h>
  3. #include <patch.hpp>
  4. #include <asset.hpp>
  5. #include <system.hpp>
  6. #include <engine/Engine.hpp>
  7. #include <context.hpp>
  8. #include <app/common.hpp>
  9. #include <app/Scene.hpp>
  10. #include <app/RackWidget.hpp>
  11. #include <history.hpp>
  12. #include <settings.hpp>
  13. #include <fstream>
  14. namespace rack {
  15. static const char PATCH_FILTERS[] = "VCV Rack patch (.vcv):vcv";
  16. PatchManager::PatchManager() {
  17. }
  18. PatchManager::~PatchManager() {
  19. cleanAutosave();
  20. }
  21. void PatchManager::launch(std::string pathArg) {
  22. // Load the argument if exists
  23. if (pathArg != "") {
  24. loadAction(pathArg);
  25. return;
  26. }
  27. // Try loading the autosave patch
  28. if (hasAutosave()) {
  29. try {
  30. loadAutosave();
  31. // Keep path and save state as it was stored in patch.json
  32. }
  33. catch (Exception& e) {
  34. osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, e.what());
  35. }
  36. return;
  37. }
  38. // Try loading the template patch
  39. loadTemplate();
  40. }
  41. void PatchManager::clear() {
  42. path = "";
  43. if (APP->scene) {
  44. APP->scene->rack->clear();
  45. APP->scene->rackScroll->reset();
  46. }
  47. if (APP->history) {
  48. APP->history->clear();
  49. }
  50. APP->engine->clear();
  51. }
  52. static bool promptClear(std::string text) {
  53. if (APP->history->isSaved())
  54. return true;
  55. if (APP->scene->rack->isEmpty())
  56. return true;
  57. return osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, text.c_str());
  58. }
  59. void PatchManager::save(std::string path) {
  60. INFO("Saving patch %s", path.c_str());
  61. // Save patch.json
  62. saveAutosave();
  63. // Clean up autosave directory (e.g. removed modules)
  64. cleanAutosave();
  65. // Take screenshot (disabled because there is currently no way to quickly view them on any OS or website.)
  66. // APP->window->screenshot(system::join(asset::autosavePath, "screenshot.png"));
  67. double startTime = system::getTime();
  68. // Set compression level to 1 so that a 500MB/s SSD is almost bottlenecked
  69. system::archiveFolder(path, asset::autosavePath, 1);
  70. double endTime = system::getTime();
  71. INFO("Archived patch in %lf seconds", (endTime - startTime));
  72. }
  73. void PatchManager::saveDialog() {
  74. if (path == "") {
  75. saveAsDialog();
  76. return;
  77. }
  78. // Note: If save() fails below, this should probably be reset. But we need it so toJson() doesn't set the "unsaved" property.
  79. APP->history->setSaved();
  80. this->path = path;
  81. try {
  82. save(path);
  83. }
  84. catch (Exception& e) {
  85. std::string message = string::f("Could not save patch: %s", e.what());
  86. osdialog_message(OSDIALOG_INFO, OSDIALOG_OK, message.c_str());
  87. return;
  88. }
  89. }
  90. void PatchManager::saveAsDialog() {
  91. std::string dir;
  92. std::string filename;
  93. if (this->path == "") {
  94. dir = asset::user("patches");
  95. system::createDirectories(dir);
  96. }
  97. else {
  98. dir = system::getDirectory(this->path);
  99. filename = system::getFilename(this->path);
  100. }
  101. osdialog_filters* filters = osdialog_filters_parse(PATCH_FILTERS);
  102. DEFER({osdialog_filters_free(filters);});
  103. char* pathC = osdialog_file(OSDIALOG_SAVE, dir.c_str(), filename.c_str(), filters);
  104. if (!pathC) {
  105. // Cancel silently
  106. return;
  107. }
  108. DEFER({std::free(pathC);});
  109. // Append .vcv extension if no extension was given.
  110. std::string path = pathC;
  111. if (system::getExtension(path) == "") {
  112. path += ".vcv";
  113. }
  114. APP->history->setSaved();
  115. this->path = path;
  116. try {
  117. save(path);
  118. }
  119. catch (Exception& e) {
  120. std::string message = string::f("Could not save patch: %s", e.what());
  121. osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str());
  122. return;
  123. }
  124. pushRecentPath(path);
  125. }
  126. void PatchManager::saveTemplateDialog() {
  127. // Even if <user>/template.vcv doesn't exist, this message is still valid because it overrides the <system>/template.vcv patch.
  128. if (!osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "Overwrite template patch?"))
  129. return;
  130. try {
  131. save(asset::templatePath);
  132. }
  133. catch (Exception& e) {
  134. std::string message = string::f("Could not save template patch: %s", e.what());
  135. osdialog_message(OSDIALOG_INFO, OSDIALOG_OK, message.c_str());
  136. return;
  137. }
  138. }
  139. void PatchManager::saveAutosave() {
  140. std::string patchPath = system::join(asset::autosavePath, "patch.json");
  141. INFO("Saving autosave %s", patchPath.c_str());
  142. json_t* rootJ = toJson();
  143. if (!rootJ)
  144. return;
  145. DEFER({json_decref(rootJ);});
  146. // Write to temporary path and then rename it to the correct path
  147. system::createDirectories(asset::autosavePath);
  148. std::string tmpPath = patchPath + ".tmp";
  149. FILE* file = std::fopen(tmpPath.c_str(), "w");
  150. if (!file) {
  151. // Fail silently
  152. return;
  153. }
  154. json_dumpf(rootJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9));
  155. std::fclose(file);
  156. system::remove(patchPath);
  157. system::rename(tmpPath, patchPath);
  158. }
  159. void PatchManager::cleanAutosave() {
  160. // Remove files and folders in the `autosave/modules` folder that doesn't match a module in the rack.
  161. std::string modulesDir = system::join(asset::autosavePath, "modules");
  162. if (system::isDirectory(modulesDir)) {
  163. for (const std::string& entry : system::getEntries(modulesDir)) {
  164. try {
  165. int64_t moduleId = std::stol(system::getFilename(entry));
  166. // Ignore modules that exist in the rack
  167. if (APP->engine->getModule(moduleId))
  168. continue;
  169. }
  170. catch (std::invalid_argument& e) {
  171. }
  172. // Remove the entry.
  173. system::removeRecursively(entry);
  174. }
  175. }
  176. }
  177. static bool isPatchLegacyPre2(std::string path) {
  178. FILE* f = std::fopen(path.c_str(), "rb");
  179. if (!f)
  180. return false;
  181. DEFER({std::fclose(f);});
  182. // All Zstandard frames start with this magic number.
  183. char zstdMagic[] = "\x28\xb5\x2f\xfd";
  184. char buf[4] = {};
  185. std::fread(buf, 1, sizeof(buf), f);
  186. // If the patch file doesn't begin with the magic number, it's a legacy patch.
  187. return std::memcmp(buf, zstdMagic, sizeof(buf)) != 0;
  188. }
  189. void PatchManager::load(std::string path) {
  190. INFO("Loading patch %s", path.c_str());
  191. system::removeRecursively(asset::autosavePath);
  192. system::createDirectories(asset::autosavePath);
  193. if (isPatchLegacyPre2(path)) {
  194. // Copy the .vcv file directly to "patch.json".
  195. system::copy(path, system::join(asset::autosavePath, "patch.json"));
  196. }
  197. else {
  198. // Extract the .vcv file as a .tar.zst archive.
  199. double startTime = system::getTime();
  200. system::unarchiveToFolder(path, asset::autosavePath);
  201. double endTime = system::getTime();
  202. INFO("Unarchived patch in %lf seconds", (endTime - startTime));
  203. }
  204. loadAutosave();
  205. }
  206. void PatchManager::loadTemplate() {
  207. try {
  208. load(asset::templatePath);
  209. }
  210. catch (Exception& e) {
  211. // Do nothing because it's okay for the user template to not exist.
  212. try {
  213. load(asset::system("template.vcv"));
  214. }
  215. catch (Exception& e) {
  216. std::string message = string::f("Could not load system template patch, clearing rack: %s", e.what());
  217. osdialog_message(OSDIALOG_INFO, OSDIALOG_OK, message.c_str());
  218. clear();
  219. }
  220. }
  221. // load() sets the patch's original patch, but we don't want to use that.
  222. this->path = "";
  223. APP->history->setSaved();
  224. }
  225. void PatchManager::loadTemplateDialog() {
  226. if (!promptClear("The current patch is unsaved. Clear it and start a new patch?")) {
  227. return;
  228. }
  229. loadTemplate();
  230. }
  231. bool PatchManager::hasAutosave() {
  232. std::string patchPath = system::join(asset::autosavePath, "patch.json");
  233. INFO("Loading autosave %s", patchPath.c_str());
  234. FILE* file = std::fopen(patchPath.c_str(), "r");
  235. if (!file)
  236. return false;
  237. std::fclose(file);
  238. return true;
  239. }
  240. void PatchManager::loadAutosave() {
  241. std::string patchPath = system::join(asset::autosavePath, "patch.json");
  242. INFO("Loading autosave %s", patchPath.c_str());
  243. FILE* file = std::fopen(patchPath.c_str(), "r");
  244. if (!file)
  245. throw Exception("Could not open autosave patch %s", patchPath.c_str());
  246. DEFER({std::fclose(file);});
  247. json_error_t error;
  248. json_t* rootJ = json_loadf(file, 0, &error);
  249. if (!rootJ)
  250. throw Exception("Failed to load patch. JSON parsing error at %s %d:%d %s", error.source, error.line, error.column, error.text);
  251. DEFER({json_decref(rootJ);});
  252. fromJson(rootJ);
  253. }
  254. void PatchManager::loadAction(std::string path) {
  255. try {
  256. load(path);
  257. }
  258. catch (Exception& e) {
  259. std::string message = string::f("Could not load patch: %s", e.what());
  260. osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str());
  261. return;
  262. }
  263. this->path = path;
  264. APP->history->setSaved();
  265. pushRecentPath(path);
  266. }
  267. void PatchManager::loadDialog() {
  268. if (!promptClear("The current patch is unsaved. Clear it and open a new patch?"))
  269. return;
  270. std::string dir;
  271. if (this->path == "") {
  272. dir = asset::user("patches");
  273. system::createDirectory(dir);
  274. }
  275. else {
  276. dir = system::getDirectory(this->path);
  277. }
  278. osdialog_filters* filters = osdialog_filters_parse(PATCH_FILTERS);
  279. DEFER({osdialog_filters_free(filters);});
  280. char* pathC = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, filters);
  281. if (!pathC) {
  282. // Fail silently
  283. return;
  284. }
  285. std::string path = pathC;
  286. std::free(pathC);
  287. loadAction(path);
  288. }
  289. void PatchManager::loadPathDialog(std::string path) {
  290. if (!promptClear("The current patch is unsaved. Clear it and open the new patch?"))
  291. return;
  292. loadAction(path);
  293. }
  294. void PatchManager::revertDialog() {
  295. if (path == "")
  296. return;
  297. if (!promptClear("Revert patch to the last saved state?"))
  298. return;
  299. loadAction(path);
  300. }
  301. void PatchManager::pushRecentPath(std::string path) {
  302. auto& recent = settings::recentPatchPaths;
  303. // Remove path from recent patches (if exists)
  304. recent.remove(path);
  305. // Add path to top of recent patches
  306. recent.push_front(path);
  307. // Limit recent patches size
  308. recent.resize(std::min((int) recent.size(), 10));
  309. }
  310. void PatchManager::disconnectDialog() {
  311. APP->scene->rack->clearCablesAction();
  312. }
  313. json_t* PatchManager::toJson() {
  314. // root
  315. json_t* rootJ = json_object();
  316. // version
  317. json_t* versionJ = json_string(APP_VERSION.c_str());
  318. json_object_set_new(rootJ, "version", versionJ);
  319. // path
  320. if (path != "") {
  321. json_t* pathJ = json_string(path.c_str());
  322. json_object_set_new(rootJ, "path", pathJ);
  323. }
  324. // unsaved
  325. if (!APP->history->isSaved())
  326. json_object_set_new(rootJ, "unsaved", json_boolean(true));
  327. // Merge with rootJ
  328. json_t* engineJ = APP->engine->toJson();
  329. json_object_update(rootJ, engineJ);
  330. json_decref(engineJ);
  331. if (APP->scene) {
  332. APP->scene->rack->mergeJson(rootJ);
  333. }
  334. return rootJ;
  335. }
  336. void PatchManager::fromJson(json_t* rootJ) {
  337. clear();
  338. legacy = 0;
  339. // version
  340. std::string version;
  341. json_t* versionJ = json_object_get(rootJ, "version");
  342. if (versionJ)
  343. version = json_string_value(versionJ);
  344. if (version != APP_VERSION) {
  345. INFO("Patch was made with Rack v%s, current Rack version is v%s", version.c_str(), APP_VERSION.c_str());
  346. }
  347. // Detect old patches with ModuleWidget::params/inputs/outputs indices.
  348. if (string::startsWith(version, "0.3.") || string::startsWith(version, "0.4.") || string::startsWith(version, "0.5.") || version == "" || version == "dev") {
  349. // Use ModuleWidget::params/inputs/outputs indices instead of Module.
  350. legacy = 1;
  351. }
  352. else if (string::startsWith(version, "0.6.")) {
  353. legacy = 2;
  354. }
  355. if (legacy) {
  356. INFO("Loading patch using legacy mode %d", legacy);
  357. }
  358. // path
  359. json_t* pathJ = json_object_get(rootJ, "path");
  360. if (pathJ)
  361. path = json_string_value(pathJ);
  362. else
  363. path = "";
  364. // unsaved
  365. json_t* unsavedJ = json_object_get(rootJ, "unsaved");
  366. if (!unsavedJ)
  367. APP->history->setSaved();
  368. APP->engine->fromJson(rootJ);
  369. if (APP->scene) {
  370. APP->scene->rack->fromJson(rootJ);
  371. }
  372. // At this point, ModuleWidgets and CableWidgets should own all Modules and Cables.
  373. // TODO Assert this
  374. // Display a message if we have something to say.
  375. if (warningLog != "") {
  376. osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, warningLog.c_str());
  377. }
  378. warningLog = "";
  379. }
  380. bool PatchManager::isLegacy(int level) {
  381. return legacy && legacy <= level;
  382. }
  383. void PatchManager::log(std::string msg) {
  384. warningLog += msg;
  385. warningLog += "\n";
  386. }
  387. } // namespace rack