#include #include #include #include #include #include #include #include #include #include #include #include #include namespace rack { namespace patch { static const char PATCH_FILTERS[] = "VCV Rack patch (.vcv):vcv"; Manager::Manager() { autosavePath = asset::user("autosave"); templatePath = asset::user("template.vcv"); factoryTemplatePath = asset::system("template.vcv"); } Manager::~Manager() { // Dispatch onSave to all Modules so they save their patch storage, etc. APP->engine->prepareSave(); // Save autosave if not headless if (!settings::headless) { APP->patch->saveAutosave(); } cleanAutosave(); } void Manager::launch(std::string pathArg) { // Load the argument if exists if (pathArg != "") { loadAction(pathArg); return; } // Try loading the autosave patch if (hasAutosave()) { try { loadAutosave(); // Keep path and save state as it was stored in patch.json } catch (Exception& e) { osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, e.what()); } return; } // Try loading the template patch loadTemplate(); } void Manager::clear() { path = ""; if (APP->scene) { APP->scene->rack->clear(); APP->scene->rackScroll->reset(); } if (APP->history) { APP->history->clear(); } APP->engine->clear(); } static bool promptClear(std::string text) { if (APP->history->isSaved()) return true; if (APP->scene->rack->hasModules()) return true; return osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, text.c_str()); } void Manager::save(std::string path) { INFO("Saving patch %s", path.c_str()); // Dispatch SaveEvent to modules APP->engine->prepareSave(); // Save patch.json saveAutosave(); // Clean up autosave directory (e.g. removed modules) cleanAutosave(); // Take screenshot (disabled because there is currently no way to quickly view them on any OS or website.) // APP->window->screenshot(system::join(autosavePath, "screenshot.png")); double startTime = system::getTime(); // Set compression level to 1 so that a 500MB/s SSD is almost bottlenecked system::archiveDirectory(path, autosavePath, 1); double endTime = system::getTime(); INFO("Archived patch in %lf seconds", (endTime - startTime)); } void Manager::saveDialog() { if (path == "") { saveAsDialog(); return; } // Note: If save() fails below, this should probably be reset. But we need it so toJson() doesn't set the "unsaved" property. APP->history->setSaved(); try { save(path); } catch (Exception& e) { std::string message = string::f("Could not save patch: %s", e.what()); osdialog_message(OSDIALOG_INFO, OSDIALOG_OK, message.c_str()); return; } } void Manager::saveAsDialog() { std::string dir; std::string filename; if (this->path == "") { dir = asset::user("patches"); system::createDirectories(dir); filename = "Untitled.vcv"; } else { dir = system::getDirectory(this->path); filename = system::getFilename(this->path); } osdialog_filters* filters = osdialog_filters_parse(PATCH_FILTERS); DEFER({osdialog_filters_free(filters);}); char* pathC = osdialog_file(OSDIALOG_SAVE, dir.c_str(), filename.c_str(), filters); if (!pathC) { // Cancel silently return; } DEFER({std::free(pathC);}); // Automatically append .vcv extension std::string path = pathC; if (system::getExtension(path) != ".vcv") { path += ".vcv"; } APP->history->setSaved(); this->path = path; try { save(path); } catch (Exception& e) { std::string message = string::f("Could not save patch: %s", e.what()); osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str()); return; } pushRecentPath(path); } void Manager::saveTemplateDialog() { // Even if /template.vcv doesn't exist, this message is still valid because it overrides the /template.vcv patch. if (!osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "Overwrite template patch?")) return; try { save(templatePath); } catch (Exception& e) { std::string message = string::f("Could not save template patch: %s", e.what()); osdialog_message(OSDIALOG_INFO, OSDIALOG_OK, message.c_str()); return; } } void Manager::saveAutosave() { std::string patchPath = system::join(autosavePath, "patch.json"); INFO("Saving autosave %s", patchPath.c_str()); json_t* rootJ = toJson(); if (!rootJ) return; DEFER({json_decref(rootJ);}); // Write to temporary path and then rename it to the correct path system::createDirectories(autosavePath); std::string tmpPath = patchPath + ".tmp"; FILE* file = std::fopen(tmpPath.c_str(), "w"); if (!file) { // Fail silently return; } json_dumpf(rootJ, file, JSON_INDENT(2)); std::fclose(file); system::remove(patchPath); system::rename(tmpPath, patchPath); } void Manager::cleanAutosave() { // Remove files and directories in the `autosave/modules` directory that doesn't match a module in the rack. std::string modulesDir = system::join(autosavePath, "modules"); if (system::isDirectory(modulesDir)) { for (const std::string& entry : system::getEntries(modulesDir)) { try { int64_t moduleId = std::stoll(system::getFilename(entry)); // Ignore modules that exist in the rack if (APP->engine->getModule(moduleId)) continue; } catch (std::invalid_argument& e) {} catch (std::out_of_range& e) {} // Remove the entry. system::removeRecursively(entry); } } } static bool isPatchLegacyV1(std::string path) { FILE* f = std::fopen(path.c_str(), "rb"); if (!f) return false; DEFER({std::fclose(f);}); // All Zstandard frames start with this magic number. char zstdMagic[] = "\x28\xb5\x2f\xfd"; char buf[4] = {}; std::fread(buf, 1, sizeof(buf), f); // If the patch file doesn't begin with the magic number, it's a legacy patch. return std::memcmp(buf, zstdMagic, sizeof(buf)) != 0; } void Manager::load(std::string path) { INFO("Loading patch %s", path.c_str()); clear(); system::removeRecursively(autosavePath); system::createDirectories(autosavePath); if (isPatchLegacyV1(path)) { // Copy the .vcv file directly to "patch.json". system::copy(path, system::join(autosavePath, "patch.json")); } else { // Extract the .vcv file as a .tar.zst archive. double startTime = system::getTime(); system::unarchiveToDirectory(path, autosavePath); double endTime = system::getTime(); INFO("Unarchived patch in %lf seconds", (endTime - startTime)); } loadAutosave(); } void Manager::loadTemplate() { clear(); try { load(templatePath); } catch (Exception& e) { // Try loading the system template patch try { load(factoryTemplatePath); } catch (Exception& e) { std::string message = string::f("Could not load system template patch, clearing rack: %s", e.what()); osdialog_message(OSDIALOG_INFO, OSDIALOG_OK, message.c_str()); system::removeRecursively(autosavePath); system::createDirectories(autosavePath); } } // load() sets the patch's original patch, but we don't want to use that. this->path = ""; APP->history->setSaved(); } void Manager::loadTemplateDialog() { if (!promptClear("The current patch is unsaved. Clear it and start a new patch?")) { return; } loadTemplate(); } bool Manager::hasAutosave() { std::string patchPath = system::join(autosavePath, "patch.json"); INFO("Loading autosave %s", patchPath.c_str()); FILE* file = std::fopen(patchPath.c_str(), "r"); if (!file) return false; std::fclose(file); return true; } void Manager::loadAutosave() { std::string patchPath = system::join(autosavePath, "patch.json"); INFO("Loading autosave %s", patchPath.c_str()); FILE* file = std::fopen(patchPath.c_str(), "r"); if (!file) throw Exception("Could not open autosave patch %s", patchPath.c_str()); DEFER({std::fclose(file);}); json_error_t error; json_t* rootJ = json_loadf(file, 0, &error); if (!rootJ) throw Exception("Failed to load patch. JSON parsing error at %s %d:%d %s", error.source, error.line, error.column, error.text); DEFER({json_decref(rootJ);}); fromJson(rootJ); } void Manager::loadAction(std::string path) { try { load(path); } catch (Exception& e) { std::string message = string::f("Could not load patch: %s", e.what()); osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str()); return; } this->path = path; APP->history->setSaved(); pushRecentPath(path); } void Manager::loadDialog() { if (!promptClear("The current patch is unsaved. Clear it and open a new patch?")) return; std::string dir; if (this->path == "") { dir = asset::user("patches"); system::createDirectory(dir); } else { dir = system::getDirectory(this->path); } osdialog_filters* filters = osdialog_filters_parse(PATCH_FILTERS); DEFER({osdialog_filters_free(filters);}); char* pathC = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, filters); if (!pathC) { // Fail silently return; } std::string path = pathC; std::free(pathC); loadAction(path); } void Manager::loadPathDialog(std::string path) { if (!promptClear("The current patch is unsaved. Clear it and open the new patch?")) return; loadAction(path); } void Manager::revertDialog() { if (path == "") return; if (!promptClear("Revert patch to the last saved state?")) return; loadAction(path); } void Manager::pushRecentPath(std::string path) { auto& recent = settings::recentPatchPaths; // Remove path from recent patches (if exists) recent.remove(path); // Add path to top of recent patches recent.push_front(path); // Limit recent patches size recent.resize(std::min((int) recent.size(), 10)); } void Manager::disconnectDialog() { APP->scene->rack->clearCablesAction(); } json_t* Manager::toJson() { // root json_t* rootJ = json_object(); // version json_t* versionJ = json_string(APP_VERSION.c_str()); json_object_set_new(rootJ, "version", versionJ); // path if (path != "") { json_t* pathJ = json_string(path.c_str()); json_object_set_new(rootJ, "path", pathJ); } // unsaved if (!APP->history->isSaved()) json_object_set_new(rootJ, "unsaved", json_boolean(true)); if (APP->scene) { // zoom float zoom = APP->scene->rackScroll->getZoom(); json_object_set_new(rootJ, "zoom", json_real(zoom)); // gridOffset math::Vec gridOffset = APP->scene->rackScroll->getGridOffset(); json_t* gridOffsetJ = json_pack("[f, f]", gridOffset.x, gridOffset.y); json_object_set_new(rootJ, "gridOffset", gridOffsetJ); } // Merge with Engine JSON json_t* engineJ = APP->engine->toJson(); json_object_update(rootJ, engineJ); json_decref(engineJ); // Merge with RackWidget JSON if (APP->scene) { APP->scene->rack->mergeJson(rootJ); } return rootJ; } void Manager::fromJson(json_t* rootJ) { clear(); // version std::string version; json_t* versionJ = json_object_get(rootJ, "version"); if (versionJ) version = json_string_value(versionJ); if (version != APP_VERSION) { INFO("Patch was made with Rack v%s, current Rack version is v%s", version.c_str(), APP_VERSION.c_str()); } // path json_t* pathJ = json_object_get(rootJ, "path"); if (pathJ) path = json_string_value(pathJ); else path = ""; // unsaved json_t* unsavedJ = json_object_get(rootJ, "unsaved"); if (!unsavedJ) APP->history->setSaved(); if (APP->scene) { // zoom json_t* zoomJ = json_object_get(rootJ, "zoom"); if (zoomJ) APP->scene->rackScroll->setZoom(json_number_value(zoomJ)); // gridOffset json_t* gridOffsetJ = json_object_get(rootJ, "gridOffset"); if (gridOffsetJ) { double x, y; json_unpack(gridOffsetJ, "[F, F]", &x, &y); APP->scene->rackScroll->setGridOffset(math::Vec(x, y)); } } // Pass JSON to Engine and RackWidget try { APP->engine->fromJson(rootJ); if (APP->scene) { APP->scene->rack->fromJson(rootJ); } } catch (Exception& e) { warningLog += "\n"; warningLog += e.what(); } // At this point, ModuleWidgets and CableWidgets should own all Modules and Cables. // TODO Assert this // Display a message if we have something to say. if (warningLog != "") { osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, warningLog.c_str()); } warningLog = ""; } void Manager::log(std::string msg) { warningLog += msg; warningLog += "\n"; } } // namespace patch } // namespace rack