mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
- Clean up `check_sexp_syntax` to use `eval_ship` (and incidentally `eval_wing` and `eval_prop`) rather than the old lookups. - Disable SEXP node caching while FRED is running. Also use smart pointers and fix a cache memory leak.
1781 lines
52 KiB
C++
1781 lines
52 KiB
C++
/*
|
|
* Created by Hassan "Karajorma" Kazmi and Josh "jg18" Glatt for The FreeSpace 2 Source Code Project.
|
|
* You may not sell or otherwise commercially exploit the source or things you
|
|
* create based on the source.
|
|
*/
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <iterator>
|
|
|
|
#include "gamesequence/gamesequence.h"
|
|
#include "globalincs/pstypes.h"
|
|
#include "mission/missiongoals.h"
|
|
#include "parse/generic_log.h"
|
|
#include "parse/parselo.h"
|
|
#include "parse/sexp_container.h"
|
|
#include "utils/Random.h"
|
|
|
|
static SCP_vector<sexp_container> Sexp_containers;
|
|
|
|
|
|
const SCP_vector<sexp_container> &get_all_sexp_containers()
|
|
{
|
|
return Sexp_containers;
|
|
}
|
|
|
|
static const SCP_vector<list_modifier> List_modifiers = {
|
|
{ "Get_First", ListModifier::GET_FIRST },
|
|
{ "Get_Last", ListModifier::GET_LAST },
|
|
{ "Remove_First", ListModifier::REMOVE_FIRST },
|
|
{ "Remove_Last", ListModifier::REMOVE_LAST },
|
|
{ "Get_Random", ListModifier::GET_RANDOM },
|
|
{ "Remove_Random", ListModifier::REMOVE_RANDOM },
|
|
{ "At", ListModifier::AT_INDEX }
|
|
};
|
|
|
|
const SCP_vector<list_modifier>& get_all_list_modifiers()
|
|
{
|
|
return List_modifiers;
|
|
}
|
|
|
|
namespace {
|
|
const char *Empty_str = "";
|
|
|
|
void
|
|
report_problematic_container(const SCP_string &sexp_name, const SCP_string &problem, const char *container_name)
|
|
{
|
|
const SCP_string msg = sexp_name + " called on " + problem + " container " + container_name;
|
|
Warning(LOCATION, "%s", msg.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
}
|
|
|
|
void report_nonexistent_container(const SCP_string &sexp_name, const char *container_name)
|
|
{
|
|
report_problematic_container(sexp_name, "nonexistent", container_name);
|
|
}
|
|
|
|
void report_non_list_container(const SCP_string &sexp_name, const char *container_name)
|
|
{
|
|
report_problematic_container(sexp_name, "non-list", container_name);
|
|
}
|
|
|
|
void report_non_map_container(const SCP_string &sexp_name, const char *container_name)
|
|
{
|
|
report_problematic_container(sexp_name, "non-map", container_name);
|
|
}
|
|
|
|
void report_container_used_in_special_arg(const SCP_string &op_name, const char *container_name)
|
|
{
|
|
const SCP_string msg =
|
|
op_name + " called on container " + container_name + " while it is being used in a special argument SEXP";
|
|
Warning(LOCATION, "%s", msg.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
}
|
|
|
|
void add_to_map_internal(sexp_container &container, const SCP_string &key, const SCP_string &data) {
|
|
Assertion(container.is_map(),
|
|
"Attempt to add map data to non-map container %s. Please report!",
|
|
container.container_name.c_str());
|
|
|
|
auto &map_data = container.map_data;
|
|
|
|
#if !defined(NDEBUG) || defined(SCP_RELEASE_LOGGING)
|
|
if (map_data.find(key) != map_data.end()) {
|
|
// replacing an existing key's data isn't an error but should be logged
|
|
const SCP_string msg = SCP_string("Key '") + key + "' already exists in map container " +
|
|
container.container_name + ". Replacing its data.";
|
|
mprintf(("%s\n", msg.c_str()));
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
}
|
|
#endif
|
|
map_data[key] = data;
|
|
}
|
|
|
|
// Containers should not be modified if the game is simply checking the syntax.
|
|
bool are_containers_modifiable()
|
|
{
|
|
// can always modify containers during the mission itself
|
|
if (Game_mode & GM_IN_MISSION) {
|
|
return true;
|
|
}
|
|
|
|
// can also modify if we are calling the sexp from a script or if we are briefing / debriefing, etc.
|
|
switch (gameseq_get_state()) {
|
|
case GS_STATE_BRIEFING:
|
|
case GS_STATE_DEBRIEF:
|
|
case GS_STATE_CMD_BRIEF:
|
|
case GS_STATE_FICTION_VIEWER:
|
|
case GS_STATE_SCRIPTING:
|
|
case GS_STATE_SCRIPTING_MISSION:
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const SCP_string Remove_op_prefix = "Remove_";
|
|
} // namespace
|
|
|
|
// map_container_hash impl
|
|
|
|
uint32_t map_container_hash::operator()(const SCP_string &str) const
|
|
{
|
|
// hash function recipe from Effective Java, Item 9
|
|
// occasional overflow is expected and not a problem
|
|
// map key matching is case-sensitive
|
|
uint32_t result = 42u; // arbitrary non-zero number
|
|
for (char c : str) {
|
|
result = result * 31 + c;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// sexp_container data and functions
|
|
const SCP_string sexp_container::DELIM_STR(1, sexp_container::DELIM);
|
|
const SCP_string sexp_container::NAME_NODE_PREFIX(2, sexp_container::DELIM);
|
|
|
|
bool sexp_container::operator==(const sexp_container &sc) const
|
|
{
|
|
return container_name == sc.container_name && type == sc.type && opf_type == sc.opf_type &&
|
|
list_data == sc.list_data && map_data == sc.map_data;
|
|
}
|
|
|
|
ContainerType sexp_container::get_data_type() const
|
|
{
|
|
return type & (ContainerType::STRING_DATA | ContainerType::NUMBER_DATA);
|
|
}
|
|
|
|
bool sexp_container::name_matches(const sexp_container &container) const
|
|
{
|
|
return lcase_equal(container.container_name, container_name);
|
|
}
|
|
|
|
bool sexp_container::empty() const
|
|
{
|
|
return size() == 0;
|
|
}
|
|
|
|
int sexp_container::size() const
|
|
{
|
|
if (is_list()) {
|
|
return (int)list_data.size();
|
|
} else if (is_map()) {
|
|
return (int)map_data.size();
|
|
} else {
|
|
UNREACHABLE("Unknown container type %d", static_cast<int>(type));
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static constexpr ContainerType CONTAINER_PERSISTENCE_FLAGS =
|
|
ContainerType::SAVE_ON_MISSION_CLOSE | ContainerType::SAVE_ON_MISSION_PROGRESS | ContainerType::NETWORK |
|
|
ContainerType::SAVE_TO_PLAYER_FILE;
|
|
|
|
ContainerType sexp_container::get_non_persistent_type() const
|
|
{
|
|
return type & ~CONTAINER_PERSISTENCE_FLAGS;
|
|
}
|
|
|
|
bool sexp_container::type_matches(const sexp_container &container) const
|
|
{
|
|
return get_non_persistent_type() == container.get_non_persistent_type();
|
|
}
|
|
|
|
bool sexp_container::is_of_string_type() const
|
|
{
|
|
return (is_list() && any(type & ContainerType::STRING_DATA)) ||
|
|
(is_map() && any(type & ContainerType::STRING_KEYS));
|
|
}
|
|
|
|
const SCP_string &sexp_container::get_value_at_index(int index) const
|
|
{
|
|
Assertion(index >= 0 && index < size(),
|
|
"Attempt to get out-of-range index %d from container %s (size %d). Please report!",
|
|
index,
|
|
container_name.c_str(),
|
|
size());
|
|
|
|
if (is_list()) {
|
|
return *std::next(list_data.cbegin(), index);
|
|
} else if (is_map()) {
|
|
// should produce the same value on all platforms
|
|
// since map_data uses a custom hash function
|
|
return std::next(map_data.cbegin(), index)->first;
|
|
} else {
|
|
UNREACHABLE("Container %s has invalid type (%d). Please report!", container_name.c_str(), static_cast<int>(type));
|
|
return *list_data.cbegin(); // gibberish
|
|
}
|
|
}
|
|
|
|
SCP_string sexp_container::apply_list_modifier(ListModifier modifier, int data_index)
|
|
{
|
|
Assertion(is_list(),
|
|
"Attempt to call list modifier (%d) on non-list container %s (type %d). Please report!",
|
|
(int)modifier,
|
|
container_name.c_str(),
|
|
(int)type);
|
|
Assertion(!list_data.empty(),
|
|
"Attempt to call list modifier (%d) on empty list container %s. Please report!",
|
|
(int)modifier,
|
|
container_name.c_str());
|
|
|
|
switch (modifier) {
|
|
case ListModifier::GET_FIRST:
|
|
case ListModifier::REMOVE_FIRST:
|
|
return list_get_first(modifier == ListModifier::REMOVE_FIRST);
|
|
|
|
case ListModifier::GET_LAST:
|
|
case ListModifier::REMOVE_LAST:
|
|
return list_get_last(modifier == ListModifier::REMOVE_LAST);
|
|
|
|
case ListModifier::GET_RANDOM:
|
|
case ListModifier::REMOVE_RANDOM:
|
|
return list_get_random(modifier == ListModifier::REMOVE_RANDOM);
|
|
|
|
case ListModifier::AT_INDEX:
|
|
return list_get_at(data_index);
|
|
|
|
default:
|
|
UNREACHABLE("Unhandled list modifier %d. Please report!", static_cast<int>(modifier));
|
|
return "";
|
|
}
|
|
}
|
|
|
|
SCP_string sexp_container::list_get_first(bool remove)
|
|
{
|
|
return list_apply_iterator(list_data.begin(), "First", remove);
|
|
}
|
|
|
|
SCP_string sexp_container::list_get_last(bool remove)
|
|
{
|
|
return list_apply_iterator(--list_data.end(), "Last", remove);
|
|
}
|
|
|
|
SCP_string sexp_container::list_get_random(bool remove)
|
|
{
|
|
const int rand_index = util::Random::next((int)list_data.size());
|
|
auto list_it = std::next(list_data.begin(), rand_index);
|
|
return list_apply_iterator(list_it, "Random", remove);
|
|
}
|
|
|
|
SCP_string sexp_container::list_get_at(int index)
|
|
{
|
|
Assertion(index >= 0 && index < (int)list_data.size(),
|
|
"Attempt to use At list modifier for container %s with invalid index %d. Please report!",
|
|
container_name.c_str(),
|
|
index);
|
|
|
|
auto list_it = std::next(list_data.begin(), index);
|
|
return list_apply_iterator(list_it, "At", false);
|
|
}
|
|
|
|
SCP_string sexp_container::list_apply_iterator(SCP_list<SCP_string>::iterator list_it, const char *location, bool remove)
|
|
{
|
|
Assertion(list_it != list_data.end(),
|
|
"Attempt to access invalid position in list container %s at location %s. Please report!",
|
|
container_name.c_str(),
|
|
location);
|
|
|
|
SCP_string result = *list_it;
|
|
|
|
if (remove && are_containers_modifiable()) {
|
|
if (is_being_used_in_special_arg()) {
|
|
report_container_used_in_special_arg(Remove_op_prefix + location, container_name.c_str());
|
|
} else {
|
|
list_data.erase(list_it);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// ListModifier-related functions
|
|
|
|
ListModifier get_list_modifier(const char *text, bool accept_prefix)
|
|
{
|
|
Assertion(text != nullptr, "Attempt to get list modifier from null name. Please report!");
|
|
|
|
for (const auto &modifier_obj : List_modifiers) {
|
|
if (accept_prefix && !strnicmp(modifier_obj.name, text, strlen(modifier_obj.name))) {
|
|
return modifier_obj.modifier;
|
|
} else if (!accept_prefix && !stricmp(modifier_obj.name, text)) {
|
|
return modifier_obj.modifier;
|
|
}
|
|
}
|
|
|
|
// not found
|
|
return ListModifier::INVALID;;
|
|
}
|
|
|
|
const char *get_list_modifier_name(const ListModifier modifier)
|
|
{
|
|
Assertion(modifier != ListModifier::INVALID, "Attempt to get name of invalid list modifier. Please report!");
|
|
|
|
for (const auto &modifier_obj : List_modifiers) {
|
|
if (modifier_obj.modifier == modifier) {
|
|
return modifier_obj.name;
|
|
}
|
|
}
|
|
|
|
Assertion(false, "get_list_modifier_name() given unknown modifier %d", static_cast<int>(modifier));
|
|
return Empty_str;
|
|
}
|
|
|
|
// sexp_container.h functions
|
|
|
|
/**
|
|
* Clear the SEXP Containers and related data
|
|
*/
|
|
void init_sexp_containers()
|
|
{
|
|
Sexp_containers.clear();
|
|
}
|
|
|
|
void update_sexp_containers(SCP_vector<sexp_container> &containers,
|
|
const SCP_unordered_map<SCP_string, SCP_string, SCP_string_lcase_hash, SCP_string_lcase_equal_to>
|
|
&renamed_containers)
|
|
{
|
|
Sexp_containers = std::move(containers);
|
|
|
|
if (!renamed_containers.empty()) {
|
|
for (int i = 0; i < Num_sexp_nodes; i++) {
|
|
auto &node = Sexp_nodes[i];
|
|
if (node.type == SEXP_ATOM &&
|
|
(node.subtype == SEXP_ATOM_CONTAINER_NAME || node.subtype == SEXP_ATOM_CONTAINER_DATA)) {
|
|
const auto new_name_it = renamed_containers.find(node.text);
|
|
if (new_name_it != renamed_containers.cend()) {
|
|
strcpy_s(node.text, new_name_it->second.c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool stuff_one_generic_sexp_container(SCP_string &name, ContainerType &type, int &opf_type, SCP_vector<SCP_string> &data)
|
|
{
|
|
bool valid = true; // try to skip past bad containers by not returning until end of function
|
|
SCP_string temp_type_string;
|
|
|
|
data.clear();
|
|
|
|
required_string("$Name:");
|
|
stuff_string(name, F_NAME);
|
|
|
|
if (name.empty()) {
|
|
Warning(LOCATION, "SEXP Container with empty name found");
|
|
log_printf(LOGFILE_EVENT_LOG, "SEXP Container with empty name found");
|
|
valid = false;
|
|
} else if (name.find(' ') != SCP_string::npos || name.find(sexp_container::DELIM) != SCP_string::npos) {
|
|
Warning(LOCATION,
|
|
"SEXP Container name %s contains spaces and/or %c characters",
|
|
name.c_str(),
|
|
sexp_container::DELIM);
|
|
log_printf(LOGFILE_EVENT_LOG,
|
|
"SEXP Container name %s contains spaces and/or %c characters",
|
|
name.c_str(),
|
|
sexp_container::DELIM);
|
|
valid = false;
|
|
} else if (name.length() > sexp_container::NAME_MAX_LENGTH) {
|
|
Warning(LOCATION,
|
|
"SEXP Container name %s is longer than limit %d",
|
|
name.c_str(),
|
|
sexp_container::NAME_MAX_LENGTH);
|
|
log_printf(LOGFILE_EVENT_LOG,
|
|
"SEXP Container name %s is longer than limit %d",
|
|
name.c_str(),
|
|
sexp_container::NAME_MAX_LENGTH);
|
|
valid = false;
|
|
} else if (get_sexp_container(name.c_str()) != nullptr) {
|
|
Warning(LOCATION, "A SEXP Container named %s already exists", name.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "A SEXP Container named %s already exists", name.c_str());
|
|
valid = false;
|
|
}
|
|
|
|
required_string("$Data Type:");
|
|
stuff_string(temp_type_string, F_NAME);
|
|
if (!stricmp(temp_type_string.c_str(), "Number")) {
|
|
type |= ContainerType::NUMBER_DATA;
|
|
opf_type = OPF_NUMBER;
|
|
} else if (!stricmp(temp_type_string.c_str(), "String")) {
|
|
type |= ContainerType::STRING_DATA;
|
|
opf_type = OPF_STRING;
|
|
} else {
|
|
Warning(LOCATION, "Unknown SEXP Container type %s found", temp_type_string.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "Unknown SEXP Container type %s found", temp_type_string.c_str());
|
|
valid = false;
|
|
}
|
|
|
|
if (optional_string("$Key Type:")) {
|
|
Assertion(any(type & ContainerType::MAP), "$Key Type: found for container which doesn't use keys!");
|
|
|
|
stuff_string(temp_type_string, F_NAME);
|
|
if (!stricmp(temp_type_string.c_str(), "Number")) {
|
|
type |= ContainerType::NUMBER_KEYS;
|
|
} else if (!stricmp(temp_type_string.c_str(), "String")) {
|
|
type |= ContainerType::STRING_KEYS;
|
|
} else {
|
|
Warning(LOCATION, "Unknown SEXP Container type %s found", temp_type_string.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "Unknown SEXP Container type %s found", temp_type_string.c_str());
|
|
valid = false;
|
|
}
|
|
}
|
|
|
|
if (optional_string("+Strictly Typed Keys")) {
|
|
Assertion(any(type & ContainerType::MAP), "+Strictly Typed Keys found for container which doesn't use keys!");
|
|
Warning(LOCATION, "Container %s is marked for strictly typed keys, which are not yet supported.", name.c_str());
|
|
type |= ContainerType::STRICTLY_TYPED_KEYS;
|
|
}
|
|
|
|
if (optional_string("+Strictly Typed Data")) {
|
|
Warning(LOCATION, "Container %s is marked for strictly typed data, which are not yet supported.", name.c_str());
|
|
type |= ContainerType::STRICTLY_TYPED_DATA;
|
|
}
|
|
|
|
required_string("$Data:");
|
|
stuff_string_list(data);
|
|
|
|
if (optional_string("+Network Container")) {
|
|
Warning(LOCATION, "Container %s is marked as a network container, which is not yet supported.", name.c_str());
|
|
type |= ContainerType::NETWORK;
|
|
}
|
|
|
|
if (optional_string("+Eternal")) {
|
|
type |= ContainerType::SAVE_TO_PLAYER_FILE;
|
|
}
|
|
|
|
// campaign-persistent
|
|
if (optional_string("+Save On Mission Progress")) {
|
|
type |= ContainerType::SAVE_ON_MISSION_PROGRESS;
|
|
}
|
|
|
|
// player-persistent
|
|
if (optional_string("+Save On Mission Close")) {
|
|
Assert(none(type & ContainerType::SAVE_ON_MISSION_PROGRESS));
|
|
type |= ContainerType::SAVE_ON_MISSION_CLOSE;
|
|
}
|
|
|
|
Assert(none(type & ContainerType::SAVE_TO_PLAYER_FILE) ||
|
|
(any(type & ContainerType::SAVE_ON_MISSION_PROGRESS) ^ any(type & ContainerType::SAVE_ON_MISSION_CLOSE)));
|
|
|
|
return valid;
|
|
}
|
|
|
|
void stuff_sexp_list_containers()
|
|
{
|
|
SCP_vector<SCP_string> parsed_data;
|
|
|
|
while (required_string_either("$End Lists", "$Name:")) {
|
|
sexp_container new_list;
|
|
|
|
new_list.type = ContainerType::LIST;
|
|
if (stuff_one_generic_sexp_container(new_list.container_name, new_list.type, new_list.opf_type, parsed_data)) {
|
|
std::copy(parsed_data.begin(), parsed_data.end(), back_inserter(new_list.list_data));
|
|
Sexp_containers.emplace_back(std::move(new_list));
|
|
}
|
|
}
|
|
}
|
|
|
|
void stuff_sexp_map_containers()
|
|
{
|
|
SCP_vector<SCP_string> parsed_data;
|
|
|
|
while (required_string_either("$End Maps", "$Name:")) {
|
|
sexp_container new_map;
|
|
|
|
new_map.type = ContainerType::MAP;
|
|
if (stuff_one_generic_sexp_container(new_map.container_name, new_map.type, new_map.opf_type, parsed_data)) {
|
|
if (parsed_data.size() % 2 != 0) {
|
|
Warning(LOCATION,
|
|
"Data in the SEXP Map container is corrupt. Must be an even number of entries. Instead have %d",
|
|
(int)parsed_data.size());
|
|
log_printf(LOGFILE_EVENT_LOG,
|
|
"Data in the SEXP Map container is corrupt. Must be an even number of entries. Instead have %d",
|
|
(int)parsed_data.size());
|
|
} else {
|
|
for (int i = 0; i < (int)parsed_data.size(); i += 2) {
|
|
new_map.map_data.emplace(parsed_data[i], parsed_data[i + 1]);
|
|
}
|
|
Sexp_containers.emplace_back(std::move(new_map));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sexp_container *get_sexp_container(const char *name) {
|
|
Assertion(name != nullptr, "Attempt to look up container from null name. Please report!");
|
|
// unlike get_sexp_container_special(), empty name is ok
|
|
// even if the result will always be nullptr
|
|
// it can happen if CTEXT() returns empty str
|
|
|
|
for (auto &container : Sexp_containers) {
|
|
if (!stricmp(name, container.container_name.c_str())) {
|
|
return &container;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
/**
|
|
* Tests whether this position in a character array is the start of a container name
|
|
* Returns a pointer to the container or nullptr if not found
|
|
**/
|
|
sexp_container *get_sexp_container_special(const SCP_string &text, size_t start_pos)
|
|
{
|
|
Assert(!text.empty());
|
|
Assert(start_pos < text.length());
|
|
|
|
for (auto &container : Sexp_containers) {
|
|
// in case one container's name is a prefix of another's, include terminating DELIM
|
|
const SCP_string name_to_find = container.container_name + sexp_container::DELIM_STR;
|
|
if (!strnicmp(text.c_str() + start_pos, name_to_find.c_str(), name_to_find.length())) {
|
|
return &container;
|
|
}
|
|
}
|
|
|
|
// not found
|
|
return nullptr;
|
|
}
|
|
|
|
/**
|
|
* Helper function for sexp_container_replace_refs_with_values(). Given a SEXP Container it works out what modifier was used and what the replacement string should be.
|
|
**/
|
|
bool get_replace_text_for_modifier(const SCP_string &text,
|
|
sexp_container &container,
|
|
const size_t lookHere,
|
|
SCP_string &replacement_text,
|
|
size_t &num_chars_to_replace)
|
|
{
|
|
// for map containers, check if this matches a map key
|
|
if (container.is_map()) {
|
|
if (container.map_data.empty()) {
|
|
Warning(LOCATION, "Attempt to replace text using empty map container %s", container.container_name.c_str());
|
|
return false;
|
|
}
|
|
|
|
const size_t key_ends_here = text.find(sexp_container::DELIM, lookHere);
|
|
if (key_ends_here == SCP_string::npos) {
|
|
Warning(LOCATION,
|
|
"Attempt to replace text from map container %s but the key is "
|
|
"missing a final '&'",
|
|
container.container_name.c_str());
|
|
return false;
|
|
}
|
|
|
|
const SCP_string key = text.substr(lookHere, key_ends_here - lookHere);
|
|
const auto iter = container.map_data.find(key);
|
|
|
|
if (iter == container.map_data.end()) {
|
|
Warning(LOCATION,
|
|
"Attempt to replace text from map container %s with nonexistent key %s",
|
|
container.container_name.c_str(),
|
|
key.c_str());
|
|
return false;
|
|
}
|
|
|
|
replacement_text = iter->second;
|
|
|
|
num_chars_to_replace += key.length();
|
|
} else {
|
|
// for list containers, check if this matches a list modifier
|
|
Assertion(container.is_list(),
|
|
"Attempt to replace text using container %s with unknwon type %d. Please report!",
|
|
container.container_name.c_str(),
|
|
(int)container.type);
|
|
|
|
if (container.list_data.empty()) {
|
|
Warning(LOCATION, "Attempt to replace text using empty list container %s", container.container_name.c_str());
|
|
return false;
|
|
}
|
|
|
|
const auto modifier = get_list_modifier(text.c_str() + lookHere, true);
|
|
|
|
if (modifier == ListModifier::INVALID) {
|
|
Warning(LOCATION,
|
|
"Attempt to replace text from list container %s using unrecognized list modifier",
|
|
container.container_name.c_str());
|
|
return false;
|
|
}
|
|
|
|
const size_t modifier_length = strlen(get_list_modifier_name(modifier));
|
|
int data_index = -1;
|
|
size_t number_length = 0;
|
|
|
|
if (modifier == ListModifier::AT_INDEX) {
|
|
SCP_string number_string = text.substr(lookHere + modifier_length);
|
|
if (number_string.empty()) {
|
|
Warning(LOCATION,
|
|
"Container text replacement found empty index into list container %s",
|
|
container.container_name.c_str());
|
|
return false;
|
|
}
|
|
|
|
data_index = atoi(number_string.c_str());
|
|
|
|
if (data_index < 0 || data_index >= (int)container.list_data.size()) {
|
|
Warning(LOCATION,
|
|
"Container text replacement found invalid index '%s' into list container %s",
|
|
number_string.c_str(),
|
|
container.container_name.c_str());
|
|
return false;
|
|
}
|
|
|
|
// include spaces, in case the FREDder put one between "At" and the index
|
|
number_length = strspn(number_string.c_str(), " 0123456789");
|
|
}
|
|
|
|
replacement_text = container.apply_list_modifier(modifier, data_index);
|
|
|
|
num_chars_to_replace += modifier_length;
|
|
|
|
// for the "At" modifier, we need to include the chars used by the index
|
|
if (modifier == ListModifier::AT_INDEX) {
|
|
num_chars_to_replace += number_length;
|
|
}
|
|
}
|
|
|
|
// account for the final '&'
|
|
++num_chars_to_replace;
|
|
|
|
return true;
|
|
}
|
|
|
|
// inspired by sexp_replace_variable_names_with_values()
|
|
/**
|
|
* Replace container references in a string with their values
|
|
**/
|
|
bool sexp_container_replace_refs_with_values(SCP_string &text)
|
|
{
|
|
bool replaced_anything = false;
|
|
size_t lookHere = 0;
|
|
int num_replacements = 0;
|
|
|
|
while (lookHere < text.length()) {
|
|
// look for the meta-character
|
|
size_t foundHere = text.find(sexp_container::DELIM, lookHere);
|
|
|
|
// found?
|
|
if (foundHere != SCP_string::npos && foundHere != text.length() - 1) {
|
|
// see if a container starts at the next char
|
|
auto *p_container = get_sexp_container_special(text, foundHere + 1);
|
|
if (p_container != nullptr) {
|
|
// we want to replace either &list_container&list_modifier& or &map_container&map_key&
|
|
|
|
// + 2 for the enclosing '&' chars around the container name
|
|
size_t num_chars_to_replace = p_container->container_name.length() + 2;
|
|
|
|
// make sure the enclosing '&' appears after the container name
|
|
size_t expected_amp_pos = foundHere + num_chars_to_replace - 1;
|
|
if (expected_amp_pos >= text.length() || text[expected_amp_pos] != sexp_container::DELIM) {
|
|
// no modifier/key specified - ignore this matching container name, as the replacement is malformed
|
|
lookHere = foundHere + 1;
|
|
Warning(LOCATION,
|
|
"Attempt to replace text from container %s with no modifier. "
|
|
"This format is expected for lists &Container_Name&Modifier&. This "
|
|
"format is expected for maps &Container_Name&Key&",
|
|
p_container->container_name.c_str());
|
|
continue;
|
|
}
|
|
|
|
size_t modifierLookHere = foundHere + num_chars_to_replace;
|
|
|
|
SCP_string replacement_text;
|
|
|
|
if (!get_replace_text_for_modifier(text, *p_container, modifierLookHere, replacement_text, num_chars_to_replace)) {
|
|
// list modifier/map key was invalid
|
|
// skip past the text we were unable to replace
|
|
lookHere = foundHere + num_chars_to_replace;
|
|
continue;
|
|
}
|
|
|
|
// perform the replacement
|
|
text.replace(foundHere, num_chars_to_replace, replacement_text);
|
|
replaced_anything = true;
|
|
|
|
++num_replacements;
|
|
// avoid potential infinite loops
|
|
if (num_replacements >= 50) {
|
|
Warning(LOCATION,
|
|
"Container text replacement found potential infinite loop in multidimensionality "
|
|
"in '%s' at position %d.",
|
|
text.c_str(),
|
|
(int)lookHere);
|
|
break;
|
|
}
|
|
|
|
// don't go past the replacement point, because if container multidimensionality
|
|
// is being used, then the resulting text will contain another container ref
|
|
lookHere = foundHere;
|
|
} else { // no container name found
|
|
lookHere = foundHere + 1;
|
|
}
|
|
} else {
|
|
// no potential metacharacter found, so we're done replacing
|
|
break;
|
|
}
|
|
}
|
|
|
|
return replaced_anything;
|
|
}
|
|
|
|
bool sexp_container_replace_refs_with_values(char *text, size_t max_len)
|
|
{
|
|
Assertion(text != nullptr, "Attempt to perform container text replacement with null text. Please report!");
|
|
|
|
SCP_string text_str = text;
|
|
|
|
bool replaced_anything = sexp_container_replace_refs_with_values(text_str);
|
|
|
|
if (replaced_anything) {
|
|
Assertion(max_len > 0, "Attempt to perform container text replacement with zero-length allowed string. Please report!");
|
|
strncpy(text, text_str.c_str(), max_len);
|
|
text[max_len] = 0;
|
|
}
|
|
|
|
return replaced_anything;
|
|
}
|
|
|
|
bool sexp_container_CTEXT_helper(int &node, sexp_container &container, SCP_string &result)
|
|
{
|
|
if (container.is_map()) {
|
|
if (container.map_data.empty()) {
|
|
Warning(LOCATION, "Attempt to retrieve data from empty map container %s", container.container_name.c_str());
|
|
return false;
|
|
}
|
|
|
|
const char *key = CTEXT(node);
|
|
const auto value_it = container.map_data.find(SCP_string(key));
|
|
|
|
if (value_it != container.map_data.end()) {
|
|
result = value_it->second;
|
|
return true;
|
|
} else {
|
|
if (Log_event) {
|
|
SCP_string log_string = container.container_name;
|
|
log_string.append(" map container returned nothing when searched for key ");
|
|
log_string.append(key);
|
|
Current_event_log_container_buffer->emplace_back(log_string);
|
|
}
|
|
return false;
|
|
}
|
|
} else if (container.is_list()) {
|
|
if (container.list_data.empty()) {
|
|
Warning(LOCATION, "Attempt to retrieve data from empty list container %s", container.container_name.c_str());
|
|
return false;
|
|
}
|
|
|
|
const char *modifier_name = CTEXT(node);
|
|
const auto modifier = get_list_modifier(modifier_name);
|
|
|
|
if (modifier == ListModifier::INVALID) {
|
|
const SCP_string &container_name = container.container_name;
|
|
Warning(LOCATION,
|
|
"Illegal operation attempted on %s container. There is no modifier called %s.",
|
|
container_name.c_str(),
|
|
modifier_name);
|
|
log_printf(LOGFILE_EVENT_LOG,
|
|
"Illegal operation attempted on %s container. There is no modifier called %s.",
|
|
container_name.c_str(),
|
|
modifier_name);
|
|
return false;
|
|
}
|
|
|
|
int data_index = -1;
|
|
|
|
if (modifier == ListModifier::AT_INDEX) {
|
|
node = CDR(node);
|
|
if (node == -1) {
|
|
return false;
|
|
}
|
|
|
|
bool is_nan, is_nan_forever;
|
|
data_index = eval_num(node, is_nan, is_nan_forever);
|
|
if (is_nan || is_nan_forever) {
|
|
return false;
|
|
}
|
|
|
|
if (data_index < 0 || data_index >= (int)container.list_data.size()) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
result = container.apply_list_modifier(modifier, data_index);
|
|
return true;
|
|
} else {
|
|
UNREACHABLE("Container %s has invalid type (%d). Please report!",
|
|
container.container_name.c_str(),
|
|
static_cast<int>(container.type));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const char *sexp_container_CTEXT(int node)
|
|
{
|
|
auto *p_container = get_sexp_container(Sexp_nodes[node].text);
|
|
|
|
if (!p_container) {
|
|
Warning(LOCATION, "sexp_container_CTEXT() called for %s, a container which does not exist!", Sexp_nodes[node].text);
|
|
log_printf(LOGFILE_EVENT_LOG, "sexp_container_CTEXT() called for %s, a container which does not exist!", Sexp_nodes[node].text);
|
|
return Empty_str;
|
|
}
|
|
|
|
node = CAR(node);
|
|
Assert(node != -1);
|
|
|
|
SCP_string result;
|
|
|
|
for (int sanity = 0; sanity < 20; ++sanity) {
|
|
auto &container = *p_container;
|
|
bool success = sexp_container_CTEXT_helper(node, container, result);
|
|
|
|
if (Log_event) {
|
|
SCP_string log_string = container.container_name;
|
|
if (success) {
|
|
log_string += " container returned ";
|
|
log_string += result;
|
|
} else {
|
|
log_string += " container lookup failed";
|
|
}
|
|
Current_event_log_container_buffer->emplace_back(log_string);
|
|
}
|
|
|
|
if (!success || result.empty()) {
|
|
return Empty_str;
|
|
}
|
|
|
|
if (result.front() != sexp_container::DELIM) {
|
|
if (!Sexp_nodes[node].cache) {
|
|
Sexp_nodes[node].cache = std::make_unique<sexp_cached_data>(OPF_CONTAINER_NAME, result);
|
|
} else {
|
|
Sexp_nodes[node].cache->update_container_CTEXT_result(result);
|
|
}
|
|
return Sexp_nodes[node].cache->container_CTEXT_result;
|
|
} else {
|
|
// we're dealing with a multidimentional container
|
|
node = CDR(node);
|
|
|
|
if (node == -1) {
|
|
Warning(LOCATION,
|
|
"Multidimensional container argument missing for '%s' from container %s!",
|
|
result.c_str(),
|
|
container.container_name.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG,
|
|
"Multidimensional container argument missing for '%s' from container %s!",
|
|
result.c_str(),
|
|
container.container_name.c_str());
|
|
return Empty_str;
|
|
}
|
|
|
|
// "- 2" because we're skipping the first and last chars, which are/should both be &
|
|
auto *p_next_container = get_sexp_container(result.substr(1, (result.length() - 2)).c_str());
|
|
|
|
if (p_next_container != nullptr) {
|
|
p_container = p_next_container;
|
|
result.clear();
|
|
} else {
|
|
Warning(LOCATION, "There is no container called %s in this mission.", result.c_str());
|
|
return Empty_str;
|
|
}
|
|
}
|
|
}
|
|
|
|
// we've hit sanity limit; give up
|
|
return Empty_str;
|
|
}
|
|
|
|
// status SEXPs
|
|
int sexp_is_container_empty(int node)
|
|
{
|
|
const char *container_name = CTEXT(node);
|
|
const auto *p_container = get_sexp_container(container_name);
|
|
|
|
if (!p_container) {
|
|
report_nonexistent_container("Is-container-empty", container_name);
|
|
return SEXP_FALSE;
|
|
}
|
|
|
|
const auto &container = *p_container;
|
|
|
|
if (container.is_map()) {
|
|
if (container.map_data.empty()) {
|
|
return SEXP_TRUE;
|
|
}
|
|
} else if (container.is_list()) {
|
|
if (container.list_data.empty()) {
|
|
return SEXP_TRUE;
|
|
}
|
|
} else {
|
|
UNREACHABLE("Container %s has invalid type (%d). Please report!", container_name, static_cast<int>(container.type));
|
|
}
|
|
|
|
return SEXP_FALSE;
|
|
}
|
|
|
|
int sexp_get_container_size(int node)
|
|
{
|
|
const char *container_name = CTEXT(node);
|
|
const auto *p_container = get_sexp_container(container_name);
|
|
|
|
if (!p_container) {
|
|
report_nonexistent_container("Get-container-size", container_name);
|
|
return 0;
|
|
}
|
|
|
|
const auto &container = *p_container;
|
|
|
|
if (container.is_map()) {
|
|
return (int)container.map_data.size();
|
|
} else if (container.is_list()) {
|
|
return (int)container.list_data.size();
|
|
} else {
|
|
UNREACHABLE("Container %s has invalid type (%d). Please report!", container_name, static_cast<int>(container.type));
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
int sexp_list_has_data(int node)
|
|
{
|
|
const char *container_name = CTEXT(node);
|
|
const auto *p_container = get_sexp_container(container_name);
|
|
|
|
if (!p_container) {
|
|
report_nonexistent_container("List-has-data", container_name);
|
|
return SEXP_FALSE;
|
|
}
|
|
|
|
const auto &container = *p_container;
|
|
|
|
if (!container.is_list()) {
|
|
report_non_list_container("List-has-data", container_name);
|
|
return SEXP_FALSE;
|
|
}
|
|
|
|
const auto &list_data = container.list_data;
|
|
|
|
node = CDR(node);
|
|
Assertion(node != -1, "List-has-data wasn't given data to look for. Please report!");
|
|
|
|
SCP_string possible_data;
|
|
while (node != -1) {
|
|
// TODO: revisit if traversing the list for every item proves unacceptably slow
|
|
possible_data = CTEXT(node);
|
|
|
|
if (std::find(list_data.begin(), list_data.end(), possible_data) == list_data.end()) {
|
|
return SEXP_FALSE;
|
|
}
|
|
|
|
node = CDR(node);
|
|
}
|
|
|
|
return SEXP_TRUE;
|
|
}
|
|
|
|
int sexp_list_data_index(int node)
|
|
{
|
|
const char *container_name = CTEXT(node);
|
|
const auto *p_container = get_sexp_container(container_name);
|
|
|
|
if (!p_container) {
|
|
report_nonexistent_container("List-data-index", container_name);
|
|
return -1;
|
|
}
|
|
|
|
const auto &container = *p_container;
|
|
|
|
if (!container.is_list()) {
|
|
report_non_list_container("List-data-index", container_name);
|
|
return -1;
|
|
}
|
|
|
|
node = CDR(node);
|
|
Assertion(node != -1, "List-data-index wasn't given data to look for. Please report!");
|
|
|
|
const SCP_string possible_data = CTEXT(node);
|
|
|
|
int data_index = 0;
|
|
for (const SCP_string &data : container.list_data) {
|
|
if (possible_data == data) {
|
|
return data_index;
|
|
}
|
|
++data_index;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
int sexp_map_has_key(int node)
|
|
{
|
|
const char *container_name = CTEXT(node);
|
|
const auto *p_container = get_sexp_container(container_name);
|
|
|
|
if (!p_container) {
|
|
report_nonexistent_container("Map-has-key", container_name);
|
|
return SEXP_FALSE;
|
|
}
|
|
|
|
const auto &container = *p_container;
|
|
|
|
if (!container.is_map()) {
|
|
report_non_map_container("Map-has-key", container_name);
|
|
return SEXP_FALSE;
|
|
}
|
|
|
|
const auto &map_data = container.map_data;
|
|
|
|
node = CDR(node);
|
|
Assertion(node != -1, "Map-has-key wasn't given keys to look for. Please report!");
|
|
|
|
SCP_string possible_key;
|
|
while (node != -1) {
|
|
possible_key = CTEXT(node);
|
|
if (map_data.find(possible_key) == map_data.end()) {
|
|
return SEXP_FALSE;
|
|
}
|
|
|
|
node = CDR(node);
|
|
}
|
|
|
|
return SEXP_TRUE;
|
|
}
|
|
|
|
/**
|
|
* Check if a SEXP map container has a key whose data matches a specific string.
|
|
* If a third argument (string variable) is provided, store the data's associated key, if a match is found.
|
|
* Returns -1 if the element is not in the container
|
|
*/
|
|
int sexp_map_has_data_item(int node)
|
|
{
|
|
const char *container_name = CTEXT(node);
|
|
const auto *p_container = get_sexp_container(container_name);
|
|
|
|
if (!p_container) {
|
|
report_nonexistent_container("Map-has-data-item", container_name);
|
|
return SEXP_FALSE;
|
|
}
|
|
|
|
const auto &container = *p_container;
|
|
|
|
if (!container.is_map()) {
|
|
report_non_map_container("Map-has-data-item", container_name);
|
|
return SEXP_FALSE;
|
|
}
|
|
|
|
node = CDR(node);
|
|
Assertion(node != -1, "Map-has-data-item wasn't given data to look for. Please report!");
|
|
|
|
const SCP_string possible_data = CTEXT(node);
|
|
|
|
for (const auto &kv_pair : container.map_data) {
|
|
if (possible_data == kv_pair.second) {
|
|
// check for optional variable to store the key
|
|
node = CDR(node);
|
|
if (node != -1) {
|
|
int var_index = -1;
|
|
if (Sexp_nodes[node].type & SEXP_FLAG_VARIABLE) {
|
|
var_index = sexp_get_variable_index(node);
|
|
} else {
|
|
// perhaps the node text is the variable name as data
|
|
var_index = get_index_sexp_variable_name(Sexp_nodes[node].text);
|
|
}
|
|
|
|
if (var_index >= 0) {
|
|
const int var_type = Sexp_variables[var_index].type;
|
|
if (((var_type & SEXP_VARIABLE_STRING) && any(container.type & ContainerType::STRING_KEYS)) ||
|
|
((var_type & SEXP_VARIABLE_NUMBER) && any(container.type & ContainerType::NUMBER_KEYS))) {
|
|
// assign key to variable
|
|
sexp_modify_variable(kv_pair.first.c_str(), var_index);
|
|
} else {
|
|
const SCP_string msg = SCP_string("Map-has-data-item given optional variable ") +
|
|
Sexp_variables[var_index].variable_name +
|
|
" whose type doesn't match key type of map container " + container_name;
|
|
Warning(LOCATION, "%s", msg.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
}
|
|
} else {
|
|
const SCP_string msg =
|
|
SCP_string("Map-has-data-item given invalid optional variable ") + Sexp_nodes[node].text;
|
|
Warning(LOCATION, "%s", msg.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
}
|
|
}
|
|
|
|
return SEXP_TRUE;
|
|
}
|
|
}
|
|
|
|
return SEXP_FALSE;
|
|
}
|
|
|
|
int sexp_container_eval_status_sexp(int op_num, int node)
|
|
{
|
|
switch (op_num) {
|
|
case OP_IS_CONTAINER_EMPTY:
|
|
return sexp_is_container_empty(node);
|
|
break;
|
|
|
|
case OP_GET_CONTAINER_SIZE:
|
|
return sexp_get_container_size(node);
|
|
break;
|
|
|
|
case OP_LIST_HAS_DATA:
|
|
return sexp_list_has_data(node);
|
|
break;
|
|
|
|
case OP_LIST_DATA_INDEX:
|
|
return sexp_list_data_index(node);
|
|
break;
|
|
|
|
case OP_MAP_HAS_KEY:
|
|
return sexp_map_has_key(node);
|
|
break;
|
|
|
|
case OP_MAP_HAS_DATA_ITEM:
|
|
return sexp_map_has_data_item(node);
|
|
break;
|
|
|
|
default:
|
|
UNREACHABLE("Unknown container status SEXP operator %d. Please report!", op_num);
|
|
return SEXP_FALSE;
|
|
}
|
|
}
|
|
|
|
// Change SEXPs
|
|
void sexp_add_to_list(int node)
|
|
{
|
|
const char *container_name = CTEXT(node);
|
|
auto *p_container = get_sexp_container(container_name);
|
|
|
|
if (!p_container) {
|
|
report_nonexistent_container("Add-to-list", container_name);
|
|
return;
|
|
}
|
|
|
|
auto &container = *p_container;
|
|
|
|
node = CDR(node);
|
|
Assertion(node != -1, "Add-to-list wasn't told which end of the list to add to. Please report!");
|
|
|
|
const bool add_to_back = is_sexp_true(node);
|
|
|
|
node = CDR(node);
|
|
Assertion(node != -1, "Add-to-list wasn't given data to add. Please report!");
|
|
|
|
if (!container.is_list()) {
|
|
report_non_list_container("Add-to-list", container_name);
|
|
return;
|
|
}
|
|
|
|
if (container.is_being_used_in_special_arg()) {
|
|
report_container_used_in_special_arg("Add-to-list", container_name);
|
|
return;
|
|
}
|
|
|
|
while (node != -1) {
|
|
if (add_to_back) {
|
|
container.list_data.emplace_back(CTEXT(node));
|
|
} else {
|
|
container.list_data.emplace_front(CTEXT(node));
|
|
}
|
|
|
|
node = CDR(node);
|
|
}
|
|
}
|
|
|
|
void sexp_remove_from_list(int node)
|
|
{
|
|
const char *container_name = CTEXT(node);
|
|
auto *p_container = get_sexp_container(container_name);
|
|
|
|
if (!p_container) {
|
|
report_nonexistent_container("Remove-from-list", container_name);
|
|
return;
|
|
}
|
|
|
|
auto &container = *p_container;
|
|
|
|
if (!container.is_list()) {
|
|
report_non_list_container("Remove-from-list", container_name);
|
|
return;
|
|
}
|
|
|
|
if (container.is_being_used_in_special_arg()) {
|
|
report_container_used_in_special_arg("Remove-from-list", container_name);
|
|
return;
|
|
}
|
|
|
|
node = CDR(node);
|
|
Assertion(node != -1, "Remove-from-list wasn't given data to remove. Please report!");
|
|
|
|
auto &list_data = container.list_data;
|
|
|
|
SCP_string data_to_remove;
|
|
while (node != -1) {
|
|
data_to_remove = CTEXT(node);
|
|
auto list_it = std::find(list_data.begin(), list_data.end(), data_to_remove);
|
|
if (list_it != list_data.end()) {
|
|
list_data.erase(list_it);
|
|
} else {
|
|
const SCP_string msg =
|
|
"Remove-from-list couldn't find data " + data_to_remove + " inside list container " + container_name;
|
|
Warning(LOCATION, "%s", msg.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
}
|
|
|
|
node = CDR(node);
|
|
}
|
|
}
|
|
|
|
void sexp_add_to_map(int node)
|
|
{
|
|
const char *container_name = CTEXT(node);
|
|
auto *p_container = get_sexp_container(container_name);
|
|
|
|
if (!p_container) {
|
|
report_nonexistent_container("Add-to-map", container_name);
|
|
return;
|
|
}
|
|
|
|
auto &container = *p_container;
|
|
|
|
if (!container.is_map()) {
|
|
report_non_map_container("Add-to-map", container_name);
|
|
return;
|
|
}
|
|
|
|
node = CDR(node);
|
|
Assertion(node != -1, "Add-to-map wasn't given values to add. Please report!");
|
|
|
|
while (node != -1) {
|
|
// key but no value
|
|
if (CDR(node) == -1) {
|
|
const SCP_string msg = SCP_string("Add-to-map with container ") + container_name + " was provided a key " +
|
|
CTEXT(node) + " with no associated data";
|
|
Warning(LOCATION, "%s", msg.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
return;
|
|
}
|
|
|
|
const SCP_string key = CTEXT(node);
|
|
|
|
if (container.is_being_used_in_special_arg()) {
|
|
report_container_used_in_special_arg("Add-to-map", container_name);
|
|
node = CDDR(node); // skip to next key-data pair
|
|
continue;
|
|
}
|
|
|
|
const SCP_string data = CTEXT(CDR(node));
|
|
add_to_map_internal(container, key, data);
|
|
|
|
// skip the value and move onto the next key
|
|
node = CDDR(node);
|
|
}
|
|
}
|
|
|
|
void sexp_remove_from_map(int node)
|
|
{
|
|
const char *container_name = CTEXT(node);
|
|
auto *p_container = get_sexp_container(container_name);
|
|
|
|
if (!p_container) {
|
|
report_nonexistent_container("Remove-from-map", container_name);
|
|
return;
|
|
}
|
|
|
|
auto &container = *p_container;
|
|
|
|
if (!container.is_map()) {
|
|
report_non_map_container("Remove-from-map", container_name);
|
|
return;
|
|
}
|
|
|
|
if (container.is_being_used_in_special_arg()) {
|
|
report_container_used_in_special_arg("Remove-from-map", container_name);
|
|
return;
|
|
}
|
|
|
|
node = CDR(node);
|
|
Assertion(node != -1, "Remove-from-map wasn't given keys to remove. Please report!");
|
|
|
|
SCP_string key_to_remove;
|
|
while (node != -1) {
|
|
key_to_remove = CTEXT(node);
|
|
if (container.map_data.erase(key_to_remove) == 0) {
|
|
const SCP_string msg =
|
|
"Remove-from-map couldn't find key " + key_to_remove + " inside map container " + container_name;
|
|
mprintf(("%s\n", msg.c_str()));
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
}
|
|
|
|
node = CDR(node);
|
|
}
|
|
}
|
|
|
|
void sexp_get_map_keys(int node)
|
|
{
|
|
bool replace_contents = true;
|
|
|
|
const char *map_container_name = CTEXT(node);
|
|
const auto *p_map_container = get_sexp_container(map_container_name);
|
|
|
|
if (!p_map_container) {
|
|
report_nonexistent_container("Get-map-keys", map_container_name);
|
|
return;
|
|
}
|
|
|
|
node = CDR(node);
|
|
Assertion(node != -1, "Get-map-keys wasn't given a list container. Please report!");
|
|
|
|
const char *list_container_name = CTEXT(node);
|
|
|
|
auto *p_list_container = get_sexp_container(list_container_name);
|
|
|
|
if (!p_list_container) {
|
|
report_nonexistent_container("Get-map-keys", list_container_name);
|
|
return;
|
|
}
|
|
|
|
const auto &map_container = *p_map_container;
|
|
auto &list_container = *p_list_container;
|
|
|
|
// both containers must be of the correct type.
|
|
if (!map_container.is_map()) {
|
|
report_non_map_container("Get-map-keys", map_container_name);
|
|
return;
|
|
}
|
|
if (!list_container.is_list()) {
|
|
report_non_list_container("Get-map-keys", list_container_name);
|
|
return;
|
|
}
|
|
if ((any(map_container.type & ContainerType::STRING_KEYS) &&
|
|
none(list_container.type & ContainerType::STRING_DATA)) ||
|
|
(any(map_container.type & ContainerType::NUMBER_KEYS) &&
|
|
none(list_container.type & ContainerType::NUMBER_DATA))) {
|
|
const SCP_string msg = SCP_string("Get-map-keys called on map container ") + map_container_name +
|
|
" whose key type doesn't match data type of list container " + list_container_name;
|
|
Warning(LOCATION, "%s", msg.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
return;
|
|
}
|
|
|
|
// SEXP doesn't modify map container, so using it in special arg here is OK
|
|
if (list_container.is_being_used_in_special_arg()) {
|
|
report_container_used_in_special_arg("Get-map-keys", list_container_name);
|
|
return;
|
|
}
|
|
|
|
node = CDR(node);
|
|
if (node != -1) {
|
|
replace_contents = is_sexp_true(node);
|
|
}
|
|
|
|
if (replace_contents) {
|
|
list_container.list_data.clear();
|
|
}
|
|
|
|
for (const auto &kv_pair : map_container.map_data) {
|
|
list_container.list_data.emplace_back(kv_pair.first);
|
|
}
|
|
}
|
|
|
|
void sexp_clear_container(int node)
|
|
{
|
|
Assertion(node != -1, "Clear-container wasn't given any containers. Please report!");
|
|
|
|
for (; node != -1; node = CDR(node)) {
|
|
const char *container_name = CTEXT(node);
|
|
auto *p_container = get_sexp_container(container_name);
|
|
|
|
if (!p_container) {
|
|
report_nonexistent_container("Clear-container", container_name);
|
|
continue;
|
|
}
|
|
|
|
auto &container = *p_container;
|
|
|
|
if (container.is_being_used_in_special_arg()) {
|
|
report_container_used_in_special_arg("Clear-container", container_name);
|
|
continue;
|
|
}
|
|
|
|
if (container.is_map()) {
|
|
container.map_data.clear();
|
|
} else if (container.is_list()) {
|
|
container.list_data.clear();
|
|
} else {
|
|
UNREACHABLE("Container %s has invalid type (%d). Please report!", container_name, static_cast<int>(container.type));
|
|
}
|
|
}
|
|
}
|
|
|
|
void sexp_copy_container(int node)
|
|
{
|
|
Assertion(node != -1, "Copy-container wasn't given a container to copy from. Please report!");
|
|
|
|
const char *src_container_name = CTEXT(node);
|
|
const auto * const p_src_container = get_sexp_container(src_container_name);
|
|
|
|
if (!p_src_container) {
|
|
report_nonexistent_container("Copy-container", src_container_name);
|
|
return;
|
|
}
|
|
const auto &src_container = *p_src_container;
|
|
|
|
node = CDR(node);
|
|
Assertion(node != -1, "Copy-container wasn't given a container to copy to. Please report!");
|
|
|
|
const char *dest_container_name = CTEXT(node);
|
|
auto *p_dest_container = get_sexp_container(dest_container_name);
|
|
|
|
if (!p_dest_container) {
|
|
report_nonexistent_container("Copy-container", dest_container_name);
|
|
return;
|
|
}
|
|
if (p_src_container == p_dest_container) {
|
|
const SCP_string msg =
|
|
SCP_string("Copy-container called to copy container ") + src_container_name + " to itself";
|
|
Warning(LOCATION, "%s", msg.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
return;
|
|
}
|
|
auto &dest_container = *p_dest_container;
|
|
|
|
if (src_container.get_non_persistent_type() != dest_container.get_non_persistent_type()) {
|
|
const SCP_string msg = SCP_string("Copy-container called on container ") + src_container_name +
|
|
" and container " + dest_container_name + " whose types don't match";
|
|
Warning(LOCATION, "%s", msg.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
return;
|
|
}
|
|
|
|
// SEXP doesn't modify source container, so using it in special arg here is OK
|
|
if (dest_container.is_being_used_in_special_arg()) {
|
|
report_container_used_in_special_arg("Copy-container", dest_container_name);
|
|
return;
|
|
}
|
|
|
|
bool replace_contents = true;
|
|
|
|
node = CDR(node);
|
|
if (node != -1) {
|
|
replace_contents = is_sexp_true(node);
|
|
}
|
|
|
|
if (src_container.is_list()) {
|
|
const auto &src_list = src_container.list_data;
|
|
auto &dest_list = dest_container.list_data;
|
|
|
|
if (replace_contents) {
|
|
dest_list.clear();
|
|
}
|
|
|
|
std::copy(src_list.begin(), src_list.end(), back_inserter(dest_list));
|
|
} else if (src_container.is_map()) {
|
|
if (replace_contents) {
|
|
dest_container.map_data.clear();
|
|
}
|
|
|
|
for (const auto &kv_pair : src_container.map_data) {
|
|
add_to_map_internal(dest_container, kv_pair.first, kv_pair.second);
|
|
}
|
|
} else {
|
|
UNREACHABLE("Container %s has invalid type (%d). Please report!", src_container_name, static_cast<int>(src_container.type));
|
|
}
|
|
}
|
|
|
|
void sexp_apply_container_filter(int node)
|
|
{
|
|
Assertion(node != -1, "Apply-container-filter wasn't given a container to apply a filter to. Please report!");
|
|
|
|
const char *container_name = CTEXT(node);
|
|
auto *const p_container = get_sexp_container(container_name);
|
|
|
|
if (!p_container) {
|
|
report_nonexistent_container("Apply-container-filter", container_name);
|
|
return;
|
|
}
|
|
|
|
auto &container = *p_container;
|
|
|
|
node = CDR(node);
|
|
Assertion(node != -1, "Apply-container-filter wasn't given a container to use as a filter. Please report!");
|
|
|
|
const char *filter_container_name = CTEXT(node);
|
|
const auto *p_filter_container = get_sexp_container(filter_container_name);
|
|
|
|
if (!p_filter_container) {
|
|
report_nonexistent_container("Apply-container-filter", filter_container_name);
|
|
return;
|
|
}
|
|
|
|
if (p_container == p_filter_container) {
|
|
const SCP_string msg = SCP_string("Apply-container-filter called to filter container ") + container_name +
|
|
" using itself as the filter.";
|
|
Warning(LOCATION, "%s", msg.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
return;
|
|
}
|
|
|
|
const auto &filter_container = *p_filter_container;
|
|
|
|
if (!filter_container.is_list()) {
|
|
report_non_list_container("Apply-container-filter", filter_container_name);
|
|
return;
|
|
}
|
|
|
|
// SEXP doesn't modify filter container, so using it here is ok
|
|
if (container.is_being_used_in_special_arg()) {
|
|
report_container_used_in_special_arg("Apply-container-filter", container_name);
|
|
return;
|
|
}
|
|
|
|
if (filter_container.list_data.empty()) {
|
|
#if !defined(NDEBUG) || defined(SCP_RELEASE_LOGGING)
|
|
// while an empty filter isn't technically an error, it should be logged
|
|
const SCP_string msg = SCP_string("Apply-container-filter called to filter container ") + container_name +
|
|
" using empty filter container " + filter_container_name;
|
|
mprintf(("%s\n", msg.c_str()));
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
if (container.is_list()) {
|
|
if (container.get_data_type() != filter_container.get_data_type()) {
|
|
const SCP_string msg = SCP_string("Apply-container-filter called on list container ") + container_name +
|
|
" whose data type doesn't match data type of filter container " +
|
|
filter_container_name;
|
|
Warning(LOCATION, "%s", msg.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
return;
|
|
}
|
|
|
|
for (const SCP_string &filter_data : filter_container.list_data) {
|
|
auto list_it = std::find(container.list_data.begin(), container.list_data.end(), filter_data);
|
|
if (list_it != container.list_data.end()) {
|
|
container.list_data.erase(list_it);
|
|
}
|
|
}
|
|
} else if (container.is_map()) {
|
|
if ((any(container.type & ContainerType::STRING_KEYS) &&
|
|
none(filter_container.type & ContainerType::STRING_DATA)) ||
|
|
(any(container.type & ContainerType::NUMBER_KEYS) &&
|
|
none(filter_container.type & ContainerType::NUMBER_DATA))) {
|
|
const SCP_string msg = SCP_string("Apply-container-filter called on map container ") + container_name +
|
|
" whose key type doesn't match data type of filter container " +
|
|
filter_container_name;
|
|
Warning(LOCATION, "%s", msg.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
return;
|
|
}
|
|
|
|
for (const SCP_string &filter_data : filter_container.list_data) {
|
|
container.map_data.erase(filter_data);
|
|
}
|
|
} else {
|
|
UNREACHABLE("Container %s has invalid type (%d). Please report!", container_name, static_cast<int>(container.type));
|
|
}
|
|
}
|
|
|
|
int sexp_container_eval_change_sexp(int op_num, int node)
|
|
{
|
|
switch (op_num) {
|
|
case OP_CONTAINER_ADD_TO_LIST:
|
|
sexp_add_to_list(node);
|
|
return SEXP_TRUE;
|
|
|
|
case OP_CONTAINER_REMOVE_FROM_LIST:
|
|
sexp_remove_from_list(node);
|
|
return SEXP_TRUE;
|
|
|
|
case OP_CONTAINER_ADD_TO_MAP:
|
|
sexp_add_to_map(node);
|
|
return SEXP_TRUE;
|
|
|
|
case OP_CONTAINER_REMOVE_FROM_MAP:
|
|
sexp_remove_from_map(node);
|
|
return SEXP_TRUE;
|
|
|
|
case OP_CONTAINER_GET_MAP_KEYS:
|
|
sexp_get_map_keys(node);
|
|
return SEXP_TRUE;
|
|
|
|
case OP_CLEAR_CONTAINER:
|
|
sexp_clear_container(node);
|
|
return SEXP_TRUE;
|
|
|
|
case OP_COPY_CONTAINER:
|
|
sexp_copy_container(node);
|
|
return SEXP_TRUE;
|
|
|
|
case OP_APPLY_CONTAINER_FILTER:
|
|
sexp_apply_container_filter(node);
|
|
return SEXP_TRUE;
|
|
|
|
default:
|
|
UNREACHABLE("Unknown container change SEXP operator %d", op_num);
|
|
return SEXP_FALSE;
|
|
}
|
|
}
|
|
|
|
int collect_container_values(int node,
|
|
SCP_vector<std::pair<const char*, int>> &argument_vector,
|
|
bool just_count,
|
|
const char *sexp_name,
|
|
bool map_keys_only)
|
|
{
|
|
Assertion(node != -1, "%s wasn't given any ontainers. Please report!", sexp_name);
|
|
|
|
int num_args = 0;
|
|
|
|
for (; node != -1; node = CDR(node)) {
|
|
const char *container_name = CTEXT(node);
|
|
const auto *p_container = get_sexp_container(container_name);
|
|
|
|
if (!p_container) {
|
|
report_nonexistent_container(sexp_name, container_name);
|
|
continue;
|
|
}
|
|
|
|
const auto &container = *p_container;
|
|
|
|
if (map_keys_only) {
|
|
if (!container.is_map()) {
|
|
report_non_map_container(sexp_name, container_name);
|
|
continue;
|
|
}
|
|
|
|
if (none(container.type & ContainerType::STRING_KEYS)) {
|
|
const SCP_string msg = SCP_string(sexp_name) + " called on map container " + container_name +
|
|
" but its keys aren't strings";
|
|
Warning(LOCATION, "%s", msg.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
continue;
|
|
}
|
|
|
|
num_args += (int)container.map_data.size();
|
|
|
|
if (!just_count) {
|
|
for (const auto &kv_pair : container.map_data) {
|
|
argument_vector.emplace_back(vm_strdup(kv_pair.first.c_str()), -1);
|
|
}
|
|
}
|
|
} else {
|
|
if (none(container.type & ContainerType::STRING_DATA)) {
|
|
const SCP_string msg =
|
|
SCP_string(sexp_name) + " called on container " + container_name + " but its data aren't strings";
|
|
Warning(LOCATION, "%s", msg.c_str());
|
|
log_printf(LOGFILE_EVENT_LOG, "%s", msg.c_str());
|
|
continue;
|
|
}
|
|
|
|
if (container.is_map()) {
|
|
num_args += (int)container.map_data.size();
|
|
|
|
if (!just_count) {
|
|
for (const auto &kv_pair : container.map_data) {
|
|
argument_vector.emplace_back(vm_strdup(kv_pair.second.c_str()), -1);
|
|
}
|
|
}
|
|
} else if (container.is_list()) {
|
|
num_args += (int)container.list_data.size();
|
|
|
|
if (!just_count) {
|
|
for (const auto &value : container.list_data) {
|
|
argument_vector.emplace_back(vm_strdup(value.c_str()), -1);
|
|
}
|
|
}
|
|
} else {
|
|
UNREACHABLE("Container %s has invalid type (%d). Please report!", container_name, static_cast<int>(container.type));
|
|
}
|
|
}
|
|
}
|
|
|
|
return num_args;
|
|
}
|
|
|
|
int sexp_container_collect_data_arguments(int node,
|
|
SCP_vector<std::pair<const char*, int>> &argument_vector,
|
|
bool just_count)
|
|
{
|
|
return collect_container_values(node, argument_vector, just_count, "For-container-data", false);
|
|
}
|
|
|
|
int sexp_container_collect_map_key_arguments(int node,
|
|
SCP_vector<std::pair<const char*, int>> &argument_vector,
|
|
bool just_count)
|
|
{
|
|
return collect_container_values(node, argument_vector, just_count, "For-map-container-keys", true);
|
|
}
|
|
|
|
int sexp_container_query_sexp_args_count(const int node, SCP_vector<int> &cumulative_arg_countss, bool only_valid_args)
|
|
{
|
|
Assertion(cumulative_arg_countss.empty(),
|
|
"Attempt to count number of SEXP arguments when counts already exit. Please repot!");
|
|
|
|
int count = 0;
|
|
|
|
// intiial value, for no arguments
|
|
cumulative_arg_countss.emplace_back(0);
|
|
|
|
int n = CDR(node);
|
|
|
|
for (; n != -1; n = CDR(n))
|
|
{
|
|
if (only_valid_args && !(Sexp_nodes[n].flags & SNF_ARGUMENT_VALID)) {
|
|
continue;
|
|
}
|
|
|
|
const int prev_index = cumulative_arg_countss.back();
|
|
|
|
if (Sexp_nodes[n].subtype == SEXP_ATOM_CONTAINER_NAME) {
|
|
const char *container_name = Sexp_nodes[n].text;
|
|
const auto *p_container = get_sexp_container(container_name);
|
|
|
|
// should have been checked in get_sexp()
|
|
Assertion(p_container, "Special argument SEXP given nonexistent container %s. Please report!", container_name);
|
|
const auto &container = *p_container;
|
|
|
|
Assertion(container.is_of_string_type(),
|
|
"Attempt to use non-string-valued container %s as special argument options. Please report!",
|
|
container_name);
|
|
// if the container is empty, the index will be a duplicate, but that's ok
|
|
cumulative_arg_countss.emplace_back(prev_index + container.size());
|
|
} else {
|
|
cumulative_arg_countss.emplace_back(prev_index + 1);
|
|
}
|
|
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
void sexp_container_set_special_arg_status(int arg_handler_node, bool status)
|
|
{
|
|
Assertion(arg_handler_node != -1,
|
|
"Attempt to set special argument status on invalid argument handler. Please report!");
|
|
|
|
for (int arg_node = CDR(arg_handler_node); arg_node != -1; arg_node = CDR(arg_node)) {
|
|
Assertion(Sexp_nodes[arg_node].subtype != SEXP_ATOM_CONTAINER_DATA,
|
|
"Attempt to use Replace Container Data as special argument option, which isn't supported. Please report!");
|
|
|
|
if (Sexp_nodes[arg_node].subtype == SEXP_ATOM_CONTAINER_NAME) {
|
|
const char *container_name = Sexp_nodes[arg_node].text;
|
|
auto *p_container = get_sexp_container(container_name);
|
|
|
|
// should have been checked in get_sexp()
|
|
Assertion(p_container,
|
|
"Special argument SEXP given nonexistent container %s. Please report!",
|
|
container_name);
|
|
auto &container = *p_container;
|
|
|
|
// status could match, if the FREDder added the same container multiple times
|
|
// as an argument to the same SEXP, which is odd but not an error
|
|
container.used_in_special_arg = status;
|
|
}
|
|
}
|
|
}
|