Add INI file versioning

This introduces a new INI config option called "IniVersion". Whenever
default values change or options are renmaed, the latest version number is
incremented and an update path is run.
This change also rewrites part of the initialization routine by adding
more safety checks before creating or reading the configuration file.
This commit is contained in:
Fabrice de Gans 2023-02-06 15:16:43 -08:00 committed by Rafael Kitover
parent ae09ae7d59
commit 91873254d3
11 changed files with 455 additions and 363 deletions

File diff suppressed because it is too large Load Diff

View File

@ -208,10 +208,10 @@ EVT_HANDLER(RecentReset, "Reset recent ROM list")
while (gopts.recent->GetCount())
gopts.recent->RemoveFileFromHistory(0);
wxFileConfig* cfg = wxGetApp().cfg;
cfg->SetPath(wxT("/Recent"));
wxConfigBase* cfg = wxConfigBase::Get();
cfg->SetPath("/Recent");
gopts.recent->Save(*cfg);
cfg->SetPath(wxT("/"));
cfg->SetPath("/");
cfg->Flush();
}
}
@ -2857,14 +2857,12 @@ EVT_HANDLER(UpdateEmu, "Check for updates...")
EVT_HANDLER(FactoryReset, "Factory Reset...")
{
wxMessageDialog dlg(NULL, wxString(wxT(
"YOUR CONFIGURATION WILL BE DELETED!\n\n")) + wxString(wxT(
"Are you sure?")),
wxT("FACTORY RESET"), wxYES_NO | wxNO_DEFAULT | wxCENTRE);
wxMessageDialog dlg(
nullptr, _("YOUR CONFIGURATION WILL BE DELETED!\n\nAre you sure?"),
_("FACTORY RESET"), wxYES_NO | wxNO_DEFAULT | wxCENTRE);
if (dlg.ShowModal() == wxID_YES) {
wxGetApp().cfg->DeleteAll();
wxConfigBase::Get()->DeleteAll();
wxExecute(wxStandardPaths::Get().GetExecutablePath(), wxEXEC_ASYNC);
Close(true);
}

View File

@ -161,6 +161,9 @@ std::array<Option, kNbOptions>& Option::All() {
double video_scale = 3;
bool retain_aspect = true;
/// General
uint32_t ini_version = kIniLatestVersion;
/// Geometry
bool window_maximized = false;
uint32_t window_height = 0;
@ -226,6 +229,7 @@ std::array<Option, kNbOptions>& Option::All() {
Option(OptionID::kGenScreenshotDir, &gopts.scrshot_dir),
Option(OptionID::kGenStateDir, &gopts.state_dir),
Option(OptionID::kGenStatusBar, &gopts.statusbar),
Option(OptionID::kGenIniVersion, &g_owned_opts.ini_version, 0, std::numeric_limits<uint32_t>::max()),
/// Joypad
Option(OptionID::kJoy),
@ -402,6 +406,7 @@ const std::array<OptionData, kNbOptions + 1> kAllOptionsData = {
_("Directory to store saved state files (relative paths are "
"relative to BatteryDir)")},
OptionData{"General/StatusBar", "StatusBar", _("Enable status bar")},
OptionData{"General/IniVersion", "", _("INI file version (DO NOT MODIFY)")},
/// Joypad
OptionData{"Joypad/*/*", "",

View File

@ -53,6 +53,7 @@ enum class OptionID {
kGenScreenshotDir,
kGenStateDir,
kGenStatusBar,
kGenIniVersion,
/// Joypad
kJoy,

View File

@ -59,6 +59,7 @@ static constexpr std::array<Option::Type, kNbOptions> kOptionsTypes = {
/*kGenScreenshotDir*/ Option::Type::kString,
/*kGenStateDir*/ Option::Type::kString,
/*kGenStatusBar*/ Option::Type::kBool,
/*kGenIniVersion*/ Option::Type::kUnsigned,
/// Joypad
/*kJoy*/ Option::Type::kNone,

View File

@ -79,6 +79,10 @@ enum class RenderMethod {
static constexpr size_t kNbRenderMethods =
static_cast<size_t>(RenderMethod::kLast);
// This is incremented whenever we want to change a default value between
// release versions. The option update code is in load_opts.
static constexpr uint32_t kIniLatestVersion = 1;
// Represents a single option saved in the INI file. Option does not own the
// individual option, but keeps a pointer to where the data is actually saved.
//

View File

@ -1,11 +1,12 @@
#include "opts.h"
#include <algorithm>
#include <limits>
#include <memory>
#include <unordered_set>
#include <wx/defs.h>
#include <wx/log.h>
#include <wx/display.h>
#include "config/option-observer.h"
#include "config/option-proxy.h"
@ -25,7 +26,7 @@
namespace {
void SaveOption(config::Option* option) {
wxFileConfig* cfg = wxGetApp().cfg;
wxConfigBase* cfg = wxConfigBase::Get();
switch (option->type()) {
case config::Option::Type::kNone:
@ -72,6 +73,33 @@ void InitializeOptionObservers() {
}
}
// Helper function to work around wxWidgets limitations when converting string
// to unsigned int.
uint32_t LoadUnsignedOption(wxConfigBase* cfg,
const wxString& option_name,
uint32_t default_value) {
wxString temp;
if (!cfg->Read(option_name, &temp)) {
return default_value;
}
if (!temp.IsNumber()) {
return default_value;
}
// Go through ulonglong to get enough space to work with. Also, older
// versions do not have a conversion function for unsigned int.
wxULongLong_t out;
if (!temp.ToULongLong(&out)) {
return default_value;
}
if (out > std::numeric_limits<uint32_t>::max()) {
return default_value;
}
return out;
}
} // namespace
#define WJKB config::UserInput
@ -317,7 +345,7 @@ opts_t::opts_t()
}
// FIXME: simulate MakeInstanceFilename(vbam.ini) using subkeys (Slave%d/*)
void load_opts() {
void load_opts(bool first_time_launch) {
// just for sanity...
static bool did_init = false;
assert(!did_init);
@ -326,8 +354,9 @@ void load_opts() {
// enumvals should not be translated, since they would cause config file
// change after lang change
// instead, translate when presented to user
wxFileConfig* cfg = wxGetApp().cfg;
cfg->SetPath(wxT("/"));
wxConfigBase* cfg = wxConfigBase::Get();
cfg->SetPath("/");
// enure there are no unknown options present
// note that items cannot be deleted until after loop or loop will fail
wxArrayString item_del, grp_del;
@ -341,9 +370,22 @@ void load_opts() {
item_del.push_back(s);
}
// Date of last online update check;
gopts.last_update = cfg->Read(wxT("General/LastUpdated"), (long)0);
cfg->Read(wxT("General/LastUpdatedFileName"), &gopts.last_updated_filename);
// Read the IniVersion now since the Option initialization code will reset
// it to kIniLatestVersion if it is unset.
uint32_t ini_version = 0;
if (first_time_launch) {
// Just go with the default values for the first time launch.
ini_version = config::kIniLatestVersion;
} else {
// We want to default to 0 if the option is not set.
ini_version = LoadUnsignedOption(cfg, "General/IniVersion", 0);
if (ini_version > config::kIniLatestVersion) {
wxLogWarning(
_("The INI file was written for a more recent version of "
"VBA-M. Some INI option values may have been reset."));
ini_version = config::kIniLatestVersion;
}
}
for (cont = cfg->GetFirstGroup(s, grp_idx); cont;
cont = cfg->GetNextGroup(s, grp_idx)) {
@ -464,8 +506,8 @@ void load_opts() {
break;
}
case config::Option::Type::kUnsigned: {
int temp;
cfg->Read(opt.config_name(), &temp, opt.GetUnsigned());
uint32_t temp =
LoadUnsignedOption(cfg, opt.config_name(), opt.GetUnsigned());
opt.SetUnsigned(temp);
break;
}
@ -537,11 +579,6 @@ void load_opts() {
}
}
// Make sure link_timeout is not set to 1, which was the previous default.
if (gopts.link_timeout <= 1) {
gopts.link_timeout = 500;
}
// recent is special
// Recent does not get written with defaults
cfg->SetPath(wxT("/Recent"));
@ -549,6 +586,8 @@ void load_opts() {
cfg->SetPath(wxT("/"));
cfg->Flush();
InitializeOptionObservers();
// We default the MaxThreads option to 0, so set it to the CPU count here.
config::OptionProxy<config::OptionID::kDispMaxThreads> max_threads;
if (max_threads == 0) {
@ -563,13 +602,32 @@ void load_opts() {
}
}
InitializeOptionObservers();
// Apply Option updates.
while (ini_version < config::kIniLatestVersion) {
// Update the ini version as we go in case we fail halfway through.
OPTION(kGenIniVersion) = ini_version;
switch (ini_version) {
case 0: { // up to 2.1.5 included.
#ifndef NO_LINK
// Previous default was 1.
if (OPTION(kGBALinkTimeout) == 1) {
OPTION(kGBALinkTimeout) = 500;
}
#endif
// Previous default was true.
OPTION(kGBALCDFilter) = false;
}
}
ini_version++;
}
// Finally, overwrite the value to the current version.
OPTION(kGenIniVersion) = config::kIniLatestVersion;
}
// Note: run load_opts() first to guarantee all config opts exist
void update_opts()
{
wxFileConfig* cfg = wxGetApp().cfg;
void update_opts() {
wxConfigBase* cfg = wxConfigBase::Get();
for (config::Option& opt : config::Option::All()) {
SaveOption(&opt);

View File

@ -53,8 +53,6 @@ extern struct opts_t {
bool autoload_state = false;
bool autoload_cheats = false;
wxString battery_dir;
long last_update;
wxString last_updated_filename;
bool recent_freeze = false;
wxString recording_dir;
int rewind_interval = 0;
@ -116,7 +114,7 @@ extern const int num_def_accels;
// call to load config (once)
// will write defaults for options not present and delete bad opts
// will also initialize opts[] array translations
void load_opts();
void load_opts(bool first_time_launch);
// call whenever opt vars change
// will detect changes and write config if necessary
void update_opts();

View File

@ -170,14 +170,14 @@ void GameArea::LoadGame(const wxString& name)
}
{
wxFileConfig* cfg = wxGetApp().cfg;
wxConfigBase* cfg = wxConfigBase::Get();
if (!gopts.recent_freeze) {
gopts.recent->AddFileToHistory(name);
wxGetApp().frame->SetRecentAccels();
cfg->SetPath(wxT("/Recent"));
cfg->SetPath("/Recent");
gopts.recent->Save(*cfg);
cfg->SetPath(wxT("/"));
cfg->SetPath("/");
cfg->Flush();
}
}

View File

@ -38,6 +38,11 @@
#include "strutils.h"
#include "wayland.h"
namespace {
static const wxString kOldConfigFileName("vbam.conf");
static const wxString knewConfigFileName("vbam.ini");
} // namespace
#ifdef __WXMSW__
int __stdcall WinMain(HINSTANCE hInstance,
@ -197,16 +202,14 @@ const wxString wxvbamApp::GetPluginsDir()
return wxStandardPaths::Get().GetPluginsDir();
}
wxString wxvbamApp::GetConfigurationPath()
{
wxString config(wxT("vbam.ini"));
wxString wxvbamApp::GetConfigurationPath() {
// first check if config files exists in reverse order
// (from system paths to more local paths.)
if (data_path.empty()) {
get_config_path(config_path);
for (int i = config_path.size() - 1; i >= 0; i--) {
wxFileName fn(config_path[i], config);
wxFileName fn(config_path[i], knewConfigFileName);
if (fn.FileExists() && fn.IsFileWritable()) {
data_path = config_path[i];
@ -312,53 +315,52 @@ bool wxvbamApp::OnInit() {
wxSetWorkingDirectory(cwd);
if (!cfg) {
// set up config file
// this needs to be in a subdir to support other config as well
// but subdir flag behaves differently 2.8 vs. 2.9. Oh well.
// NOTE: this does not support XDG (freedesktop.org) paths
wxString confname(wxT("vbam.ini"));
wxFileName vbamconf(GetConfigurationPath(), confname);
// /MIGRATION
// migrate from 'vbam.{cfg,conf}' to 'vbam.ini' to manage a single config
// file for all platforms.
wxString oldConf(GetConfigurationPath() + wxT(FILE_SEP) + wxT("vbam.conf"));
wxString newConf(GetConfigurationPath() + wxT(FILE_SEP) + wxT("vbam.ini"));
if (!config_file_.IsOk()) {
// Set up the default configuration file.
// This needs to be in a subdir to support other config as well.
// NOTE: this does not support XDG (freedesktop.org) paths.
// We rely on wx to build the paths in a cross-platform manner. However,
// the wxFileName APIs are weird and don't quite work as intended so we
// use the wxString APIs for files instead.
const wxString old_conf_file(
wxFileName(GetConfigurationPath(), kOldConfigFileName)
.GetFullPath());
const wxString new_conf_file(
wxFileName(GetConfigurationPath(), knewConfigFileName)
.GetFullPath());
if (!wxFileExists(newConf) && wxFileExists(oldConf))
wxRenameFile(oldConf, newConf, false);
if (wxDirExists(new_conf_file)) {
wxLogError(_("Invalid configuration file provided: %s"),
new_conf_file);
return false;
}
// /MIGRATION
// Migrate from 'vbam.conf' to 'vbam.ini' to manage a single config
// file for all platforms.
if (!wxFileExists(new_conf_file) && wxFileExists(old_conf_file)) {
wxRenameFile(old_conf_file, new_conf_file, false);
}
// /END_MIGRATION
cfg = new wxFileConfig(wxT("vbam"), wxEmptyString,
vbamconf.GetFullPath(),
wxEmptyString, wxCONFIG_USE_LOCAL_FILE);
// set global config for e.g. Windows font mapping
wxFileConfig::Set(cfg);
// yet another bug/deficiency in wxConfig: dirs are not created if needed
// since a default config is always written, dirs are always needed
// Can't figure out statically if using wxFileConfig w/o duplicating wx's
// logic, so do it at run-time
// wxFileConfig *f = wxDynamicCast(cfg, wxFileConfig);
// wxConfigBase does not derive from wxObject!!! so no wxDynamicCast
wxFileConfig* fc = dynamic_cast<wxFileConfig*>(cfg);
if (fc) {
wxFileName s(wxFileConfig::GetLocalFileName(GetAppName()));
// at least up to 2.8.12, GetLocalFileName returns the dir if
// SUBDIR is specified instead of actual file name
// and SUBDIR only affects UNIX
#if defined(__UNIX__) && !wxCHECK_VERSION(2, 9, 0)
s.AppendDir(s.GetFullName());
#endif
// only the path part gets created
// note that 0777 is default (assumes umask will do og-w)
s.Mkdir(0777, wxPATH_MKDIR_FULL);
s = wxFileName::DirName(GetConfigurationPath());
s.Mkdir(0777, wxPATH_MKDIR_FULL);
}
config_file_ = new_conf_file;
}
load_opts();
if (!config_file_.IsOk() || wxDirExists(config_file_.GetFullPath())) {
wxLogError(_("Invalid configuration file provided: %s"),
config_file_.GetFullPath());
return false;
}
// wx takes ownership of the wxFileConfig here. It will be deleted on app
// destruction.
wxConfigBase::DontCreateOnDemand();
wxConfigBase::Set(new wxFileConfig("vbam", wxEmptyString,
config_file_.GetFullPath(),
wxEmptyString, wxCONFIG_USE_LOCAL_FILE));
// Load the default options.
load_opts(!config_file_.Exists());
// wxGLCanvas segfaults under wayland before wx 3.2
#if defined(HAVE_WAYLAND_SUPPORT) && !defined(HAVE_WAYLAND_EGL)
@ -703,9 +705,7 @@ bool wxvbamApp::OnCmdLineParsed(wxCmdLineParser& cl)
wxLogError(_("Configuration file not found."));
return false;
}
cfg = new wxFileConfig(wxT("vbam"), wxEmptyString,
vbamconf.GetFullPath(),
wxEmptyString, wxCONFIG_USE_LOCAL_FILE);
config_file_ = s;
}
#if !defined(NO_LINK) && !defined(__WXMSW__)

View File

@ -104,8 +104,6 @@ public:
return accels;
}
// the main configuration
wxFileConfig* cfg = nullptr;
// vba-over.ini
wxFileConfig* overrides = nullptr;
@ -144,6 +142,9 @@ protected:
private:
wxPathList config_path;
char* home = nullptr;
// Main configuration file.
wxFileName config_file_;
};
DECLARE_APP(wxvbamApp);