mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
* CC_preset: Prevent post-construction type modification and handle pst duplication * remove redundant function * CC_preset: remove default constructor, require explicit type * remove duplicate
530 lines
15 KiB
C++
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));
|
|
}
|