Files
fs2open.github.com/code/controlconfig/presets.cpp
T
LuytenKy bb69b1b9db Refactor: CC_preset (#7809)
* CC_preset: Prevent post-construction type modification and handle pst duplication

* remove redundant function

* CC_preset: remove default constructor, require explicit type

* remove duplicate
2026-11-02 22:40:54 -05:00

530 lines
15 KiB
C++

#include "controlconfig/controlsconfig.h"
#include "controlconfig/presets.h"
#include "gamesequence/gamesequence.h"
#include "libs/jansson.h"
#include "parse/parselo.h"
#include "popup/popup.h"
#include <jansson.h>
using namespace io::presets;
const SCP_vector<std::pair<Section, const char*>> SectionMapping {
std::pair<Section, const char*>(Section::Unnamed, nullptr),
std::pair<Section, const char*>(Section::Actions, "actions"),
std::pair<Section, const char*>(Section::Primary, "primary"),
std::pair<Section, const char*>(Section::Secondary, "secondary"),
};
const char * lookupSectionName(Section id) {
for (const auto& pair: SectionMapping) {
if (pair.first == id) {
return pair.second;
}
}
return nullptr;
}
Section lookupSectionValue(const char* name) {
Assertion(name != nullptr, "Key name must be a valid pointer!");
for (auto& pair : SectionMapping) {
if (!strcmp(pair.second, name)) {
return pair.first;
}
}
return Section::Invalid;
}
void load_preset_files(SCP_string clone) {
SCP_vector<SCP_string> filelist;
cf_get_file_list(filelist, CF_TYPE_PLAYER_BINDS, NOX("*.json"), CF_SORT_NAME, nullptr,
CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
std::unique_ptr<PresetFileHandler> handler = nullptr;
for (const auto &file : filelist) {
CFILE* fp = cfopen((file + ".json").c_str(), "r", CF_TYPE_PLAYER_BINDS, false,
CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
if (!fp) {
mprintf(("PST => Unable to open '%s' for loading!\n", file.c_str()));
// try next
continue;
}
try {
handler.reset(new PresetFileHandler(fp, true));
} catch (const std::exception& e) {
mprintf(("PST => Failed to open JSON: `%s`\n", e.what()));
continue;
}
// Check the signature
int pst_id = handler->readInt("signature");
if (pst_id != PST_FILE_ID) {
mprintf(("PST => Invalid header id for `%s`, skipping...\n", file.c_str()));
continue;
}
// Version
int version = handler->readInt("version");
mprintf(("PST => Loading `%s` with version %i\n", file.c_str(), version));
// Start reading in data
CC_preset preset(file, Preset_t::pst);
preset.bindings.resize(Control_config.size());
size_t size;
handler->beginArrayRead(Section::Actions, size);
for (size_t i = 0; i < size; ++i, handler->nextArrayElement()) {
SCP_string str;
int id;
//handler->beginSectionRead(Section::Unnamed);
str = handler->readString("bind");
id = ActionToVal(str.c_str());
if (id < 0) {
// Unknown bind, possibly custom. Ignore for now
handler->endSectionRead(); // Unnamed
continue;
}
auto &item = preset.bindings[id];
handler->beginSectionRead(Section::Primary);
str = handler->readString("cid");
auto cid = CIDToVal(str.c_str());
str = handler->readString("flags");
auto flags = CCFToVal(str.c_str());
str = handler->readString("input");
auto btn = InputToVal(cid, str.c_str());
item.first.take(cid, btn, flags);
handler->endSectionRead(); // Primary
handler->beginSectionRead(Section::Secondary);
str = handler->readString("cid");
cid = CIDToVal(str.c_str());
str = handler->readString("flags");
flags = CCFToVal(str.c_str());
str = handler->readString("input");
btn = InputToVal(cid, str.c_str());
item.second.take(cid, btn, flags);
handler->endSectionRead(); // Secondary
//handler->endSectionRead(); // Unnamed
}
handler->endArrayRead(); // Actions
// Done with the file
auto it = preset_find_duplicate(preset);
// If we just cloned the file, then allow the duplicate. Just this once.
if ((clone == preset.name) || (it == Control_config_presets.end())) {
Control_config_presets.push_back(preset);
} else if ((it->name != preset.name) || ((it->get_type() != Preset_t::pst))) {
if (gameseq_get_state() == GS_STATE_CONTROL_CONFIG) {
popup(PF_TITLE_WHITE | PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, "Preset '%s' is a duplicate of an existing preset, ignoring", preset.name.c_str());
} else {
// Complain and ignore if the preset names or the type differs
Warning(LOCATION, "PST => Preset '%s' is a duplicate of an existing preset, ignoring", preset.name.c_str());
}
} // else, silent ignore
}
}
bool preset_file_exists(SCP_string name) {
if (name.find(".json") == std::string::npos) {
name.append(".json");
}
return cf_exists(name.c_str(), CF_TYPE_PLAYER_BINDS) != 0;
}
bool delete_preset_file(CC_preset preset) {
// can't remove the default preset!
if (preset.name == "default") {
return false;
}
auto it = control_config_get_current_preset();
// can't remove the currently loaded preset either!
if (preset.name == it->name) {
return false;
}
SCP_string filename = preset.name + ".json";
cf_delete(filename.c_str(), CF_TYPE_PLAYER_BINDS, CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
// Reload the presets from file.
Control_config_presets.erase(Control_config_presets.begin() + 1, Control_config_presets.end());
load_preset_files();
return true;
}
bool save_preset_file(CC_preset preset, bool overwrite) {
// Must have a name
if (preset.name.empty()) {
mprintf(("PST => Unable to save preset, missing name!\n"));
return false;
}
SCP_string filename = preset.name + ".json";
std::unique_ptr<PresetFileHandler> handler = nullptr;
// Check if there's a file already
CFILE* fp = cfopen(filename.c_str(), "r", CF_TYPE_PLAYER_BINDS, false,
CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
if ((fp != nullptr) && !overwrite) {
mprintf(("PST => Unable to save '%s', file already exists!\n", filename.c_str()));
return false;
}
// Try opening file for write
fp = cfopen(filename.c_str(), "w", CF_TYPE_PLAYER_BINDS, false,
CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
if (!fp) {
mprintf(("PST => Unable to save '%s', unknown error!\n", filename.c_str()));
}
try {
handler.reset(new PresetFileHandler(fp, false));
} catch (const std::exception& e) {
mprintf(("PST => Failed to open JSON: %s\n", e.what()));
return false;
}
// Write header and version
handler->writeInt("signature", PST_FILE_ID);
handler->writeInt("version", PST_VERSION);
mprintf(("PST => Saving %s with version %d...\n", filename.c_str(), (int)PST_VERSION));
handler->beginArrayWrite(Section::Actions);
for (int i = 0; static_cast<size_t>(i) < preset.bindings.size(); ++i) {
const auto& item = preset.bindings[i];
const auto& first = item.first;
const auto& second = item.second;
handler->beginSectionWrite(Section::Unnamed);
handler->writeString("bind", ValToAction(i));
handler->beginSectionWrite(Section::Primary);
handler->writeString("cid", ValToCID(first.get_cid()));
handler->writeString("flags", ValToCCF(first.get_flags()));
handler->writeString("input", ValToInput(first));
handler->endSectionWrite(); // Primary
handler->beginSectionWrite(Section::Secondary);
handler->writeString("cid", ValToCID(second.get_cid()));
handler->writeString("flags", ValToCCF(second.get_flags()));
handler->writeString("input", ValToInput(second));
handler->endSectionWrite(); // Secondary
handler->endSectionWrite(); // Control_config[i]
}
handler->endArrayWrite(); // Actions
handler->flush();
return true;
}
SCP_vector<CC_preset>::iterator preset_find_duplicate(const CC_preset& new_preset) {
SCP_vector<CC_preset>::iterator it = Control_config_presets.begin(); // NOLINT(modernize-use-auto)
for (; it != Control_config_presets.end(); ++it) {
size_t i;
for (i = 0; i < it->bindings.size(); ++i) {
if (new_preset.bindings[i] != it->bindings[i]) {
// These two differ, check the next in the vector
break;
}
}
if (i == it->bindings.size()) {
// These two are equal
break;
}
}
return it;
}
PresetFileHandler::PresetFileHandler(CFILE* cfp, bool reading) : _cfp(cfp) {
Assertion(cfp != nullptr, "File pointer must be valid!");
if (reading) {
json_error_t error;
_rootObj = json_load_cfile(cfp, 0, &error);
if (!_rootObj) {
SCP_string errorStr;
sprintf(errorStr, "Error while reading pilot file! %d: %s", error.line, error.text);
throw std::runtime_error(errorStr);
}
} else {
_rootObj = json_object();
}
_currentEl = _rootObj;
_elementStack.push_back(_currentEl);
};
PresetFileHandler::~PresetFileHandler() {
json_decref(_rootObj);
cfclose(_cfp);
_cfp = nullptr;
};
bool PresetFileHandler::beginSectionRead(Section s) {
const char * sectionName = lookupSectionName(s);
ensureExists(sectionName);
auto section = json_object_get(_currentEl, sectionName);
if (json_typeof(section) != JSON_OBJECT) {
Error(LOCATION, "Section must be a JSON object!");
return false;
}
pushElement(section);
return true;
};
void PresetFileHandler::beginSectionWrite(Section s) {
auto key_name = lookupSectionName(s);
json_t* obj = json_object();
if (json_is_array(_currentEl)) {
// Currently in an array, section must be unnamed
Assertion(key_name == nullptr, "Elements of array may not be named sections!");
json_array_append_new(_currentEl, obj);
} else {
Assertion(key_name != nullptr, "Section outside of arrays must be named!");
json_object_set_new(_currentEl, key_name, obj);
}
pushElement(obj);
};
bool PresetFileHandler::beginArrayRead(Section s, size_t &size) {
Assertion(_arrayIndex == INVALID_SIZE, "Array nesting is not supported yet!");
const char * name = lookupSectionName(s);
ensureExists(name);
auto array = json_object_get(_currentEl, name);
if (json_typeof(array) != JSON_ARRAY) {
Error(LOCATION, "Expected an array for '%s' but it was a different type!", name);
return false;
}
pushElement(array);
size = json_array_size(array);
if (size == 0) {
// Nothing to do here, avoid calling nextArraySection since that assumes that there is at least one element
return true;
} // Else, advance to the first element in the array
_arrayIndex = 0;
nextArrayElement();
return true;
}
void PresetFileHandler::beginArrayWrite(Section s) {
const char * name = lookupSectionName(s);
auto array = json_array();
if (json_is_array(_currentEl)) {
// We are in an array, section must be unnamed
Assertion(name == nullptr, "Inside an array there can be no named section!");
json_array_append_new(_currentEl, array);
} else {
Assertion(name != nullptr, "Section outside of arrays must be named!");
json_object_set_new(_currentEl, name, array);
}
pushElement(array);
}
void PresetFileHandler::endSectionRead() {
Assertion(json_typeof(_currentEl) == JSON_OBJECT, "Current element for section reading is not an object!");
popElement();
};
void PresetFileHandler::endSectionWrite() {
Assertion(json_is_object(_currentEl), "Section ended while not in a section!");
popElement();
};
void PresetFileHandler::endArrayRead() {
// First, remove the element we are currently in if it exists
if (json_typeof(_currentEl) != JSON_ARRAY) {
popElement();
}
Assertion(json_typeof(_currentEl) == JSON_ARRAY, "Current element must be an array!");
// TODO Sections are straightforward, but Arrays may need special consideration re: index.
popElement();
_arrayIndex = INVALID_SIZE;
};
void PresetFileHandler::endArrayWrite() {
Assertion(json_is_array(_currentEl), "Array ended while not in an array!");
popElement();
};
void PresetFileHandler::ensureExists(const char* name) {
if (json_typeof(_currentEl) != JSON_OBJECT) {
Error(LOCATION, "JSON reading requires a value with name '%s' but the current element is not an object!", name);
}
if (json_object_get(_currentEl, name) == nullptr) {
Error(LOCATION, "JSON reading requires a value with name '%s' but there is no such value!", name);
}
}
void PresetFileHandler::ensureNotExists(const char* name) {
// Stuff debug checks into booleans, otherwise clang will trigger a false positive for static method with just the asserts
// Make sure we're in an element that can support keys
bool supports_keys = json_is_object(_currentEl);
// Make sure we don't overwrite previous values
bool element_is_unique = json_object_get(_currentEl, name) == nullptr;
Assertion(supports_keys, "Currently not in an element that supports keys!");
Assertion(element_is_unique, "Entry with name %s already exists!", name);
}
bool PresetFileHandler::exists(const char* name) {
if (json_typeof(_currentEl) != JSON_OBJECT) {
return false;
}
if (json_object_get(_currentEl, name) == nullptr) {
return false;
}
return true;
}
void PresetFileHandler::flush() {
Assertion(_elementStack.size() == 1, "Not all sections or arrays have been ended!");
json_dump_cfile(_rootObj, _cfp, JSON_INDENT(4) | JSON_PRESERVE_ORDER);
}
void PresetFileHandler::nextArrayElement() {
Assertion(_arrayIndex != INVALID_SIZE, "Array index must be valid for this function!");
bool in_section = json_is_object(_currentEl);
if (in_section) {
// We have to pop the previous section first
popElement();
Assertion(json_typeof(_currentEl) == JSON_ARRAY, "The previous element should have been an array!");
}
auto max = json_array_size(_currentEl);
Assertion(_arrayIndex <= max, "Invalid array index detected!");
// Silently ignore if we are one past the last element since this function is used in a for loop where this function
// is executed one time after the index is incremented past the last element
if (_arrayIndex == max) {
// Increment the index so we catch usage errors the next time someone tries to call this function
++_arrayIndex;
return;
}
// We use the current array index and then increment it to avoid skipping the first element
auto el = json_array_get(_currentEl, _arrayIndex);
++_arrayIndex;
pushElement(el);
}
json_t * PresetFileHandler::popElement() {
Assertion(_elementStack.size() > 1, "Element stack may not get smaller than one element!");
_elementStack.pop_back();
_currentEl = _elementStack.back();
return _currentEl;
}
void PresetFileHandler::pushElement(json_t* el) {
Assertion(el != nullptr, "Invalid JSON element pointer passed!");
_currentEl = el;
_elementStack.push_back(_currentEl);
}
SCP_string PresetFileHandler::readString(const char* key) {
auto el = json_object_get(_currentEl, key);
if (el == nullptr || json_typeof(el) != JSON_STRING) {
Error(LOCATION, "JSON element %s must be a string but it is not valid!", key);
return SCP_string();
}
auto json_str = json_string_value(el);
SCP_string val;
val.assign(json_str, json_str + json_string_length(el));
return val;
}
int PresetFileHandler::readInt(const char* key) {
auto el = json_object_get(_currentEl, key);
if (el == nullptr || json_typeof(el) != JSON_INTEGER) {
Error(LOCATION, "JSON element %s must be an integer but it is not valid!", key);
return 0;
}
return static_cast<int>(json_integer_value(el));
}
void PresetFileHandler::writeString(const char* key, SCP_string s) {
ensureNotExists(key);
json_t *jstr = nullptr;
if (!s.empty()) {
// if this string isn't proper UTF-8, try to convert it
if (utf8::find_invalid(s.begin(), s.end()) != s.end()) {
SCP_string buffer;
coerce_to_utf8(buffer, s.c_str());
jstr = json_string(buffer.c_str());
} else {
jstr = json_string(s.c_str());
}
}
json_object_set_new(_currentEl, key, jstr);
};
void PresetFileHandler::writeInt(const char* key, int val) {
ensureNotExists(key);
json_object_set_new(_currentEl, key, json_integer(val));
}