diff --git a/code/mission/missiongoals.cpp b/code/mission/missiongoals.cpp index 6a1a046cdab..ec140604e99 100644 --- a/code/mission/missiongoals.cpp +++ b/code/mission/missiongoals.cpp @@ -180,8 +180,6 @@ SCP_vector Mission_events; SCP_vector Mission_goals; // structure for the goals of this mission static goal_text Goal_text; -SCP_vector Event_annotations; - #define DIRECTIVE_SOUND_DELAY 500 // time directive success sound effect is delayed #define DIRECTIVE_SPECIAL_DELAY 7000 // mark special directives as true after 7 seconds diff --git a/code/mission/missiongoals.h b/code/mission/missiongoals.h index 6de041b8968..3a3ca116d0d 100644 --- a/code/mission/missiongoals.h +++ b/code/mission/missiongoals.h @@ -132,22 +132,6 @@ extern int Event_index; // used by sexp code to tell what event it came from extern bool Log_event; extern bool Snapshot_all_events; - -// only used in FRED -struct event_annotation -{ - void *handle = nullptr; // the handle of the tree node in the event editor. This is an HTREEITEM in FRED and TBD in qtFRED. - int item_image = -1; // the previous image of the tree node (replaced by a comment icon when there is a comment) - SCP_list path; // a way to find the node that the annotation represents: - // the first number is the event, the second number is the node on the first layer, etc. - SCP_string comment; - ubyte r = 255; - ubyte g = 255; - ubyte b = 255; -}; -extern SCP_vector Event_annotations; - - // prototypes void mission_goals_and_events_init( void ); void mission_show_goals_init(); diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index 0b6d033cfb6..e46c997bcb2 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -46,6 +46,7 @@ #include "mission/missionlog.h" #include "mission/missionmessage.h" #include "mission/missionparse.h" +#include "missioneditor/sexp_annotation_model.h" #include "missionui/fictionviewer.h" #include "missionui/missioncmdbrief.h" #include "missionui/redalert.h" diff --git a/code/missioneditor/missionsave.cpp b/code/missioneditor/missionsave.cpp index e62b7d8aa9d..01cbcf56e27 100644 --- a/code/missioneditor/missionsave.cpp +++ b/code/missioneditor/missionsave.cpp @@ -33,6 +33,7 @@ #include "mission/missionmessage.h" #include "mission/missionparse.h" #include "missioneditor/common.h" +#include "missioneditor/sexp_annotation_model.h" #include "missionui/fictionviewer.h" #include "missionui/missioncmdbrief.h" #include "nebula/neb.h" diff --git a/code/missioneditor/sexp_annotation_model.cpp b/code/missioneditor/sexp_annotation_model.cpp new file mode 100644 index 00000000000..604ec403359 --- /dev/null +++ b/code/missioneditor/sexp_annotation_model.cpp @@ -0,0 +1,279 @@ +#include "missioneditor/sexp_annotation_model.h" +#include "missioneditor/sexp_tree_model.h" + +SCP_vector Event_annotations; + +// ----------------------------------------------------------------------- +// Lifecycle +// ----------------------------------------------------------------------- + +// Copy the global Event_annotations into our working set and resolve each +// stored path to a live annotation key (tree_nodes[] index or root key). +// Annotations whose paths can't be resolved (e.g. event was deleted) are +// reset to default values so they'll be pruned on save. +void SexpAnnotationModel::loadFromGlobal(const SCP_vector& tree_nodes, const SCP_vector& events, const SCP_vector& sig) +{ + m_annotations = Event_annotations; + + for (auto& ea : m_annotations) { + ea.node_index = -1; + + if (ea.path.empty()) + continue; + + int key = resolveFromPath(ea.path, tree_nodes, events, sig); + if (key != -1) { + ea.node_index = key; + } else { + // Path could not be resolved (event was probably deleted). + // Reset to default so it will be pruned on save. + ea.comment.clear(); + ea.r = ea.g = ea.b = 255; + ea.node_index = -1; + } + } +} + +// Rebuild persistable paths from annotation keys, attempt fallback resolution +// for any lost nodes, prune default-valued annotations, and write the result +// back to the global Event_annotations. +void SexpAnnotationModel::saveToGlobal(const SCP_vector& tree_nodes, const SCP_vector& events, const SCP_vector& sig) +{ + for (auto& ea : m_annotations) { + int key = ea.node_index; + SCP_list old_path = ea.path; + ea.path.clear(); + + if (key >= 0 && key < static_cast(tree_nodes.size()) && + tree_nodes[key].type != SEXPT_UNUSED) { + ea.path = buildPath(key, tree_nodes, events); + } else if (isRootKey(key)) { + ea.path = buildPath(key, tree_nodes, events); + } else { + // Node lost — try to resolve from the old path as a fallback. + // (The old path may use original event indices, so we need sig.) + int resolved = resolveFromPath(old_path, tree_nodes, events, sig); + if (resolved >= 0 || isRootKey(resolved)) { + ea.path = buildPath(resolved, tree_nodes, events); + } else { + // Truly gone; mark default for pruning. + ea.comment.clear(); + ea.r = ea.g = ea.b = 255; + } + } + + // Reset transient field. + ea.node_index = -1; + } + + prune(); + Event_annotations = m_annotations; +} + +// ----------------------------------------------------------------------- +// Lookup +// ----------------------------------------------------------------------- + +// Return the vector index of the annotation with the given key, or -1 if not found. +int SexpAnnotationModel::findByKey(int key) const +{ + for (size_t i = 0; i < m_annotations.size(); ++i) { + if (m_annotations[i].node_index == key) + return static_cast(i); + } + return -1; +} + +// Return a const pointer to the annotation with the given key, or nullptr. +const event_annotation* SexpAnnotationModel::getByKey(int key) const +{ + int idx = findByKey(key); + return (idx >= 0) ? &m_annotations[idx] : nullptr; +} + +// Return a mutable pointer to the annotation with the given key, or nullptr. +event_annotation* SexpAnnotationModel::getByKey(int key) +{ + int idx = findByKey(key); + return (idx >= 0) ? &m_annotations[idx] : nullptr; +} + +// Get or create an annotation for the given key. If one already exists it is +// returned; otherwise a new default-valued annotation is appended. +event_annotation& SexpAnnotationModel::ensureByKey(int key) +{ + auto* existing = getByKey(key); + if (existing) + return *existing; + + m_annotations.emplace_back(); + auto& ea = m_annotations.back(); + ea.node_index = key; + return ea; +} + +// ----------------------------------------------------------------------- +// Predicates +// ----------------------------------------------------------------------- + +// True if the annotation has default values (empty comment, white color). +bool SexpAnnotationModel::isDefault(const event_annotation& ea) +{ + return ea.comment.empty() && ea.r == 255 && ea.g == 255 && ea.b == 255; +} + +// ----------------------------------------------------------------------- +// Mutation +// ----------------------------------------------------------------------- + +// Remove the annotation with the given key, if one exists. +void SexpAnnotationModel::removeByKey(int key) +{ + int idx = findByKey(key); + if (idx >= 0) { + m_annotations.erase(m_annotations.begin() + idx); + } +} + +// Remove all annotations that have default values (no useful data). +void SexpAnnotationModel::prune() +{ + m_annotations.erase( + std::remove_if(m_annotations.begin(), m_annotations.end(), + [](const event_annotation& ea) { return isDefault(ea); }), + m_annotations.end()); +} + +// Discard all annotations from the working set. +void SexpAnnotationModel::clear() +{ + m_annotations.clear(); +} + +// ----------------------------------------------------------------------- +// Path building (key -> path) +// ----------------------------------------------------------------------- + +// Build a persistable path from an annotation key to identify the node across +// save/load cycles. The path is a list of integers: [event_index, child_pos, ...]. +// For root keys the path is just [event_index]. For regular nodes the path walks +// from the root down, recording the child position at each level. +SCP_list SexpAnnotationModel::buildPath(int key, const SCP_vector& tree_nodes, const SCP_vector& events) +{ + SCP_list path; + + // --- Root key: path is just [event_index] --- + if (isRootKey(key)) { + int formula = formulaFromRootKey(key); + for (int i = 0; i < static_cast(events.size()); ++i) { + if (events[i].formula == formula) { + path.push_back(i); + return path; + } + } + return path; // empty = formula not found + } + + // --- Regular node --- + if (key < 0 || key >= static_cast(tree_nodes.size())) + return path; + + // Walk up to find the root (a node with no parent). + // Guard against cycles with a depth limit based on the tree size. + int root = key; + int depth = 0; + const int max_depth = static_cast(tree_nodes.size()); + while (tree_nodes[root].parent >= 0 && depth < max_depth) { + root = tree_nodes[root].parent; + ++depth; + } + if (depth >= max_depth) + return path; // cycle detected + + // Find the event index whose formula matches this root. + int event_idx = -1; + for (int i = 0; i < static_cast(events.size()); ++i) { + if (events[i].formula == root) { + event_idx = i; + break; + } + } + if (event_idx < 0) + return path; // root not found in events + + path.push_back(event_idx); + + // Collect child-position indices from the target node up to the root, + // then reverse them so the path reads top-down. + std::vector positions; + int cur = key; + for (;;) { + int parent = tree_nodes[cur].parent; + if (parent < 0) + break; + + int pos = 0; + int sibling = tree_nodes[parent].child; + while (sibling >= 0 && sibling != cur) { + ++pos; + sibling = tree_nodes[sibling].next; + } + positions.push_back(pos); + cur = parent; + } + + for (auto rit = positions.rbegin(); rit != positions.rend(); ++rit) + path.push_back(*rit); + + return path; +} + +// ----------------------------------------------------------------------- +// Path resolution (path -> key) +// ----------------------------------------------------------------------- + +// Resolve a stored path back to an annotation key by mapping the original event +// index through the sig table, then walking child positions down the tree. +// Returns a tree_nodes[] index (>= 0), a root key (<= -2), or -1 on failure. +int SexpAnnotationModel::resolveFromPath(const SCP_list& path, const SCP_vector& tree_nodes, const SCP_vector& events, const SCP_vector& sig) +{ + if (path.empty()) + return -1; + + int orig_event_idx = path.front(); + + // Map the original event index (from the saved path) to a formula + // using the sig table (which maps current dialog index → original index). + int formula = -1; + for (int i = 0; i < static_cast(sig.size()); ++i) { + if (sig[i] == orig_event_idx) { + if (i < static_cast(events.size())) { + formula = events[i].formula; + } + break; + } + } + if (formula < 0) + return -1; + + // Path of length 1 = root label annotation. + if (path.size() == 1) + return rootKey(formula); + + // Walk down the tree from the formula node. + int node = formula; + auto it = path.begin(); + ++it; // skip event index + for (; it != path.end() && node >= 0; ++it) { + int target = *it; + if (node < 0 || node >= static_cast(tree_nodes.size())) + return -1; + int child = tree_nodes[node].child; + for (int c = 0; c < target && child >= 0; ++c) { + child = tree_nodes[child].next; + } + node = child; + } + + return node; // >= 0 if resolution succeeded, -1 if not +} diff --git a/code/missioneditor/sexp_annotation_model.h b/code/missioneditor/sexp_annotation_model.h new file mode 100644 index 00000000000..fef784dee48 --- /dev/null +++ b/code/missioneditor/sexp_annotation_model.h @@ -0,0 +1,95 @@ +#pragma once + +#include "mission/missiongoals.h" + +struct event_annotation { + int node_index = -1; // index into sexp tree_nodes[] (-1 if unresolved); transient, not persisted + int item_image = -1; // the previous image of the tree node (replaced by a comment icon when there is a comment) + SCP_list path; // a way to find the node that the annotation represents: + // the first number is the event, the second number is the node on the first layer, etc. + SCP_string comment; + ubyte r = 255; + ubyte g = 255; + ubyte b = 255; +}; +extern SCP_vector Event_annotations; + +struct sexp_tree_item; + +class SexpAnnotationModel { +public: + // --------------------------------------------------------------- + // Lifecycle + // --------------------------------------------------------------- + + // Copy Event_annotations into the local working set, resolving each + // stored path to an annotation key (node index or root key). + // `sig` maps current dialog event index → original Mission_events index. + void loadFromGlobal(const SCP_vector& tree_nodes, const SCP_vector& events, const SCP_vector& sig); + + // Rebuild paths from annotation keys, prune defaults, and write the + // result back to the global Event_annotations. + void saveToGlobal(const SCP_vector& tree_nodes, const SCP_vector& events, const SCP_vector& sig); + + // --------------------------------------------------------------- + // Lookup + // --------------------------------------------------------------- + + // Find the vector index of the annotation with the given key, or -1. + int findByKey(int key) const; + + // Get a pointer to the annotation with the given key, or nullptr. + const event_annotation* getByKey(int key) const; + event_annotation* getByKey(int key); + + // Get or create an annotation for the given key. + event_annotation& ensureByKey(int key); + + // --------------------------------------------------------------- + // Predicates + // --------------------------------------------------------------- + + // True if the annotation has default values (empty comment, white color). + static bool isDefault(const event_annotation& ea); + + // --------------------------------------------------------------- + // Mutation + // --------------------------------------------------------------- + + // Remove the annotation with the given key, if one exists. + void removeByKey(int key); + + // Remove all annotations that are at default values. + void prune(); + + // Discard all annotations. + void clear(); + + // --------------------------------------------------------------- + // Direct access + // --------------------------------------------------------------- + + SCP_vector& annotations() { return m_annotations; } + const SCP_vector& annotations() const { return m_annotations; } + + // --------------------------------------------------------------- + // Root key encoding helpers + // --------------------------------------------------------------- + // Root labels (event names) don't have a tree_nodes[] entry. + // We encode them as -(formula + 2), which is always <= -2, + // avoiding collision with -1 (the default/unresolved sentinel). + + static int rootKey(int formula) { return -(formula + 2); } + static bool isRootKey(int key) { return key <= -2; } + static int formulaFromRootKey(int key) { return -(key + 2); } + +private: + SCP_vector m_annotations; + + // Build a persistable path from an annotation key. + static SCP_list buildPath(int key, const SCP_vector& tree_nodes, const SCP_vector& events); + + // Resolve a stored path back to an annotation key. + // Returns the key (>= 0 for regular nodes, <= -2 for root keys) or -1 on failure. + static int resolveFromPath(const SCP_list& path, const SCP_vector& tree_nodes, const SCP_vector& events, const SCP_vector& sig); +}; diff --git a/code/missioneditor/sexp_tree_actions.cpp b/code/missioneditor/sexp_tree_actions.cpp new file mode 100644 index 00000000000..033525d9111 --- /dev/null +++ b/code/missioneditor/sexp_tree_actions.cpp @@ -0,0 +1,1003 @@ +#include "missioneditor/sexp_tree_actions.h" + +#include "parse/sexp.h" +#include "parse/sexp_container.h" + +// Sexp tree action logic — action execution that bridges model and UI. + +// Construct with references to the shared model (tree node data) and the +// UI callback interface (widget updates). Both must outlive this object. +SexpTreeActions::SexpTreeActions(SexpTreeModel& model, ISexpTreeUI& ui) + : _model(model), _ui(ui) +{ +} + +// Free all children of a node from both the model and the UI widget. +// After this call, the node has no children in either layer. +void SexpTreeActions::clear_node_children(int node_index) +{ + int child = _model.tree_nodes[node_index].child; + if (child != -1) + _model.free_node2(child); + + _model.tree_nodes[node_index].child = -1; + + void* h = _model.tree_nodes[node_index].handle; + if (h == nullptr) { + return; + } + + while (_ui.ui_has_children(h)) + _ui.ui_delete_item(_ui.ui_get_child_item(h)); +} + +// ----------------------------------------------------------------------- +// Replace operations +// ----------------------------------------------------------------------- + +// Replace the currently selected node (item_index) with plain data. +// Clears any existing children, sets the node type/text, updates the widget icon and text, +// marks the node as EDITABLE, then walks subsequent siblings to verify argument validity. +void SexpTreeActions::replace_data(const char* data, int type) +{ + int node_idx = _model.item_index; + + clear_node_children(node_idx); + + _model.set_node(node_idx, type, data); + void* h = _model.tree_nodes[node_idx].handle; + _ui.ui_set_item_text(h, data); + NodeImage bmap = _model.get_data_image(node_idx); + _ui.ui_set_item_image(h, bmap); + _model.tree_nodes[node_idx].flags = EDITABLE; + + // check remaining data beyond replaced data for validity + verify_and_fix_arguments(_model.tree_nodes[node_idx].parent); + + if (_model.modified) + *_model.modified = 1; + _ui.ui_update_help(h); +} + +// Replace the currently selected node with a variable reference. +// Builds "varname(value)" display text from the global Sexp_variables table, +// sets the VARIABLE icon, marks NOT_EDITABLE, then verifies subsequent arguments. +void SexpTreeActions::replace_variable_data(int var_idx, int type) +{ + char buf[128]; + + Assertion(type & SEXPT_VARIABLE, "Invalid variable type"); + + int node_idx = _model.item_index; + + clear_node_children(node_idx); + + // Assemble name + snprintf(buf, sizeof(buf), "%s(%s)", Sexp_variables[var_idx].variable_name, Sexp_variables[var_idx].text); + + _model.set_node(node_idx, type, buf); + void* h = _model.tree_nodes[node_idx].handle; + _ui.ui_set_item_text(h, buf); + _ui.ui_set_item_image(h, NodeImage::VARIABLE); + _model.tree_nodes[node_idx].flags = NOT_EDITABLE; + + // check remaining data beyond replaced data for validity + verify_and_fix_arguments(_model.tree_nodes[node_idx].parent); + + if (_model.modified) + *_model.modified = 1; + _ui.ui_update_help(h); +} + +// Replace the currently selected node with a container name reference. +// Clears children, sets CONTAINER_NAME type flags, updates the icon and text, +// and marks the node as NOT_EDITABLE (container names can only be changed via menus). +void SexpTreeActions::replace_container_name(const sexp_container& container) +{ + int node_idx = _model.item_index; + + clear_node_children(node_idx); + + _model.set_node(node_idx, (SEXPT_VALID | SEXPT_STRING | SEXPT_CONTAINER_NAME), container.container_name.c_str()); + void* h = _model.tree_nodes[node_idx].handle; + _ui.ui_set_item_image(h, NodeImage::CONTAINER_NAME); + _ui.ui_set_item_text(h, container.container_name.c_str()); + _model.tree_nodes[node_idx].flags = NOT_EDITABLE; + + if (_model.modified) + *_model.modified = 1; + _ui.ui_update_help(h); +} + +// Replace the currently selected node with a container data reference. +// If test_child_nodes is true and the node is already a container data node of the +// same list type, child nodes (modifiers) are preserved. Otherwise, if delete_child_nodes +// is true, all children are cleared. If set_default_modifier is true, a default modifier +// (map key or list accessor) is added as the first child. +void SexpTreeActions::replace_container_data(const sexp_container& container, + int type, + bool test_child_nodes, + bool delete_child_nodes, + bool set_default_modifier) +{ + int node_idx = _model.item_index; + + // if this is already a container of the right type, don't alter the child nodes + if (test_child_nodes && (_model.tree_nodes[node_idx].type & SEXPT_CONTAINER_DATA)) { + if (container.is_list()) { + const auto* p_old_container = get_sexp_container(_model.tree_nodes[node_idx].text); + + Assertion(p_old_container != nullptr, + "Attempt to Replace Container Data of unknown previous container %s. Please report!", + _model.tree_nodes[node_idx].text); + + if (p_old_container->is_list()) { + if (container.opf_type == p_old_container->opf_type) { + delete_child_nodes = false; + set_default_modifier = false; + } + } + } + } + + if (delete_child_nodes) { + clear_node_children(node_idx); + } + + _model.set_node(node_idx, type, container.container_name.c_str()); + void* h = _model.tree_nodes[node_idx].handle; + _ui.ui_set_item_image(h, NodeImage::CONTAINER_DATA); + _ui.ui_set_item_text(h, container.container_name.c_str()); + _model.tree_nodes[node_idx].flags = NOT_EDITABLE; + + if (set_default_modifier) { + add_default_modifier(container); + } + + if (_model.modified) + *_model.modified = 1; + _ui.ui_update_help(h); +} + +// Replace the currently selected node with a new operator. +// Clears all children (the caller is responsible for adding new arguments), +// sets the OPERATOR type, and marks the node as OPERAND. +void SexpTreeActions::replace_operator(const char* op) +{ + int node_idx = _model.item_index; + + clear_node_children(node_idx); + + _model.set_node(node_idx, (SEXPT_OPERATOR | SEXPT_VALID), op); + void* h = _model.tree_nodes[node_idx].handle; + _ui.ui_set_item_text(h, op); + _model.tree_nodes[node_idx].flags = OPERAND; + + if (_model.modified) + *_model.modified = 1; + _ui.ui_update_help(h); +} + +// ----------------------------------------------------------------------- +// Expand/merge operations +// ----------------------------------------------------------------------- + +// Expand a COMBINED operator+data display into separate operator and child nodes. +// When an operator has a single data argument, FRED can display it in condensed form +// ("operator data" on one line with COMBINED flag). This function expands it back to +// the normal tree structure so additional children can be added. +// If the node itself has the COMBINED flag, walks up to the parent operator first. +void SexpTreeActions::expand_operator(int node) +{ + int data; + + if (_model.tree_nodes[node].flags & COMBINED) { + node = _model.tree_nodes[node].parent; + Assertion((_model.tree_nodes[node].flags & OPERAND) && (_model.tree_nodes[node].flags & EDITABLE), "Invalid parent node"); + } + + if ((_model.tree_nodes[node].flags & OPERAND) && (_model.tree_nodes[node].flags & EDITABLE)) { + Assertion(_model.tree_nodes[node].type & SEXPT_OPERATOR, "Invalid operator"); + void* h = _model.tree_nodes[node].handle; + data = _model.tree_nodes[node].child; + Assertion(data != -1 && _model.tree_nodes[data].next == -1 && _model.tree_nodes[data].child == -1, "Invalid child node"); + + _ui.ui_set_item_text(h, _model.tree_nodes[node].text); + _model.tree_nodes[node].flags = OPERAND; + NodeImage bmap = _model.get_data_image(data); + _model.tree_nodes[data].handle = _ui.ui_insert_item(_model.tree_nodes[data].text, bmap, h, nullptr); + _model.tree_nodes[data].flags = EDITABLE; + _ui.ui_expand_item(h); + } +} + +// ----------------------------------------------------------------------- +// Add operations +// ----------------------------------------------------------------------- + +// Add a plain data child (number or string) under the current node. +// First expands the operator if it was in COMBINED form, then allocates a new +// child node, creates the UI widget item, and marks it EDITABLE. +// Returns the index of the newly created node. +int SexpTreeActions::add_data(const char* data, int type) +{ + int node_idx = _model.item_index; + + expand_operator(node_idx); + int node = _model.allocate_node(node_idx); + _model.set_node(node, type, data); + NodeImage bmap = _model.get_data_image(node); + _model.tree_nodes[node].handle = _ui.ui_insert_item(data, bmap, _model.tree_nodes[node_idx].handle, nullptr); + _model.tree_nodes[node].flags = EDITABLE; + if (_model.modified) + *_model.modified = 1; + return node; +} + +// Add a variable reference child under the current node. +// The data string should be in "varname(value)" format. Sets the VARIABLE icon +// and marks NOT_EDITABLE (variables must be changed via the replace-variable menu). +// Returns the index of the newly created node. +int SexpTreeActions::add_variable_data(const char* data, int type) +{ + Assertion(type & SEXPT_VARIABLE, "Invalid variable type"); + + int node_idx = _model.item_index; + + expand_operator(node_idx); + int node = _model.allocate_node(node_idx); + _model.set_node(node, type, data); + _model.tree_nodes[node].handle = _ui.ui_insert_item(data, NodeImage::VARIABLE, _model.tree_nodes[node_idx].handle, nullptr); + _model.tree_nodes[node].flags = NOT_EDITABLE; + if (_model.modified) + *_model.modified = 1; + return node; +} + +// Add a container name reference child under the current node. +// Validates that the container exists, sets CONTAINER_NAME type flags, +// and marks NOT_EDITABLE. Returns the index of the newly created node. +int SexpTreeActions::add_container_name(const char* container_name) +{ + Assertion(container_name != nullptr, "Attempt to add null container name. Please report!"); + Assertion(get_sexp_container(container_name) != nullptr, + "Attempt to add unknown container name %s. Please report!", + container_name); + + int node_idx = _model.item_index; + + expand_operator(node_idx); + int node = _model.allocate_node(node_idx); + _model.set_node(node, (SEXPT_VALID | SEXPT_CONTAINER_NAME | SEXPT_STRING), container_name); + _model.tree_nodes[node].handle = + _ui.ui_insert_item(container_name, NodeImage::CONTAINER_NAME, _model.tree_nodes[node_idx].handle, nullptr); + _model.tree_nodes[node].flags = NOT_EDITABLE; + if (_model.modified) + *_model.modified = 1; + return node; +} + +// Add a container data reference child under the current node. +// Unlike add_container_name, this creates a node that can have modifier children +// (e.g. map key or list index). Updates item_index to the new node so that +// subsequent calls (like add_default_modifier) operate on it. +void SexpTreeActions::add_container_data(const char* container_name) +{ + Assertion(container_name != nullptr, "Attempt to add null container. Please report!"); + Assertion(get_sexp_container(container_name) != nullptr, + "Attempt to add unknown container %s. Please report!", + container_name); + int node_idx = _model.item_index; + int node = _model.allocate_node(node_idx); + _model.set_node(node, (SEXPT_VALID | SEXPT_CONTAINER_DATA | SEXPT_STRING), container_name); + _model.tree_nodes[node].handle = + _ui.ui_insert_item(container_name, NodeImage::CONTAINER_DATA, _model.tree_nodes[node_idx].handle, nullptr); + _model.tree_nodes[node].flags = NOT_EDITABLE; + _model.item_index = node; + if (_model.modified) + *_model.modified = 1; +} + +// Add an operator child under the current node, or as a new root if item_index == -1. +// For root insertion (labeled-root trees), parent_handle specifies the UI parent. +// Expands the current operator if needed, creates the node with the OPERATOR icon, +// and sets item_index to the new node so default arguments can be added after. +void SexpTreeActions::add_operator(const char* op, void* parent_handle) +{ + int node; + + if (_model.item_index == -1) { + node = _model.allocate_node(-1); + _model.set_node(node, (SEXPT_OPERATOR | SEXPT_VALID), op); + _model.tree_nodes[node].handle = _ui.ui_insert_item(op, NodeImage::OPERATOR, parent_handle, nullptr); + + } else { + expand_operator(_model.item_index); + node = _model.allocate_node(_model.item_index); + _model.set_node(node, (SEXPT_OPERATOR | SEXPT_VALID), op); + _model.tree_nodes[node].handle = _ui.ui_insert_item(op, NodeImage::OPERATOR, _model.tree_nodes[_model.item_index].handle, nullptr); + } + + _model.tree_nodes[node].flags = OPERAND; + _model.item_index = node; + if (_model.modified) + *_model.modified = 1; +} + +// Add the default first modifier child for a container data node. +// For map containers: adds a string key placeholder ("") or number "0". +// For list containers: adds the first list modifier name (e.g. "at"). +// Uses add_data internally, so item_index must point to the container data node. +void SexpTreeActions::add_default_modifier(const sexp_container& container) +{ + sexp_list_item item; + + int type_to_use = (SEXPT_VALID | SEXPT_MODIFIER); + + if (container.is_map()) { + if (any(container.type & ContainerType::STRING_KEYS)) { + item.set_data(""); + type_to_use |= SEXPT_STRING; + } else if (any(container.type & ContainerType::NUMBER_KEYS)) { + item.set_data("0"); + type_to_use |= SEXPT_NUMBER; + } else { + UNREACHABLE("Unknown map container key type %d", static_cast(container.type)); + } + } else if (container.is_list()) { + item.set_data(get_all_list_modifiers()[0].name); + type_to_use |= SEXPT_STRING; + } else { + UNREACHABLE("Unknown container type %d", static_cast(container.type)); + } + + item.type = type_to_use; + add_data(item.text.c_str(), item.type); +} + +// ----------------------------------------------------------------------- +// Compound operations +// ----------------------------------------------------------------------- + +// Add or replace an operator, then fill in all minimum required default arguments. +// If replacing an existing operator and the old arguments are type-compatible with +// the new operator (same count and matching OPF types), the old arguments are preserved. +// Otherwise, children are cleared and rebuilt with defaults via add_default_operator. +void SexpTreeActions::add_or_replace_operator(int op, int replace_flag) +{ + int i, op2; + + int saved_index = _model.item_index; + if (replace_flag) { + if (_model.tree_nodes[_model.item_index].type & SEXPT_OPERATOR) { // are both operators? + op2 = get_operator_index(_model.tree_nodes[_model.item_index].text); + Assertion(op2 >= 0, "Invalid operator index"); + i = _model.count_args(_model.tree_nodes[_model.item_index].child); + if ((i >= Operators[op].min) && (i <= Operators[op].max)) { // are old num args valid? + while (i--) + if (query_operator_argument_type(op2, i) != query_operator_argument_type(op, i)) + break; + + if (i < 0) { // everything is ok, so we can keep old arguments with new operator + _model.set_node(_model.item_index, (SEXPT_OPERATOR | SEXPT_VALID), Operators[op].text.c_str()); + _ui.ui_set_item_text(_model.tree_nodes[_model.item_index].handle, Operators[op].text.c_str()); + _model.tree_nodes[_model.item_index].flags = OPERAND; + return; + } + } + } + + replace_operator(Operators[op].text.c_str()); + + } else { + add_operator(Operators[op].text.c_str()); + } + + // fill in all the required (minimum) arguments with default values + for (i = 0; i < Operators[op].min; i++) + add_default_operator(op, i); + + _ui.ui_expand_item(_model.tree_nodes[saved_index].handle); +} + +// Add a single default argument for position 'argnum' of the operator at 'op_index'. +// Queries get_default_value for the appropriate default, then dispatches to the right +// add method based on the value type: add_or_replace_operator for operator defaults, +// add_variable_data for OPF_VARIABLE_NAME arguments, add_container_name for container +// arguments, and add_data for everything else. +// Returns 0 on success, -1 if no default value was available. +int SexpTreeActions::add_default_operator(int op_index, int argnum) +{ + char buf[256]; + sexp_list_item item; + + int saved_index = _model.item_index; + if (_model._opf.get_default_value(&item, buf, op_index, argnum)) + return -1; + + if (item.type & SEXPT_OPERATOR) { + Assertion(SCP_vector_inbounds(Operators, item.op), "Invalid operator index"); + add_or_replace_operator(item.op); + _model.item_index = saved_index; + + } else { + int sexp_var_index; + // special case for sexps that take variables + const int op_type = query_operator_argument_type(op_index, argnum); + if ((op_type == OPF_VARIABLE_NAME) && ((sexp_var_index = get_index_sexp_variable_name(item.text)) >= 0)) { + int type = SEXPT_VALID | SEXPT_VARIABLE; + if (Sexp_variables[sexp_var_index].type & SEXP_VARIABLE_STRING) { + type |= SEXPT_STRING; + } else if (Sexp_variables[sexp_var_index].type & SEXP_VARIABLE_NUMBER) { + type |= SEXPT_NUMBER; + } else { + Assertion(false, "Unexpected sexp variable type %d for variable '%s'", + Sexp_variables[sexp_var_index].type, item.text.c_str()); + } + + char node_text[2 * TOKEN_LENGTH + 2]; + snprintf(node_text, sizeof(node_text), "%s(%s)", item.text.c_str(), Sexp_variables[sexp_var_index].text); + add_variable_data(node_text, type); + } + // special case for sexps that take containers + else if (item.type & SEXPT_CONTAINER_NAME) { + Assertion(SexpTreeOPF::is_container_name_opf_type(op_type) || op_type == OPF_DATA_OR_STR_CONTAINER, + "Attempt to add default container name for a node of non-container type (%d). Please report!", + op_type); + add_container_name(item.text.c_str()); + } + // modify-variable data type depends on type of variable being modified + else if (Operators[op_index].value == OP_MODIFY_VARIABLE) { + char buf2[256]; + Assertion(argnum == 1, "Invalid argument number"); + sexp_list_item temp_item; + _model._opf.get_default_value(&temp_item, buf2, op_index, 0); + sexp_var_index = get_index_sexp_variable_name(temp_item.text); + Assertion(sexp_var_index != -1, "Invalid variable index"); + + int temp_type = Sexp_variables[sexp_var_index].type; + int type = 0; + if (temp_type & SEXP_VARIABLE_NUMBER) { + type = SEXPT_VALID | SEXPT_NUMBER; + } else if (temp_type & SEXP_VARIABLE_STRING) { + type = SEXPT_VALID | SEXPT_STRING; + } else { + Assertion(false, "Unexpected sexp variable type %d for modify-variable default", temp_type); + } + add_data(item.text.c_str(), type); + } + // all other sexps and parameters + else { + add_data(item.text.c_str(), item.type); + } + } + + return 0; +} + +int SexpTreeActions::insert_operator(int op, void* root_parent_handle) +{ + Assertion(SCP_vector_inbounds(Operators, op), "Invalid operator index"); + Assertion(_model.item_index >= 0, "Invalid selected node index"); + + const int wrapped_node = _model.item_index; + const int parent_node = _model.tree_nodes[wrapped_node].parent; + const int node_flags = _model.tree_nodes[wrapped_node].flags; + void* wrapped_handle = _model.tree_nodes[wrapped_node].handle; + + const int node = _model.allocate_node(parent_node, wrapped_node); + _model.set_node(node, (SEXPT_OPERATOR | SEXPT_VALID), Operators[op].text.c_str()); + _model.tree_nodes[node].flags = node_flags; + + void* parent_handle = nullptr; + if (parent_node >= 0) { + parent_handle = _model.tree_nodes[parent_node].handle; + } else { + if (_model._interface && _model._interface->getFlags()[TreeFlags::LabeledRoot]) { + parent_handle = root_parent_handle; + _model._interface->onRootInserted(wrapped_node, node); + } else { + _model.root_item = node; + } + } + + _model.tree_nodes[node].handle = _ui.ui_insert_item(Operators[op].text.c_str(), NodeImage::OPERATOR, parent_handle, wrapped_handle); + + _ui.ui_move_branch(wrapped_node, node); + _model.item_index = node; + for (int i = 1; i < Operators[op].min; i++) { + add_default_operator(op, i); + } + + _ui.ui_expand_item(_model.tree_nodes[node].handle); + if (_model.modified) { + *_model.modified = 1; + } + + return node; +} + +int SexpTreeActions::add_or_replace_typed_data(int data_idx, bool replace, int add_count, int replace_count) +{ + Assertion(_model.item_index >= 0, "Invalid item index"); + const int op_node = replace ? _model.tree_nodes[_model.item_index].parent : _model.item_index; + Assertion(op_node >= 0, "Invalid operator node"); + + sexp_list_item* list = nullptr; + if (_model.tree_nodes[op_node].type & SEXPT_CONTAINER_DATA) { + if (replace && replace_count == 0) { + list = _model._opf.get_container_modifiers(op_node); + } else { + list = _model._opf.get_container_multidim_modifiers(op_node); + } + } else { + const int op = get_operator_index(_model.tree_nodes[op_node].text); + Assertion(op >= 0, "Invalid operator index"); + const auto argcount = replace ? replace_count : add_count; + const auto type = query_operator_argument_type(op, argcount); + list = _model._opf.get_listing_opf(type, op_node, argcount); + } + Assertion(list, "Failed to get listing OPF"); + + auto* ptr = list; + while (data_idx) { + data_idx--; + ptr = ptr->next; + Assertion(ptr, "Invalid SEXP list"); + } + + Assertion((SEXPT_TYPE(ptr->type) != SEXPT_OPERATOR) && (ptr->op < 0), "Invalid SEXP type or operator"); + expand_operator(_model.item_index); + int added_node = -1; + if (replace) { + replace_data(ptr->text.c_str(), ptr->type); + } else { + added_node = add_data(ptr->text.c_str(), ptr->type); + } + list->destroy(); + return added_node; +} + +void SexpTreeActions::replace_variable_with_type_validation(int var_idx, int current_node_type, bool allow_type_coercion) +{ + Assertion(_model.item_index >= 0, "Invalid item index"); + Assertion((var_idx >= 0) && (var_idx < MAX_SEXP_VARIABLES), "Invalid variable index"); + Assertion((current_node_type & SEXPT_NUMBER) || (current_node_type & SEXPT_STRING), "Invalid node type"); + + int resolved_type = current_node_type; + if (allow_type_coercion) { + if (Sexp_variables[var_idx].type & SEXP_VARIABLE_NUMBER) { + resolved_type = SEXPT_NUMBER; + } else if (Sexp_variables[var_idx].type & SEXP_VARIABLE_STRING) { + resolved_type = SEXPT_STRING; + } else { + Assertion(false, "Unexpected sexp variable type %d for variable index %d", Sexp_variables[var_idx].type, var_idx); + } + } else { + if (resolved_type & SEXPT_NUMBER) { + Assertion(Sexp_variables[var_idx].type & SEXP_VARIABLE_NUMBER, "Invalid variable type"); + } + if (resolved_type & SEXPT_STRING) { + Assertion(Sexp_variables[var_idx].type & SEXP_VARIABLE_STRING, "Invalid variable type"); + } + } + + replace_variable_data(var_idx, (resolved_type | SEXPT_VARIABLE)); +} + +// ----------------------------------------------------------------------- +// Validation +// ----------------------------------------------------------------------- + +// Walk through all arguments of the operator at 'node' and verify each is valid +// for its expected OPF type. For each argument: +// - Gets the valid value listing for the argument's OPF type +// - If the current value isn't in the listing, replaces it with the first valid option +// - If no listing exists and the argument is beyond the operator minimum, removes it +// - For variable arguments, checks that the variable type matches the expected data type +// - Recurses into child operators to validate their arguments too +// Uses a static reentry guard (here_count) to prevent infinite recursion. +void SexpTreeActions::verify_and_fix_arguments(int node) +{ + int op_index, arg_num, type, tmp; + sexp_list_item* list; + sexp_list_item* ptr; + bool is_variable_arg = false; + + if (_verify_arguments_reentry_guard) + return; + + _verify_arguments_reentry_guard++; + op_index = get_operator_index(_model.tree_nodes[node].text); + if (op_index < 0) { + _verify_arguments_reentry_guard--; + return; + } + + tmp = _model.item_index; + + arg_num = 0; + _model.item_index = _model.tree_nodes[node].child; + while (_model.item_index >= 0) { + is_variable_arg = false; + // get listing of valid argument values for node item_index + type = query_operator_argument_type(op_index, arg_num); + // special case for modify-variable + if (type == OPF_AMBIGUOUS) { + is_variable_arg = true; + type = _model.get_modify_variable_type(node); + } + if (_model.tree_nodes[_model.item_index].type & SEXPT_CONTAINER_DATA) { + _model.item_index = _model.tree_nodes[_model.item_index].next; + arg_num++; + continue; + } + if (SexpTreeModel::query_restricted_opf_range(type)) { + list = _model._opf.get_listing_opf(type, node, arg_num); + if (!list && (arg_num >= Operators[op_index].min)) { + _model.free_node(_model.item_index, 1); + _model.item_index = tmp; + _verify_arguments_reentry_guard--; + return; + } + + if (list) { + char* text_ptr; + char default_variable_text[TOKEN_LENGTH]; + if (_model.tree_nodes[_model.item_index].type & SEXPT_VARIABLE) { + if (type == OPF_VARIABLE_NAME) { + get_variable_name_from_sexp_tree_node_text(_model.tree_nodes[_model.item_index].text, default_variable_text); + text_ptr = default_variable_text; + } else { + get_variable_name_from_sexp_tree_node_text(_model.tree_nodes[_model.item_index].text, default_variable_text); + int sexp_var_index = get_index_sexp_variable_name(default_variable_text); + bool types_match = false; + Assertion(sexp_var_index != -1, "Invalid variable index"); + + switch (type) { + case OPF_NUMBER: + case OPF_POSITIVE: + if (Sexp_variables[sexp_var_index].type & SEXP_VARIABLE_NUMBER) { + types_match = true; + } + break; + + default: + if (Sexp_variables[sexp_var_index].type & SEXP_VARIABLE_STRING) { + types_match = true; + } + } + + if (types_match) { + list->destroy(); + list = nullptr; + _model.item_index = _model.tree_nodes[_model.item_index].next; + arg_num++; + continue; + } else { + get_variable_default_text_from_variable_text(_model.tree_nodes[_model.item_index].text, default_variable_text); + text_ptr = default_variable_text; + } + } + } else { + text_ptr = _model.tree_nodes[_model.item_index].text; + } + + ptr = list; + while (ptr) { + if (!stricmp(ptr->text.c_str(), text_ptr)) + break; + + ptr = ptr->next; + } + + if (!ptr) { // argument isn't in list of valid choices + if (list->op >= 0) { + replace_operator(list->text.c_str()); + } else { + replace_data(list->text.c_str(), list->type); + } + } + + } else { + bool invalid = false; + if (type == OPF_AMBIGUOUS) { + if (SEXPT_TYPE(_model.tree_nodes[_model.item_index].type) == SEXPT_OPERATOR) { + invalid = true; + } + } else { + if (SEXPT_TYPE(_model.tree_nodes[_model.item_index].type) != SEXPT_OPERATOR) { + invalid = true; + } + } + + if (invalid) { + replace_data("", (SEXPT_STRING | SEXPT_VALID)); + } + } + + if (_model.tree_nodes[_model.item_index].type & SEXPT_OPERATOR) + verify_and_fix_arguments(_model.item_index); + + if (list) { + list->destroy(); + list = nullptr; + } + } + + // fix the node if it is the argument for modify-variable + if (is_variable_arg) { + switch (type) { + case OPF_AMBIGUOUS: + _model.tree_nodes[_model.item_index].type |= SEXPT_STRING; + _model.tree_nodes[_model.item_index].type &= ~SEXPT_NUMBER; + break; + + case OPF_NUMBER: + _model.tree_nodes[_model.item_index].type |= SEXPT_NUMBER; + _model.tree_nodes[_model.item_index].type &= ~SEXPT_STRING; + break; + + default: + Assertion(false, "Unexpected OPF type %d for modify-variable argument", type); + } + } + + _model.item_index = _model.tree_nodes[_model.item_index].next; + arg_num++; + } + + _model.item_index = tmp; + _verify_arguments_reentry_guard--; +} + +// ----------------------------------------------------------------------- +// Variable/container bulk operations +// ----------------------------------------------------------------------- + +// Remove all references to the named variable throughout the tree. +// Finds all SEXPT_VARIABLE nodes whose text starts with "varname(" and replaces +// them with a plain "number" or "string" placeholder, stripping the SEXPT_VARIABLE flag. +// Preserves item_index across the operation. +void SexpTreeActions::delete_sexp_tree_variable(const char* var_name) +{ + char search_str[64]; + char replace_text[TOKEN_LENGTH]; + + snprintf(search_str, sizeof(search_str), "%s(", var_name); + + int old_item_index = _model.item_index; + + for (int idx = 0; idx < static_cast(_model.tree_nodes.size()); idx++) { + if (_model.tree_nodes[idx].type & SEXPT_VARIABLE) { + if (strstr(_model.tree_nodes[idx].text, search_str) != nullptr) { + Assertion((_model.tree_nodes[idx].type & SEXPT_NUMBER) || (_model.tree_nodes[idx].type & SEXPT_STRING), "Invalid variable type"); + + int type = _model.tree_nodes[idx].type &= ~SEXPT_VARIABLE; + + if (_model.tree_nodes[idx].type & SEXPT_NUMBER) { + strcpy_s(replace_text, "number"); + } else { + strcpy_s(replace_text, "string"); + } + + _model.item_index = idx; + replace_data(replace_text, type); + } + } + } + + _model.item_index = old_item_index; +} + +// Update all references to a renamed or type-changed variable. +// Finds all SEXPT_VARIABLE nodes matching "old_name(" and replaces them with +// fresh variable data from Sexp_variables[sexp_var_index], updating the display +// text, type flags, and icon. Preserves item_index across the operation. +void SexpTreeActions::modify_sexp_tree_variable(const char* old_name, int sexp_var_index) +{ + char search_str[64]; + int type; + + Assertion(Sexp_variables[sexp_var_index].type & SEXP_VARIABLE_SET, "Invalid variable type"); + Assertion((Sexp_variables[sexp_var_index].type & SEXP_VARIABLE_NUMBER) || + (Sexp_variables[sexp_var_index].type & SEXP_VARIABLE_STRING), "Invalid variable type"); + + if (Sexp_variables[sexp_var_index].type & SEXP_VARIABLE_NUMBER) { + type = (SEXPT_NUMBER | SEXPT_VALID); + } else { + type = (SEXPT_STRING | SEXPT_VALID); + } + + int old_item_index = _model.item_index; + + snprintf(search_str, sizeof(search_str), "%s(", old_name); + + for (int idx = 0; idx < static_cast(_model.tree_nodes.size()); idx++) { + if (_model.tree_nodes[idx].type & SEXPT_VARIABLE) { + if (strstr(_model.tree_nodes[idx].text, search_str) != nullptr) { + _model.item_index = idx; + replace_variable_data(sexp_var_index, (type | SEXPT_VARIABLE)); + } + } + } + + _model.item_index = old_item_index; +} + +// ----------------------------------------------------------------------- +// Clipboard operations +// ----------------------------------------------------------------------- + +// Copy the currently selected node (and its subtree) to the global sexp clipboard. +// If a previous clipboard exists, frees it first. The subtree is serialized into +// Sexp_nodes via save_branch and marked persistent to prevent garbage collection. +void SexpTreeActions::clipboard_copy() +{ + if (_model.item_index < 0) + return; + + // If a clipboard already exists, unmark it as persistent and free old clipboard + if (Sexp_clipboard != -1) { + sexp_unmark_persistent(Sexp_clipboard); + free_sexp2(Sexp_clipboard); + } + + // Allocate new clipboard and mark persistent + Sexp_clipboard = _model.save_branch(_model.item_index, 1); + sexp_mark_persistent(Sexp_clipboard); +} + +// Replace the currently selected node with the contents of the global sexp clipboard. +// Dispatches based on clipboard content type: +// - SEXP_ATOM_OPERATOR: replaces with operator, loads children from clipboard +// - SEXP_ATOM_CONTAINER_DATA: replaces with container data, loads modifiers +// - SEXP_ATOM_NUMBER/STRING: replaces with data, handles variable flags +// After replacement, expands the subtree for visibility. +void SexpTreeActions::clipboard_paste_replace() +{ + if (_model.item_index < 0 || Sexp_clipboard < 0) + return; + + // the following assumptions are made.. + Assertion(Sexp_nodes[Sexp_clipboard].type != SEXP_NOT_USED, "Invalid SEXP node type"); + Assertion(Sexp_nodes[Sexp_clipboard].subtype != SEXP_ATOM_LIST, "Invalid SEXP node subtype"); + Assertion(Sexp_nodes[Sexp_clipboard].subtype != SEXP_ATOM_CONTAINER_NAME, + "Attempt to use container name %s from SEXP clipboard. Please report!", + Sexp_nodes[Sexp_clipboard].text); + + if (Sexp_nodes[Sexp_clipboard].subtype == SEXP_ATOM_OPERATOR) { + expand_operator(_model.item_index); + replace_operator(CTEXT(Sexp_clipboard)); + if (Sexp_nodes[Sexp_clipboard].rest != -1) { + _model.load_branch(Sexp_nodes[Sexp_clipboard].rest, _model.item_index); + _ui.ui_add_children_visual(_model.item_index); + } + + } else if (Sexp_nodes[Sexp_clipboard].subtype == SEXP_ATOM_CONTAINER_DATA) { + expand_operator(_model.item_index); + const auto* p_container = get_sexp_container(Sexp_nodes[Sexp_clipboard].text); + Assertion(p_container, + "Attempt to paste unknown container %s. Please report!", + Sexp_nodes[Sexp_clipboard].text); + const auto& container = *p_container; + // this should always be true, but just in case + const bool has_modifiers = (Sexp_nodes[Sexp_clipboard].first != -1); + int new_type = (_model.tree_nodes[_model.item_index].type & ~(SEXPT_VARIABLE | SEXPT_CONTAINER_NAME)) | SEXPT_CONTAINER_DATA; + replace_container_data(container, new_type, false, true, !has_modifiers); + if (has_modifiers) { + _model.load_branch(Sexp_nodes[Sexp_clipboard].first, _model.item_index); + _ui.ui_add_children_visual(_model.item_index); + } else { + add_default_modifier(container); + } + + } else if (Sexp_nodes[Sexp_clipboard].subtype == SEXP_ATOM_NUMBER) { + Assertion(Sexp_nodes[Sexp_clipboard].rest == -1, "Invalid SEXP node rest"); + if (Sexp_nodes[Sexp_clipboard].type & SEXP_FLAG_VARIABLE) { + int var_idx = get_index_sexp_variable_name(Sexp_nodes[Sexp_clipboard].text); + Assertion(var_idx > -1, "Invalid variable index"); + replace_variable_data(var_idx, (SEXPT_VARIABLE | SEXPT_NUMBER | SEXPT_VALID)); + } else { + expand_operator(_model.item_index); + replace_data(CTEXT(Sexp_clipboard), (SEXPT_NUMBER | SEXPT_VALID)); + } + + } else if (Sexp_nodes[Sexp_clipboard].subtype == SEXP_ATOM_STRING) { + Assertion(Sexp_nodes[Sexp_clipboard].rest == -1, "Invalid SEXP node rest"); + if (Sexp_nodes[Sexp_clipboard].type & SEXP_FLAG_VARIABLE) { + int var_idx = get_index_sexp_variable_name(Sexp_nodes[Sexp_clipboard].text); + Assertion(var_idx > -1, "Invalid variable index"); + replace_variable_data(var_idx, (SEXPT_VARIABLE | SEXPT_STRING | SEXPT_VALID)); + } else { + expand_operator(_model.item_index); + replace_data(CTEXT(Sexp_clipboard), (SEXPT_STRING | SEXPT_VALID)); + } + + } else + Assertion(0, "Unknown and/or invalid SEXP type"); + + _ui.ui_expand_branch(_model.tree_nodes[_model.item_index].handle); +} + +// Add the contents of the global sexp clipboard as a new child of the current node. +// Similar to clipboard_paste_replace, but inserts a new child instead of replacing. +// Dispatches based on clipboard content type (operator, container data, number, string). +// After insertion, expands the subtree for visibility. +void SexpTreeActions::clipboard_paste_add() +{ + if (_model.item_index < 0 || Sexp_clipboard < 0) + return; + + // the following assumptions are made.. + Assertion(Sexp_nodes[Sexp_clipboard].type != SEXP_NOT_USED, "Invalid SEXP node type"); + Assertion(Sexp_nodes[Sexp_clipboard].subtype != SEXP_ATOM_LIST, "Invalid SEXP node subtype"); + Assertion(Sexp_nodes[Sexp_clipboard].subtype != SEXP_ATOM_CONTAINER_NAME, + "Attempt to use container name %s from SEXP clipboard. Please report!", + Sexp_nodes[Sexp_clipboard].text); + + if (Sexp_nodes[Sexp_clipboard].subtype == SEXP_ATOM_OPERATOR) { + expand_operator(_model.item_index); + add_operator(CTEXT(Sexp_clipboard)); + if (Sexp_nodes[Sexp_clipboard].rest != -1) { + _model.load_branch(Sexp_nodes[Sexp_clipboard].rest, _model.item_index); + _ui.ui_add_children_visual(_model.item_index); + } + + } else if (Sexp_nodes[Sexp_clipboard].subtype == SEXP_ATOM_CONTAINER_DATA) { + expand_operator(_model.item_index); + add_container_data(Sexp_nodes[Sexp_clipboard].text); + const int modifier_node = Sexp_nodes[Sexp_clipboard].first; + if (modifier_node != -1) { + _model.load_branch(modifier_node, _model.item_index); + _ui.ui_add_children_visual(_model.item_index); + } else { + // this shouldn't happen, but just in case + const auto* p_container = get_sexp_container(Sexp_nodes[Sexp_clipboard].text); + Assertion(p_container, + "Attempt to add-paste unknown container %s. Please report!", + Sexp_nodes[Sexp_clipboard].text); + add_default_modifier(*p_container); + } + + } else if (Sexp_nodes[Sexp_clipboard].subtype == SEXP_ATOM_NUMBER) { + Assertion(Sexp_nodes[Sexp_clipboard].rest == -1, "Invalid SEXP node rest"); + expand_operator(_model.item_index); + add_data(CTEXT(Sexp_clipboard), (SEXPT_NUMBER | SEXPT_VALID)); + + } else if (Sexp_nodes[Sexp_clipboard].subtype == SEXP_ATOM_STRING) { + Assertion(Sexp_nodes[Sexp_clipboard].rest == -1, "Invalid SEXP node rest"); + expand_operator(_model.item_index); + add_data(CTEXT(Sexp_clipboard), (SEXPT_STRING | SEXPT_VALID)); + + } else + Assertion(0, "Unknown and/or invalid SEXP type"); + + _ui.ui_expand_branch(_model.tree_nodes[_model.item_index].handle); +} + +// Rename all container references (both CONTAINER_NAME and CONTAINER_DATA nodes) +// that match old_name to new_name. Updates both the model text and the UI widget text. +// Returns true if any nodes were renamed, false if no matches were found. +bool SexpTreeActions::rename_container_nodes(const SCP_string& old_name, const SCP_string& new_name) +{ + Assertion(!old_name.empty(), + "Attempt to rename container nodes looking for empty name. Please report!"); + Assertion(!new_name.empty(), + "Attempt to rename container nodes with empty name. Please report!"); + Assertion(new_name.length() <= sexp_container::NAME_MAX_LENGTH, + "Attempt to rename container nodes with name %s that is too long (%d > %d). Please report!", + new_name.c_str(), static_cast(new_name.length()), sexp_container::NAME_MAX_LENGTH); + + bool renamed_anything = false; + + for (int node_idx = 0; node_idx < static_cast(_model.tree_nodes.size()); node_idx++) { + if (_model.is_matching_container_node(node_idx, old_name)) { + strcpy_s(_model.tree_nodes[node_idx].text, new_name.c_str()); + _ui.ui_set_item_text(_model.tree_nodes[node_idx].handle, new_name.c_str()); + renamed_anything = true; + } + } + + return renamed_anything; +} diff --git a/code/missioneditor/sexp_tree_actions.h b/code/missioneditor/sexp_tree_actions.h new file mode 100644 index 00000000000..ea590ecdff0 --- /dev/null +++ b/code/missioneditor/sexp_tree_actions.h @@ -0,0 +1,129 @@ +#pragma once + +// Sexp tree action logic — action execution that bridges model and UI. +// These functions modify model data AND update the UI via ISexpTreeUI callbacks. +// Both FRED2 and QtFRED create an instance alongside their SexpTreeModel. + +#include "missioneditor/sexp_tree_model.h" + +struct sexp_container; + +class SexpTreeActions { +public: + // Construct with references to the shared model and the UI callback interface. + // The model provides tree node data; the UI interface updates the visual widget. + SexpTreeActions(SexpTreeModel& model, ISexpTreeUI& ui); + + // --- Replace operations (modify current item_index node) --- + + // Replace the current node with plain data (number or string). + // Clears any children, updates the icon to a data image, sets the node as EDITABLE, + // then verifies and fixes subsequent sibling arguments for validity. + void replace_data(const char* data, int type); + // Replace the current node with a variable reference. + // Builds "varname(value)" display text, sets the VARIABLE icon, marks NOT_EDITABLE, + // then verifies and fixes subsequent sibling arguments. + void replace_variable_data(int var_idx, int type); + // Replace the current node with a container name reference. + // Clears children, sets the CONTAINER_NAME icon, marks NOT_EDITABLE. + void replace_container_name(const sexp_container& container); + // Replace the current node with a container data reference. + // Optionally tests if child nodes can be preserved (same container type), + // optionally deletes children, and optionally adds a default modifier. + void replace_container_data(const sexp_container& container, int type, bool test_child_nodes, bool delete_child_nodes, bool set_default_modifier); + // Replace the current node with a new operator. + // Clears all children (arguments will be re-added by the caller). + void replace_operator(const char* op); + + // --- Expand/merge operations --- + + // If the node has a single COMBINED child, expand it into a proper operator+child display. + // This is used before adding children to an operator that was previously displayed + // in condensed "operator data" form. + void expand_operator(int node); + + // --- Add operations (add child under current item_index node) --- + + // Add a plain data child (number or string) under the current node. + // Expands the operator if needed, returns the new node index. + int add_data(const char* data, int type); + // Add a variable reference child under the current node. + // Sets the VARIABLE icon and marks NOT_EDITABLE. Returns the new node index. + int add_variable_data(const char* data, int type); + // Add a container name child under the current node. + // Sets the CONTAINER_NAME icon and marks NOT_EDITABLE. Returns the new node index. + int add_container_name(const char* container_name); + // Add a container data child under the current node. + // Sets the CONTAINER_DATA icon, marks NOT_EDITABLE, and updates item_index to + // the new node (so subsequent add_default_modifier works on the right node). + void add_container_data(const char* container_name); + // Add an operator child under the current node (or as a new root if item_index == -1). + // parent_handle is used for root-level insertion in labeled-root trees. + // Updates item_index to the newly created operator node. + void add_operator(const char* op, void* parent_handle = nullptr); + // Add the default first modifier for a container (map key placeholder or list modifier). + void add_default_modifier(const sexp_container& container); + + // --- Compound operations --- + + // Add or replace an operator with its full set of default arguments. + // If replacing and the old operator has compatible argument types/count, preserves them. + // Otherwise, clears old arguments and fills in all minimum required defaults. + void add_or_replace_operator(int op, int replace_flag = 0); + // Add a single default argument for position argnum of the given operator. + // Handles special cases: variable names, container names, modify-variable types. + // Returns 0 on success, -1 if no default value available. + int add_default_operator(int op_index, int argnum); + // Insert an operator above the current node and move the current node under it. + // root_parent_handle is only used for labeled-root trees, where roots are inserted + // under a label item instead of as true tree roots. + int insert_operator(int op, void* root_parent_handle = nullptr); + // Resolve and apply a typed add/replace-data menu entry by index. + // add_count and replace_count are the cached argument positions computed by menu state. + int add_or_replace_typed_data(int data_idx, bool replace, int add_count, int replace_count); + // Resolve variable type validation/coercion and replace current node with the variable. + // current_node_type should be the selected node's SEXPT_NUMBER/SEXPT_STRING flags. + void replace_variable_with_type_validation(int var_idx, int current_node_type, bool allow_type_coercion); + + // --- Validation --- + + // Walk through all arguments of the operator at 'node' and verify each one is + // still valid for its expected OPF type. Invalid arguments are replaced with the + // first valid option from the listing. Extra arguments beyond the operator minimum + // are removed if no valid listing exists. Recurses into child operators. + void verify_and_fix_arguments(int node); + + // --- Clipboard operations --- + + // Copy the current node (and its subtree) to the global sexp clipboard. + // Serializes via save_branch and marks as persistent to prevent garbage collection. + void clipboard_copy(); + // Replace the current node with the contents of the sexp clipboard. + // Handles operators (with children), container data (with modifiers), + // numbers, strings, and variable references. + void clipboard_paste_replace(); + // Add the contents of the sexp clipboard as a new child of the current node. + // Similar to paste_replace but inserts instead of replacing. + void clipboard_paste_add(); + + // --- Variable/container bulk operations --- + + // Remove all references to the named variable from the tree. + // Replaces variable nodes with plain "number" or "string" placeholder text. + void delete_sexp_tree_variable(const char* var_name); + // Update all references to a renamed/modified variable. + // Finds nodes matching old_name and replaces them with updated variable data. + void modify_sexp_tree_variable(const char* old_name, int sexp_var_index); + // Rename all container name and container data nodes matching old_name to new_name. + // Returns true if any nodes were renamed. + bool rename_container_nodes(const SCP_string& old_name, const SCP_string& new_name); + +private: + // Delete all UI children of a node and free their model data. + // Resets the model's child link to -1. + void clear_node_children(int node_index); + + SexpTreeModel& _model; // shared tree node data and logic + ISexpTreeUI& _ui; // UI callback interface for widget updates + int _verify_arguments_reentry_guard = 0; +}; diff --git a/code/missioneditor/sexp_tree_model.cpp b/code/missioneditor/sexp_tree_model.cpp new file mode 100644 index 00000000000..c999b087640 --- /dev/null +++ b/code/missioneditor/sexp_tree_model.cpp @@ -0,0 +1,2266 @@ +#include "missioneditor/sexp_tree_model.h" +#include "missioneditor/sexp_annotation_model.h" + +#include "parse/sexp.h" +#include "parse/sexp_container.h" +#include "mission/missiongoals.h" +#include "mission/missionmessage.h" +#include "mission/missionparse.h" +#include "mission/missioncampaign.h" +#include "object/object.h" +#include "object/waypoint.h" +#include "ship/ship.h" +#include "ai/ai.h" +#include "ai/ailua.h" +#include "localization/localize.h" +#include "asteroid/asteroid.h" +#include "fireball/fireballs.h" +#include "gamesnd/gamesnd.h" +#include "graphics/software/FontManager.h" +#include "hud/hudartillery.h" +#include "model/model.h" +#include "nebula/neblightning.h" +#include "starfield/starfield.h" +#include "stats/scoring.h" +#include "weapon/emp.h" + +// Shared sexp tree model — UI-independent data structures and logic. +// Used by both FRED2 (MFC) and QtFRED (Qt) sexp tree implementations. + +constexpr int TREE_NODE_INCREMENT = 100; + +// ----------------------------------------------------------------------- +// sexp_list_item implementation +// ----------------------------------------------------------------------- + +// Initialize this item as an operator. +// If op_num is an op value (>= FIRST_OP), it is converted to an operator index first. +void sexp_list_item::set_op(int op_num) +{ + int op_index = op_num; + + if (op_num >= FIRST_OP) { // do we have an op value instead of an op number (index)? + op_index = -1; + for (int i = 0; i < static_cast(Operators.size()); i++) { + if (op_num == Operators[i].value) { + op_index = i; // convert op value to op number + break; + } + } + } + + if (!SCP_vector_inbounds(Operators, op_index)) { + op = -1; + text = ""; + type = SEXPT_UNINIT; + return; + } + + op = op_index; + text = Operators[op].text; + type = (SEXPT_OPERATOR | SEXPT_VALID); +} + +// Initialize this item as a data element with the given display text and type flags. +void sexp_list_item::set_data(const char* str, int t) +{ + op = -1; + text = str; + type = t; +} + +// Allocate a new item, append it to the end of this linked list, and set it as an operator. +void sexp_list_item::add_op(int op_num) +{ + sexp_list_item* item; + sexp_list_item* ptr; + + item = new sexp_list_item; + ptr = this; + while (ptr->next) + ptr = ptr->next; + + ptr->next = item; + item->set_op(op_num); +} + +// Allocate a new item, append it to the end of this linked list, and set it as data. +void sexp_list_item::add_data(const char* str, int t) +{ + sexp_list_item* item; + sexp_list_item* ptr; + + item = new sexp_list_item; + ptr = this; + while (ptr->next) + ptr = ptr->next; + + ptr->next = item; + item->set_data(str, t); +} + +// Append another linked list to the tail of this one (transfers ownership). +void sexp_list_item::add_list(sexp_list_item* list) +{ + sexp_list_item* ptr; + + ptr = this; + while (ptr->next) + ptr = ptr->next; + + ptr->next = list; +} + +// Delete this item and all subsequent items in the linked list. +// WARNING: 'this' is deleted — do not use the pointer after calling. +void sexp_list_item::destroy() +{ + sexp_list_item* ptr; + sexp_list_item* ptr2; + + ptr = this; + while (ptr) { + ptr2 = ptr->next; + + delete ptr; + ptr = ptr2; + } +} + +// ----------------------------------------------------------------------- +// SexpTreeEditorInterface implementation +// ----------------------------------------------------------------------- + +// Default constructor — no special tree behaviors (no labeled roots, no root deletion). +// Dialogs that need labeled roots (events, goals, cutscenes) pass flags explicitly. +SexpTreeEditorInterface::SexpTreeEditorInterface() + : SexpTreeEditorInterface(flagset()) +{ +} + +// Construct with explicit tree behavior flags. +SexpTreeEditorInterface::SexpTreeEditorInterface(const flagset& flags) + : _flags(flags) +{ +} + +SexpTreeEditorInterface::~SexpTreeEditorInterface() = default; + +// Returns true if mission-specific (non-builtin) messages exist. +bool SexpTreeEditorInterface::hasDefaultMessageParameter() +{ + return Num_messages > Num_builtin_messages; +} + +// Return the names of all mission-specific messages (skipping builtins). +SCP_vector SexpTreeEditorInterface::getMessages() +{ + SCP_vector list; + + for (auto i = Num_builtin_messages; i < Num_messages; i++) { + list.emplace_back(Messages[i].name); + } + + return list; +} + +// Return all mission goal names. Campaign editor overrides this to filter by reference mission. +SCP_vector SexpTreeEditorInterface::getMissionGoals(const SCP_string& /*reference_name*/) +{ + SCP_vector list; + list.reserve(Mission_goals.size()); + + for (const auto& goal : Mission_goals) { + list.emplace_back(goal.name, 0, NAME_LENGTH - 1); + } + + return list; +} + +// Returns true if a goal name can be provided as a default for the given operator. +// Always true for previous-goal operators; otherwise requires goals to exist. +bool SexpTreeEditorInterface::hasDefaultGoal(int operator_value) +{ + return (operator_value == OP_PREVIOUS_GOAL_TRUE) || (operator_value == OP_PREVIOUS_GOAL_FALSE) + || (operator_value == OP_PREVIOUS_GOAL_INCOMPLETE) || !Mission_goals.empty(); +} + +// Return all mission event names. Campaign editor overrides this to filter by reference mission. +SCP_vector SexpTreeEditorInterface::getMissionEvents(const SCP_string& /*reference_name*/) +{ + SCP_vector list; + list.reserve(Mission_events.size()); + + for (const auto& event : Mission_events) { + list.emplace_back(event.name, 0, NAME_LENGTH - 1); + } + + return list; +} + +// Returns true if an event name can be provided as a default for the given operator. +bool SexpTreeEditorInterface::hasDefaultEvent(int operator_value) +{ + return (operator_value == OP_PREVIOUS_EVENT_TRUE) || (operator_value == OP_PREVIOUS_EVENT_FALSE) + || (operator_value == OP_PREVIOUS_EVENT_INCOMPLETE) || !Mission_events.empty(); +} + +// Return the current mission filename (campaign editor overrides for multi-mission lists). +SCP_vector SexpTreeEditorInterface::getMissionNames() +{ + SCP_vector list; + if (*Mission_filename != '\0') { + list.emplace_back(Mission_filename); + } + return list; +} + +// Returns true if a mission filename is currently set (non-empty). +bool SexpTreeEditorInterface::hasDefaultMissionName() +{ + return *Mission_filename != '\0'; +} + +// Returns the expected return type for root operators. Default is OPR_BOOL. +// Debriefing overrides this to return OPR_NULL. +int SexpTreeEditorInterface::getRootReturnType() const +{ + return OPR_BOOL; +} + +// Return the tree behavior flags configured for this editor interface. +const flagset& SexpTreeEditorInterface::getFlags() const +{ + return _flags; +} + +// Returns false by default. Campaign editor overrides to return true, +// which restricts the operator list to campaign-usable operators only. +bool SexpTreeEditorInterface::requireCampaignOperators() const +{ + return false; +} + +// ----------------------------------------------------------------------- +// SexpTreeModel implementation +// ----------------------------------------------------------------------- + +// Initialize model with all state cleared. The UI layer must set _interface and +// modified before using the tree. +SexpTreeModel::SexpTreeModel() + : total_nodes(0), item_index(-1), + root_item(-1), select_sexp_node(-1), flag(0), + _interface(nullptr), modified(nullptr), + _opf(*this) +{ +} + +SexpTreeModel::~SexpTreeModel() = default; + +// ----------------------------------------------------------------------- +// Tree node management +// ----------------------------------------------------------------------- + +// Scan for the first unused slot in the tree_nodes array. Returns -1 if none available. +int SexpTreeModel::find_free_node() const +{ + for (int i = 0; i < static_cast(tree_nodes.size()); i++) { + if (tree_nodes[i].type == SEXPT_UNUSED) + return i; + } + + return -1; +} + +// allocate a node. Remains used until freed. +int SexpTreeModel::allocate_node() +{ + int node = find_free_node(); + + // need more tree nodes? + if (node < 0) { + int old_size = static_cast(tree_nodes.size()); + + Assertion(TREE_NODE_INCREMENT > 0, "Invalid tree node increment"); + + // allocate in blocks of TREE_NODE_INCREMENT + tree_nodes.resize(tree_nodes.size() + TREE_NODE_INCREMENT); + + mprintf(("Bumping dynamic tree node limit from %d to %d...\n", old_size, static_cast(tree_nodes.size()))); + +#ifndef NDEBUG + for (int i = old_size; i < static_cast(tree_nodes.size()); i++) { + sexp_tree_item* item = &tree_nodes[i]; + Assertion(item->type == SEXPT_UNUSED, "Invalid tree node type"); + } +#endif + + // our new sexp is the first out of the ones we just created + node = old_size; + } + + // reset the new node + tree_nodes[node].type = SEXPT_UNINIT; + tree_nodes[node].parent = -1; + tree_nodes[node].child = -1; + tree_nodes[node].next = -1; + tree_nodes[node].flags = 0; + strcpy_s(tree_nodes[node].text, ""); + tree_nodes[node].handle = nullptr; + + total_nodes++; + return node; +} + +// allocate a child node under 'parent'. Appends to end of list. +int SexpTreeModel::allocate_node(int parent, int after) +{ + int i, index = allocate_node(); + + if (parent != -1) { + i = tree_nodes[parent].child; + if (i == -1) { + tree_nodes[parent].child = index; + + } else { + while ((i != after) && (tree_nodes[i].next != -1)) + i = tree_nodes[i].next; + + tree_nodes[index].next = tree_nodes[i].next; + tree_nodes[i].next = index; + } + } + + tree_nodes[index].parent = parent; + return index; +} + +// initialize the data for a node. Should be called right after a new node is allocated. +void SexpTreeModel::set_node(int node, int type, const char* text) +{ + Assertion(type != SEXPT_UNUSED, "Invalid node type"); + Assertion(tree_nodes[node].type != SEXPT_UNUSED, "Uninitialized tree node"); + tree_nodes[node].type = type; + size_t max_length; + if (type & SEXPT_VARIABLE) { + max_length = 2 * TOKEN_LENGTH + 2; + } else if (type & (SEXPT_CONTAINER_NAME | SEXPT_CONTAINER_DATA)) { + max_length = sexp_container::NAME_MAX_LENGTH + 1; + } else { + max_length = TOKEN_LENGTH; + } + Assertion(strlen(text) < max_length, "Text exceeds maximum length"); + strcpy_s(tree_nodes[node].text, text); +} + +// free a node and all its children. Also clears pointers to it, if any. +// node = node chain to free +// cascade = 0: free just this node and children under it. (default) +// !0: free this node and all siblings after it. +void SexpTreeModel::free_node(int node, int cascade) +{ + int i; + + // clear the pointer to node + i = tree_nodes[node].parent; + Assertion(i != -1, "Invalid parent node"); + if (tree_nodes[i].child == node) + tree_nodes[i].child = tree_nodes[node].next; + + else { + i = tree_nodes[i].child; + while (tree_nodes[i].next != -1) { + if (tree_nodes[i].next == node) { + tree_nodes[i].next = tree_nodes[node].next; + break; + } + + i = tree_nodes[i].next; + } + } + + if (!cascade) + tree_nodes[node].next = -1; + + // now free up the node and its children + free_node2(node); +} + +// more simple node freer, which works recursively. It frees the given node and all siblings +// that come after it, as well as all children of these. Doesn't clear any links to any of +// these freed nodes, so make sure all links are broken first. (i.e. use free_node() if you can) +void SexpTreeModel::free_node2(int node) +{ + Assertion(node != -1, "Invalid node index"); + Assertion(tree_nodes[node].type != SEXPT_UNUSED, "Uninitialized tree node"); + Assertion(total_nodes > 0, "Invalid total nodes count"); + if (modified) + *modified = 1; + tree_nodes[node].type = SEXPT_UNUSED; + tree_nodes[node].handle = nullptr; + total_nodes--; + + // Remove any annotation referencing this node so that if allocate_node() + // reuses this slot, the new node won't inherit a stale annotation. + if (annotation_model) + annotation_model->removeByKey(node); + if (tree_nodes[node].child != -1) + free_node2(tree_nodes[node].child); + + if (tree_nodes[node].next != -1) + free_node2(tree_nodes[node].next); +} + +// ----------------------------------------------------------------------- +// Tree loading — populate tree_nodes from global Sexp_nodes +// ----------------------------------------------------------------------- + +// Build "varname(value)" combined text for variable display in tree +void get_combined_variable_name(char* combined_name, const char* sexp_var_name) +{ + int sexp_var_index = get_index_sexp_variable_name(sexp_var_name); + + if (sexp_var_index >= 0) + snprintf(combined_name, 2 * TOKEN_LENGTH + 2, "%s(%s)", Sexp_variables[sexp_var_index].variable_name, Sexp_variables[sexp_var_index].text); + else + snprintf(combined_name, 2 * TOKEN_LENGTH + 2, "%s(undefined)", sexp_var_name); +} + +// Clear all tree nodes and reset counters. If 'op' is provided and non-empty, +// create a single root operator node with that text (e.g. "true" or "false"). +void SexpTreeModel::clear_tree_data(const char* op) +{ + mprintf(("Resetting dynamic tree node limit from " SIZE_T_ARG " to %d...\n", tree_nodes.size(), 0)); + + total_nodes = flag = 0; + tree_nodes.clear(); + + if (op && strlen(op)) { + set_node(allocate_node(-1), (SEXPT_OPERATOR | SEXPT_VALID), op); + } +} + +// After loading, reset select_sexp_node to -1 if the target node was not found. +void SexpTreeModel::post_load() +{ + if (!flag) + select_sexp_node = -1; +} + +// Load a complete sexp formula from the global Sexp_nodes array into tree_nodes. +// If index < 0, creates a single root with the default text. If the root is a +// bare number, converts it to "true"/"false". +void SexpTreeModel::load_tree_data(int index, const char* deflt) +{ + int cur; + + clear_tree_data(); + root_item = 0; + + if (index < 0) { + cur = allocate_node(-1); + set_node(cur, (SEXPT_OPERATOR | SEXPT_VALID), deflt); + return; + } + + if (Sexp_nodes[index].subtype == SEXP_ATOM_NUMBER) { + cur = allocate_node(-1); + if (atoi(Sexp_nodes[index].text)) + set_node(cur, (SEXPT_OPERATOR | SEXPT_VALID), "true"); + else + set_node(cur, (SEXPT_OPERATOR | SEXPT_VALID), "false"); + return; + } + + Assertion(Sexp_nodes[index].subtype == SEXP_ATOM_OPERATOR, "Invalid SEXP node subtype"); + load_branch(index, -1); +} + +// Recursively load a chain of sexp nodes (following rest pointers) as children of 'parent'. +// Handles operators, numbers, strings, variables, and container references. +// Also tracks select_sexp_node for auto-selection after load. +// Returns the index of the first tree node created in this chain. +int SexpTreeModel::load_branch(int index, int parent) +{ + int cur = -1; + char combined_var_name[2 * TOKEN_LENGTH + 2]; + + while (index != -1) { + int additional_flags = SEXPT_VALID; + + // special check for container modifiers + if ((parent != -1) && (tree_nodes[parent].type & SEXPT_CONTAINER_DATA)) { + additional_flags |= SEXPT_MODIFIER; + } + + Assertion(Sexp_nodes[index].type != SEXP_NOT_USED, "Invalid SEXP node type"); + if (Sexp_nodes[index].subtype == SEXP_ATOM_LIST) { + load_branch(Sexp_nodes[index].first, parent); + + } else if (Sexp_nodes[index].subtype == SEXP_ATOM_OPERATOR) { + cur = allocate_node(parent); + if ((index == select_sexp_node) && !flag) { + select_sexp_node = cur; + flag = 1; + } + + set_node(cur, (SEXPT_OPERATOR | additional_flags), Sexp_nodes[index].text); + load_branch(Sexp_nodes[index].rest, cur); + return cur; + + } else if (Sexp_nodes[index].subtype == SEXP_ATOM_NUMBER) { + cur = allocate_node(parent); + if (Sexp_nodes[index].type & SEXP_FLAG_VARIABLE) { + get_combined_variable_name(combined_var_name, Sexp_nodes[index].text); + set_node(cur, (SEXPT_VARIABLE | SEXPT_NUMBER | additional_flags), combined_var_name); + } else { + set_node(cur, (SEXPT_NUMBER | additional_flags), Sexp_nodes[index].text); + } + + } else if (Sexp_nodes[index].subtype == SEXP_ATOM_STRING) { + cur = allocate_node(parent); + if (Sexp_nodes[index].type & SEXP_FLAG_VARIABLE) { + get_combined_variable_name(combined_var_name, Sexp_nodes[index].text); + set_node(cur, (SEXPT_VARIABLE | SEXPT_STRING | additional_flags), combined_var_name); + } else { + set_node(cur, (SEXPT_STRING | additional_flags), Sexp_nodes[index].text); + } + + } else if (Sexp_nodes[index].subtype == SEXP_ATOM_CONTAINER_NAME) { + Assertion(!(additional_flags & SEXPT_MODIFIER), + "Found a container name node %s that is also a container modifier. Please report!", + Sexp_nodes[index].text); + Assertion(get_sexp_container(Sexp_nodes[index].text) != nullptr, + "Attempt to load unknown container data %s into SEXP tree. Please report!", + Sexp_nodes[index].text); + cur = allocate_node(parent); + set_node(cur, (SEXPT_CONTAINER_NAME | SEXPT_STRING | additional_flags), Sexp_nodes[index].text); + + } else if (Sexp_nodes[index].subtype == SEXP_ATOM_CONTAINER_DATA) { + cur = allocate_node(parent); + Assertion(get_sexp_container(Sexp_nodes[index].text) != nullptr, + "Attempt to load unknown container data %s into SEXP tree. Please report!", + Sexp_nodes[index].text); + set_node(cur, (SEXPT_CONTAINER_DATA | SEXPT_STRING | additional_flags), Sexp_nodes[index].text); + load_branch(Sexp_nodes[index].first, cur); + + } else + Assertion(0, "Unknown SEXP node subtype"); + + if ((index == select_sexp_node) && !flag) { + select_sexp_node = cur; + flag = 1; + } + + index = Sexp_nodes[index].rest; + if (index == -1) + return cur; + } + + return cur; +} + +// Load a sub-tree for labeled-root trees (events, goals, cutscenes). +// If index < 0, creates a single operator node with the given text. +// Returns the root node index of the loaded sub-tree. +int SexpTreeModel::load_sub_tree(int index, bool valid, const char* text) +{ + int cur; + + if (index < 0) { + cur = allocate_node(-1); + set_node(cur, (SEXPT_OPERATOR | (valid ? SEXPT_VALID : 0)), text); + return cur; + } + + Assertion(Sexp_nodes[index].subtype == SEXP_ATOM_OPERATOR, "Invalid SEXP node subtype"); + cur = load_branch(index, -1); + return cur; +} + +// Unlink a node from its current parent and re-parent it under 'parent'. +// The node (and its entire subtree) is appended as the last child of the new parent. +void SexpTreeModel::move_branch_data(int source, int parent) +{ + int node; + + if (source == -1) + return; + + // unlink source from its current parent + node = tree_nodes[source].parent; + if (node != -1) { + if (tree_nodes[node].child == source) { + tree_nodes[node].child = tree_nodes[source].next; + } else { + node = tree_nodes[node].child; + while (tree_nodes[node].next != source) { + node = tree_nodes[node].next; + Assertion(node != -1, "Invalid node"); + } + tree_nodes[node].next = tree_nodes[source].next; + } + } + + // link source as child of new parent + tree_nodes[source].parent = parent; + tree_nodes[source].next = -1; + if (parent != -1 && parent != 0) { + if (tree_nodes[parent].child == -1) { + tree_nodes[parent].child = source; + } else { + node = tree_nodes[parent].child; + while (tree_nodes[node].next != -1) + node = tree_nodes[node].next; + tree_nodes[node].next = source; + } + } +} + +// ----------------------------------------------------------------------- +// Tree serialization +// ----------------------------------------------------------------------- + +// Extract just the variable name from "varname(value)" display format into var_name. +static void var_name_from_sexp_tree_text(char* var_name, const char* text) +{ + auto var_name_length = strcspn(text, "("); + Assertion(var_name_length < TOKEN_LENGTH - 1, "Variable name too long"); + + strncpy(var_name, text, var_name_length); + var_name[var_name_length] = '\0'; +} + +// builds an sexp of the tree and returns the index of it. This allocates sexp nodes. +int SexpTreeModel::save_tree(int node) const +{ + if (node < 0) node = root_item; + Assertion(node >= 0, "Invalid root item"); + Assertion(tree_nodes[node].type == (SEXPT_OPERATOR | SEXPT_VALID), "Invalid root item type"); + Assertion(tree_nodes[node].next == -1, "Invalid root item next"); + return save_branch(node); +} + +constexpr int NO_PREVIOUS_NODE = -9; +// called recursively to save a tree branch and everything under it +// SEXPT_CONTAINER_NAME and SEXPT_MODIFIER require no special handling here +int SexpTreeModel::save_branch(int cur, int at_root) const +{ + int start, node = -1, last = NO_PREVIOUS_NODE; + char var_name_text[TOKEN_LENGTH]; + + start = -1; + while (cur != -1) { + if (tree_nodes[cur].type & SEXPT_OPERATOR) { + node = alloc_sexp(tree_nodes[cur].text, SEXP_ATOM, SEXP_ATOM_OPERATOR, -1, save_branch(tree_nodes[cur].child)); + + if ((tree_nodes[cur].parent >= 0) && !at_root) { + node = alloc_sexp("", SEXP_LIST, SEXP_ATOM_LIST, node, -1); + } + } else if (tree_nodes[cur].type & SEXPT_CONTAINER_NAME) { + Assertion(get_sexp_container(tree_nodes[cur].text) != nullptr, + "Attempt to save unknown container %s from SEXP tree. Please report!", + tree_nodes[cur].text); + node = alloc_sexp(tree_nodes[cur].text, SEXP_ATOM, SEXP_ATOM_CONTAINER_NAME, -1, -1); + } else if (tree_nodes[cur].type & SEXPT_CONTAINER_DATA) { + Assertion(get_sexp_container(tree_nodes[cur].text) != nullptr, + "Attempt to save unknown container %s from SEXP tree. Please report!", + tree_nodes[cur].text); + node = alloc_sexp(tree_nodes[cur].text, SEXP_ATOM, SEXP_ATOM_CONTAINER_DATA, save_branch(tree_nodes[cur].child), -1); + } else if (tree_nodes[cur].type & SEXPT_NUMBER) { + // allocate number, maybe variable + if (tree_nodes[cur].type & SEXPT_VARIABLE) { + var_name_from_sexp_tree_text(var_name_text, tree_nodes[cur].text); + node = alloc_sexp(var_name_text, (SEXP_ATOM | SEXP_FLAG_VARIABLE), SEXP_ATOM_NUMBER, -1, -1); + } else { + node = alloc_sexp(tree_nodes[cur].text, SEXP_ATOM, SEXP_ATOM_NUMBER, -1, -1); + } + } else if (tree_nodes[cur].type & SEXPT_STRING) { + // allocate string, maybe variable + if (tree_nodes[cur].type & SEXPT_VARIABLE) { + var_name_from_sexp_tree_text(var_name_text, tree_nodes[cur].text); + node = alloc_sexp(var_name_text, (SEXP_ATOM | SEXP_FLAG_VARIABLE), SEXP_ATOM_STRING, -1, -1); + } else { + node = alloc_sexp(tree_nodes[cur].text, SEXP_ATOM, SEXP_ATOM_STRING, -1, -1); + } + } else { + Assertion(0, "Unknown and/or invalid type"); + } + + if (last == NO_PREVIOUS_NODE) { + start = node; + } else if (last >= 0) { + Sexp_nodes[last].rest = node; + } + + last = node; + Assertion(last != NO_PREVIOUS_NODE, "Invalid last node"); + cur = tree_nodes[cur].next; + if (at_root) { + return start; + } + } + + return start; +} + +// ----------------------------------------------------------------------- +// Tree navigation helpers +// ----------------------------------------------------------------------- + +// Return the 0-based argument position of child_node among parent_node's children. +// Returns -1 if child_node is not a direct child of parent_node. +int SexpTreeModel::find_argument_number(int parent_node, int child_node) const +{ + int arg_num, current_node; + + // code moved/adapted from match_closest_operator + arg_num = 0; + current_node = tree_nodes[parent_node].child; + while (current_node >= 0) + { + // found? + if (current_node == child_node) + return arg_num; + + // continue iterating + arg_num++; + current_node = tree_nodes[current_node].next; + } + + // not found + return -1; +} + +// Walk up the tree from child_node until we find an ancestor whose operator constant +// matches parent_op, then return which argument of that ancestor we came through. +// Returns -1 if no matching ancestor is found. +int SexpTreeModel::find_ancestral_argument_number(int parent_op, int child_node) const +{ + if (child_node == -1) + return -1; + + int parent_node; + int current_node; + + current_node = child_node; + parent_node = tree_nodes[current_node].parent; + + while (parent_node >= 0) + { + // check if the parent operator is the one we're looking for + if (get_operator_const(tree_nodes[parent_node].text) == parent_op) + return find_argument_number(parent_node, current_node); + + // continue iterating up the tree + current_node = parent_node; + parent_node = tree_nodes[current_node].parent; + } + + return -1; +} + +// Check if the given node is inside a when-argument or every-time-argument action list, +// which makes the special string a valid value at this position. +bool SexpTreeModel::is_node_eligible_for_special_argument(int parent_node) const +{ + Assertion(parent_node != -1, + "Attempt to access invalid parent node for special arg eligibility check. Please report!"); + + const int w_arg = find_ancestral_argument_number(OP_WHEN_ARGUMENT, parent_node); + const int e_arg = find_ancestral_argument_number(OP_EVERY_TIME_ARGUMENT, parent_node); + return w_arg >= 1 || e_arg >= 1; +} + +// ----------------------------------------------------------------------- +// Query / analysis functions +// ----------------------------------------------------------------------- + +// Count the number of nodes in the sibling chain starting from 'node'. +int SexpTreeModel::count_args(int node) const +{ + int count = 0; + + while (node != -1) { + count++; + node = tree_nodes[node].next; + } + + return count; +} + +// identify what type of argument this is. You call it with the node of the first argument +// of an operator. It will search through enough of the arguments to determine what type of +// data they are. +int SexpTreeModel::identify_arg_type(int node) const +{ + int type = -1; + + while (node != -1) { + Assertion(tree_nodes[node].type & SEXPT_VALID, "Invalid tree node"); + switch (SEXPT_TYPE(tree_nodes[node].type)) { + case SEXPT_OPERATOR: + type = get_operator_const(tree_nodes[node].text); + Assertion(type, "Invalid operator"); + return query_operator_return_type(type); + + case SEXPT_NUMBER: + return OPR_NUMBER; + + case SEXPT_STRING: // either a ship or a wing + type = SEXP_ATOM_STRING; + break; // don't return, because maybe we can narrow selection down more. + } + + node = tree_nodes[node].next; + } + + return type; +} + +// given a tree node, returns the argument type it should be. +// OPF_NULL means no value (or a "void" value) is returned. OPF_NONE means there shouldn't +// be any argument at this position at all. +int SexpTreeModel::query_node_argument_type(int node) const +{ + int parent_node = tree_nodes[node].parent; + if (parent_node < 0) { // parent nodes are -1 for a top-level operator like 'when' + return OPF_NULL; + } + + int argnum = find_argument_number(parent_node, node); + if (argnum < 0) { + return OPF_NONE; + } + + int op_num = get_operator_index(tree_nodes[parent_node].text); + if (op_num < 0) { + return OPF_NONE; + } + + return query_operator_argument_type(op_num, argnum); +} + +// Determine if a given opf code has a restricted argument range (i.e. has a specific, limited +// set of argument values, or has virtually unlimited possibilities. For example, boolean values +// only have true or false, so it is restricted, but a number could be anything, so it's not. +int SexpTreeModel::query_restricted_opf_range(int opf) +{ + switch (opf) { + case OPF_NUMBER: + case OPF_POSITIVE: + case OPF_WHO_FROM: + + // Goober5000 - these are needed too (otherwise the arguments revert to their defaults) + case OPF_STRING: + case OPF_ANYTHING: + case OPF_CONTAINER_VALUE: // jg18 + case OPF_DATA_OR_STR_CONTAINER: // jg18 + return 0; + } + + return 1; +} + +// Return the 0-based position of 'node' among its parent's children. +// Returns -1 if the node has no valid parent. +int SexpTreeModel::get_sibling_place(int node) const +{ + if (!SCP_vector_inbounds(tree_nodes, node)) + return -1; + + if (!SCP_vector_inbounds(tree_nodes, tree_nodes[node].parent)) + return -1; + + const sexp_tree_item* myparent = &tree_nodes[tree_nodes[node].parent]; + + if (myparent->child == -1) + return -1; + + const sexp_tree_item* mysibling = &tree_nodes[myparent->child]; + + int count = 0; + while (true) { + if (mysibling == &tree_nodes[node]) + break; + + if (mysibling->next == -1) + break; + + count++; + mysibling = &tree_nodes[mysibling->next]; + } + + return count; +} + +// Return a numbered data icon based on sibling position (every 5th argument gets a +// numbered icon for visual grouping). Falls back to plain DATA icon for most positions. +NodeImage SexpTreeModel::get_data_image(int node) const +{ + int count = get_sibling_place(node) + 1; + + if (count <= 0) { + return NodeImage::DATA; + } + + if (count % 5 != 0) { + return NodeImage::DATA; + } + + int idx = (count % 100) / 5; + + // There are 20 numbered data icons (DATA_00 through DATA_95) + if (idx >= 20) { + return NodeImage::DATA; + } + + return static_cast(static_cast(NodeImage::DATA_00) + idx); +} + +// Return TRUE if the root-level operator is the "false" sexp operator. +int SexpTreeModel::query_false(int node) const +{ + if (node < 0) node = root_item; + Assertion(node >= 0, "Invalid node"); + Assertion(tree_nodes[node].type == (SEXPT_OPERATOR | SEXPT_VALID), "Invalid node type"); + Assertion(tree_nodes[node].next == -1, "Invalid node next"); + if (get_operator_const(tree_nodes[node].text) == OP_FALSE) { + return TRUE; + } + + return FALSE; +} + +// Look for the valid operator that is the closest match for 'str' and return the operator +// number of it. What operators are valid is determined by 'node', and an operator is valid +// if it is allowed to fit at position 'node' +SCP_string SexpTreeModel::match_closest_operator(const SCP_string& str, int node) const +{ + int z, op, arg_num, opf; + + z = tree_nodes[node].parent; + if (z < 0) { + return str; + } + + op = get_operator_index(tree_nodes[z].text); + if (op < 0) + return str; + + // determine which argument we are of the parent + arg_num = find_argument_number(z, node); + opf = query_operator_argument_type(op, arg_num); // check argument type at this position + + // find the best operator + int best = sexp_match_closest_operator(str, opf); + if (best < 0) { + Warning(LOCATION, "Unable to find an operator match for string '%s' and argument type %d", str.c_str(), opf); + return str; + } + return Operators[best].text; +} + +// Look up the help text string for the given sexp operator code from the Sexp_help table. +// Returns nullptr if no help text exists for this operator. +const char* SexpTreeModel::help(int code) +{ + int i; + + i = static_cast(Sexp_help.size()); + while (i--) { + if (Sexp_help[i].id == code) + break; + } + + if (i >= 0) + return Sexp_help[i].help.c_str(); + + return nullptr; +} + +// Search all editable, used tree nodes for exact case-insensitive text matches. +// Populates 'find' array with matching node indices (up to max_depth entries). +// Returns the number of matches found. +int SexpTreeModel::find_text(const char* text, int* find, int max_depth) const +{ + int find_count; + + // initialize find + for (int i = 0; i < max_depth; i++) { + find[i] = -1; + } + + find_count = 0; + + for (size_t i = 0; i < tree_nodes.size(); i++) { + // only look at used and editable nodes + if ((tree_nodes[i].flags & EDITABLE) && (tree_nodes[i].type != SEXPT_UNUSED)) { + // find the text + if (!stricmp(tree_nodes[i].text, text)) { + find[find_count++] = static_cast(i); + + // don't exceed max count - array bounds + if (find_count == max_depth) { + break; + } + } + } + } + + return find_count; +} + +// ----------------------------------------------------------------------- +// Free-function utilities for sexp tree variable text +// ----------------------------------------------------------------------- + +// Extract the variable name portion from "varname(value)" display format. +// Copies everything before '(' into var_name. +void get_variable_name_from_sexp_tree_node_text(const char* text, char* var_name) +{ + auto length = strcspn(text, "("); + + strncpy(var_name, text, length); + var_name[length] = '\0'; +} + +// Extract the default value portion from "varname(value)" display format. +// Copies the text between '(' and ')' into default_text. +void get_variable_default_text_from_variable_text(char* text, char* default_text) +{ + char* start; + + // find '(' + start = strstr(text, "("); + Assertion(start, "Invalid variable text format"); + start++; + + // get length and copy all but last char ")" + auto len = strlen(start); + strncpy(default_text, start, len - 1); + + // add null termination + default_text[len - 1] = '\0'; +} + +// ----------------------------------------------------------------------- +// Variable / container utilities +// ----------------------------------------------------------------------- + +// If the currently selected node (item_index) is a variable node, look up and +// return its sexp variable index. Returns -1 if not a variable or invalid. +int SexpTreeModel::get_item_index_to_var_index() const +{ + // check valid item index and node is a variable + if ((item_index >= 0) && SCP_vector_inbounds(tree_nodes, item_index) && (tree_nodes[item_index].type & SEXPT_VARIABLE)) { + return get_tree_name_to_sexp_variable_index(tree_nodes[item_index].text); + } else { + return -1; + } +} + +// Parse a "varname(value)" formatted tree node text, extract the variable name, +// and look up its index in the global Sexp_variables array. +int SexpTreeModel::get_tree_name_to_sexp_variable_index(const char* tree_name) +{ + char var_name[TOKEN_LENGTH]; + + auto chars_to_copy = strcspn(tree_name, "("); + Assertion(chars_to_copy < TOKEN_LENGTH - 1, "Variable name too long"); + + // Copy up to '(' and add null termination + strncpy(var_name, tree_name, chars_to_copy); + var_name[chars_to_copy] = '\0'; + + // Look up index + return get_index_sexp_variable_name(var_name); +} + +// For modify-variable and set-variable-by-index operators, determine the expected +// type of the value argument. Returns OPF_NUMBER for numeric variables, +// OPF_AMBIGUOUS for string variables or when the type can't be determined. +int SexpTreeModel::get_modify_variable_type(int parent) const +{ + int sexp_var_index = -1; + + Assertion(parent >= 0, "Invalid parent node"); + int op_const = get_operator_const(tree_nodes[parent].text); + + Assertion(tree_nodes[parent].child >= 0, "Invalid child node"); + const char* node_text = tree_nodes[tree_nodes[parent].child].text; + + if (op_const == OP_MODIFY_VARIABLE) { + sexp_var_index = get_tree_name_to_sexp_variable_index(node_text); + } else if (op_const == OP_SET_VARIABLE_BY_INDEX) { + if (can_construe_as_integer(node_text)) { + sexp_var_index = atoi(node_text); + } else if (strchr(node_text, '(') && strchr(node_text, ')')) { + // the variable index is itself a variable! + return OPF_AMBIGUOUS; + } + } else { + Int3(); // should not be called otherwise + } + + // if we don't have a valid variable, allow replacement with anything + if (sexp_var_index < 0) + return OPF_AMBIGUOUS; + + if (Sexp_variables[sexp_var_index].type & SEXP_VARIABLE_BLOCK || Sexp_variables[sexp_var_index].type & SEXP_VARIABLE_NOT_USED) { + // assume number so that we can allow tree display of number operators + return OPF_NUMBER; + } else if (Sexp_variables[sexp_var_index].type & SEXP_VARIABLE_NUMBER) { + return OPF_NUMBER; + } else if (Sexp_variables[sexp_var_index].type & SEXP_VARIABLE_STRING) { + return OPF_AMBIGUOUS; + } else { + Int3(); + return 0; + } +} + +// Count how many tree nodes reference the given variable name. +// Matches by checking for "varname(" prefix in variable-type nodes. +int SexpTreeModel::get_variable_count(const char* var_name) const +{ + int count = 0; + char compare_name[64]; + + // get name to compare + strcpy_s(compare_name, var_name); + strcat_s(compare_name, "("); + + // look for compare name + for (const auto& tree_node : tree_nodes) { + if (tree_node.type & SEXPT_VARIABLE) { + if (strstr(tree_node.text, compare_name)) { + count++; + } + } + } + + return count; +} + +// Returns the number of times a variable with this name has been used by player loadout +int SexpTreeModel::get_loadout_variable_count(int var_index) +{ + // we shouldn't be being passed the index of variables that do not exist + Assertion(var_index >= 0 && var_index < MAX_SEXP_VARIABLES, "Invalid variable index"); + + int idx; + int count = 0; + + for (auto& team_datum : Team_data) { + for (idx = 0; idx < team_datum.num_ship_choices; idx++) { + if (!strcmp(team_datum.ship_list_variables[idx], Sexp_variables[var_index].variable_name)) { + count++; + } + + if (!strcmp(team_datum.ship_count_variables[idx], Sexp_variables[var_index].variable_name)) { + count++; + } + } + + for (idx = 0; idx < team_datum.num_weapon_choices; idx++) { + if (!strcmp(team_datum.weaponry_pool_variable[idx], Sexp_variables[var_index].variable_name)) { + count++; + } + if (!strcmp(team_datum.weaponry_amount_variable[idx], Sexp_variables[var_index].variable_name)) { + count++; + } + } + } + + return count; +} + +// Count how many tree nodes are container name or container data references +// matching the given container_name. +int SexpTreeModel::get_container_usage_count(const SCP_string& container_name) const +{ + int count = 0; + + for (int node_idx = 0; node_idx < static_cast(tree_nodes.size()); node_idx++) { + if (is_matching_container_node(node_idx, container_name)) { + count++; + } + } + + return count; +} + +// Check if a node is a valid container reference (name or data) matching the given name. +bool SexpTreeModel::is_matching_container_node(int node, const SCP_string& container_name) const +{ + return (tree_nodes[node].type & SEXPT_VALID) && + (tree_nodes[node].type & (SEXPT_CONTAINER_NAME | SEXPT_CONTAINER_DATA)) && + !stricmp(tree_nodes[node].text, container_name.c_str()); +} + +// Check if the given node's position under its parent expects a container name argument +// (OPF_CONTAINER_NAME, OPF_LIST_CONTAINER_NAME, or OPF_MAP_CONTAINER_NAME). +bool SexpTreeModel::is_container_name_argument(int node) const +{ + Assertion(SCP_vector_inbounds(tree_nodes, node), + "Attempt to check if out-of-range node %d is a container name argument. Please report!", + node); + + if (tree_nodes[node].parent == -1) { + return false; + } + + const int arg_opf_type = query_node_argument_type(node); + return SexpTreeOPF::is_container_name_opf_type(arg_opf_type); +} + +// ----------------------------------------------------------------------- +// Context menu state computation +// ----------------------------------------------------------------------- + +// Returns true if the given operator should be hidden from context menus. +// Includes operators hidden per GitHub issue #6400 and deprecated operators +// that have been superseded by newer versions. +bool SexpTreeModel::is_operator_hidden(int op_value) +{ + switch (op_value) { + // hidden per GitHub issue #6400 + case OP_GET_VARIABLE_BY_INDEX: + case OP_SET_VARIABLE_BY_INDEX: + case OP_COPY_VARIABLE_FROM_INDEX: + case OP_COPY_VARIABLE_BETWEEN_INDEXES: + // deprecated operators + case OP_HITS_LEFT_SUBSYSTEM: + case OP_CUTSCENES_SHOW_SUBTITLE: + case OP_ORDER: + case OP_TECH_ADD_INTEL: + case OP_TECH_REMOVE_INTEL: + case OP_HUD_GAUGE_SET_ACTIVE: + case OP_HUD_ACTIVATE_GAUGE_TYPE: + case OP_JETTISON_CARGO_DELAY: + case OP_STRING_CONCATENATE: + case OP_SET_OBJECT_SPEED_X: + case OP_SET_OBJECT_SPEED_Y: + case OP_SET_OBJECT_SPEED_Z: + case OP_DISTANCE: + case OP_SCRIPT_EVAL: + case OP_TRIGGER_SUBMODEL_ANIMATION: + case OP_ADD_BACKGROUND_BITMAP: + case OP_ADD_SUN_BITMAP: + case OP_JUMP_NODE_SET_JUMPNODE_NAME: + case OP_KEY_RESET: + case OP_SET_ASTEROID_FIELD: + case OP_SET_DEBRIS_FIELD: + case OP_NEBULA_TOGGLE_POOF: + case OP_NEBULA_FADE_POOF: + return true; + default: + return false; + } +} + +// Analyze the currently selected tree node (item_index) and compute the full +// context menu state: which actions are available (edit, copy, cut, paste, delete), +// which operators are enabled for add/replace/insert, and which variables and +// containers can be used as replacements. The UI layers use this state to build +// their context menus without needing to duplicate any of the enabling logic. +SexpContextMenuState SexpTreeModel::compute_context_menu_state() +{ + SexpContextMenuState state; + + ctx_init(state); + if (ctx_handle_labeled_root(state)) { + return state; + } + + Assertion(item_index != -1, "Invalid item index"); + + state.can_edit_text = (tree_nodes[item_index].flags & EDITABLE) != 0; + if (tree_nodes[item_index].parent == -1) { + state.can_delete = false; // can't delete root + } + + ctx_compute_variable_menus(state); + ctx_compute_add_type(state); + int replace_opf_type = ctx_compute_replace_type(state); + ctx_compute_insert_type(state); + ctx_compute_operator_enablement(state); + ctx_validate_clipboard(state, replace_opf_type); + ctx_apply_restrictions(state); + + return state; +} + +// ----------------------------------------------------------------------- +// Private helpers for compute_context_menu_state() +// ----------------------------------------------------------------------- + +// Set initial context menu state: campaign mode flag, annotation availability, +// and whether the modify-variable option should be enabled. +void SexpTreeModel::ctx_init(SexpContextMenuState& state) const +{ + state.campaign_mode = _interface && _interface->requireCampaignOperators(); + + if (_interface && _interface->getFlags()[TreeFlags::AnnotationsAllowed]) { + state.can_edit_comment = true; + state.can_edit_bg_color = true; + } + + state.can_add_variable = true; + state.can_modify_variable = (sexp_variable_count() > 0); +} + +// Handle the labeled-root special case (item_index == -1 in labeled-root mode). +// Sets up a minimal state with only text editing allowed, disables all operator +// menus, and returns true if this special case applies (caller should return early). +bool SexpTreeModel::ctx_handle_labeled_root(SexpContextMenuState& state) const +{ + if (item_index != -1) { + return false; + } + + if (!(_interface && _interface->getFlags()[TreeFlags::LabeledRoot])) { + return false; + } + + state.is_labeled_root = true; + state.is_root_editable = _interface && _interface->getFlags()[TreeFlags::RootEditable]; + state.can_edit_text = state.is_root_editable; + state.can_copy = false; + + int num_ops = static_cast(Operators.size()); + state.op_add_enabled.assign(num_ops, false); + state.op_replace_enabled.assign(num_ops, false); + state.op_insert_enabled.assign(num_ops, false); + + return true; +} + +// Build the variable and container replacement menu entries for the selected node. +// Determines the expected argument type at this position, then populates +// replace_variables, replace_container_names, and replace_container_data +// with per-entry enabled/disabled state based on type compatibility. +void SexpTreeModel::ctx_compute_variable_menus(SexpContextMenuState& state) const +{ + if (item_index < 0) { + return; + } + + int type = tree_nodes[item_index].type; + int parent = tree_nodes[item_index].parent; + if (parent < 0) { + return; + } + + int op = get_operator_index(tree_nodes[parent].text); + Assertion(op >= 0 || tree_nodes[parent].type & SEXPT_CONTAINER_DATA, + "Encountered unknown SEXP operator %s. Please report!", + tree_nodes[parent].text); + int first_arg = tree_nodes[parent].child; + + // get arg count of item to replace (for variable context) + int var_replace_count = 0; + int temp = first_arg; + while (temp != item_index) { + var_replace_count++; + temp = tree_nodes[temp].next; + if (temp == -1) break; + } + + int op_type = 0; + if (op >= 0) { + op_type = query_operator_argument_type(op, var_replace_count); + } else { + Assertion(tree_nodes[parent].type & SEXPT_CONTAINER_DATA, + "Unknown SEXP operator %s. Please report!", + tree_nodes[parent].text); + const auto* p_container = get_sexp_container(tree_nodes[parent].text); + Assertion(p_container != nullptr, + "Found modifier for unknown container %s. Please report!", + tree_nodes[parent].text); + op_type = p_container->opf_type; + } + Assertion(op_type > 0, + "Found invalid operator type %d for node with text %s. Please report!", + op_type, tree_nodes[parent].text); + + // Goober5000 - handle ambiguous type + if (op_type == OPF_AMBIGUOUS) { + int modify_type = get_modify_variable_type(parent); + if (modify_type == OPF_NUMBER) { + type = SEXPT_NUMBER; + } else if (modify_type == OPF_AMBIGUOUS) { + type = SEXPT_STRING; + } else { + Int3(); + type = tree_nodes[first_arg].type; + } + } + + // Goober5000 - certain types accept both integers and a list of strings + if (op_type == OPF_GAME_SND || op_type == OPF_FIREBALL || op_type == OPF_WEAPON_BANK_NUMBER) { + type = SEXPT_NUMBER | SEXPT_STRING; + } + + // jg18 - container values can be anything + if (op_type == OPF_CONTAINER_VALUE) { + type = SEXPT_NUMBER | SEXPT_STRING; + } + + if (!((type & SEXPT_STRING) || (type & SEXPT_NUMBER))) { + return; + } + + // Build variable replacement entries + int max_sexp_vars = MAX_SEXP_VARIABLES; + Assertion(max_sexp_vars < 512, "Invalid max SEXP variables"); + + for (int idx = 0; idx < max_sexp_vars; idx++) { + if (Sexp_variables[idx].type & SEXP_VARIABLE_SET) { + if (Sexp_variables[idx].type & SEXP_VARIABLE_BLOCK) { + continue; + } + + bool enabled = false; + if ((type & SEXPT_STRING) && (Sexp_variables[idx].type & SEXP_VARIABLE_STRING)) { + enabled = true; + } + if ((type & SEXPT_NUMBER) && (Sexp_variables[idx].type & SEXP_VARIABLE_NUMBER)) { + enabled = true; + } + if (op_type == OPF_VARIABLE_NAME) { + state.modify_variable = 1; + enabled = true; + } else { + state.modify_variable = 0; + } + if (op_type == OPF_NAV_POINT) { + enabled = true; + } + if ((type & SEXPT_MODIFIER) && var_replace_count > 0) { + enabled = true; + } + + SexpContextMenuState::VariableEntry entry; + entry.var_index = idx; + entry.enabled = enabled; + state.replace_variables.push_back(entry); + } + } + + // Replace Container Name submenu + if (SexpTreeOPF::is_container_name_opf_type(op_type) || op_type == OPF_DATA_OR_STR_CONTAINER) { + state.show_container_names = true; + for (const auto& container : get_all_sexp_containers()) { + bool enabled = false; + if (op_type == OPF_CONTAINER_NAME) { + enabled = true; + } else if ((op_type == OPF_LIST_CONTAINER_NAME) && container.is_list()) { + enabled = true; + } else if ((op_type == OPF_MAP_CONTAINER_NAME) && container.is_map()) { + enabled = true; + } else if ((op_type == OPF_DATA_OR_STR_CONTAINER) && container.is_of_string_type()) { + enabled = true; + } + state.replace_container_names.push_back({enabled}); + } + } + + // Replace Container Data submenu + if (op_type != OPF_VARIABLE_NAME && (op < 0 || !is_argument_provider_op(Operators[op].value))) { + state.show_container_data = true; + for (const auto& container : get_all_sexp_containers()) { + bool enabled = false; + if ((type & SEXPT_STRING) && any(container.type & ContainerType::STRING_DATA)) { + enabled = true; + } + if ((type & SEXPT_NUMBER) && any(container.type & ContainerType::NUMBER_DATA)) { + enabled = true; + } + if ((tree_nodes[item_index].type & SEXPT_MODIFIER) && var_replace_count > 0) { + enabled = true; + } + state.replace_container_data.push_back({enabled}); + } + } +} + +// Determine what can be added as a new child of the selected node. +// Sets add_type (OPR_* return type for operator filtering), populates +// add_data_list with valid data items, and enables can_add_number/can_add_string. +// Handles both container data nodes and regular operator nodes. +void SexpTreeModel::ctx_compute_add_type(SexpContextMenuState& state) +{ + state.add_type = 0; + + if (tree_nodes[item_index].type & SEXPT_CONTAINER_DATA) { + const int modifier_node = tree_nodes[item_index].child; + Assertion(modifier_node != -1, + "No modifier found for container data node %s. Please report!", + tree_nodes[item_index].text); + const int modifier_add_count = count_args(modifier_node); + + const auto* p_container = get_sexp_container(tree_nodes[item_index].text); + Assertion(p_container, + "Found modifier for unknown container %s. Please report!", + tree_nodes[item_index].text); + + if (modifier_add_count == 1 && p_container->is_list() && + get_list_modifier(tree_nodes[modifier_node].text) == ListModifier::AT_INDEX) { + state.add_type = OPR_NUMBER; + state.can_add_number = true; + } else { + state.add_type = OPR_STRING; + state.add_data_list = _opf.get_container_multidim_modifiers(item_index); + if (state.add_data_list) { + sexp_list_item* ptr = state.add_data_list; + while (ptr) { + if (ptr->op >= 0) { + state.add_enabled_op_indices.push_back(ptr->op); + } + ptr = ptr->next; + } + } + state.can_add_number = true; + state.can_add_string = true; + } + } else if (tree_nodes[item_index].flags & OPERAND) { + state.add_type = OPR_STRING; + int child = tree_nodes[item_index].child; + state.add_count = count_args(child); + int op = get_operator_index(tree_nodes[item_index].text); + Assertion(op >= 0, "Invalid operator index"); + + int type = query_operator_argument_type(op, state.add_count); + state.add_data_opf_type = type; + state.add_data_list = _opf.get_listing_opf(type, item_index, state.add_count); + if (state.add_data_list) { + sexp_list_item* ptr = state.add_data_list; + while (ptr) { + if (ptr->op >= 0) { + state.add_enabled_op_indices.push_back(ptr->op); + } + ptr = ptr->next; + } + } + + if (type == OPF_NONE) { + state.add_type = 0; + } else if (type == OPF_NULL) { + state.add_type = OPR_NULL; + } else if (type == OPF_FLEXIBLE_ARGUMENT) { + state.add_type = OPR_FLEXIBLE_ARGUMENT; + } else if (type == OPF_NUMBER) { + state.add_type = OPR_NUMBER; + state.can_add_number = true; + } else if (type == OPF_POSITIVE) { + state.add_type = OPR_POSITIVE; + state.can_add_number = true; + } else if (type == OPF_BOOL) { + state.add_type = OPR_BOOL; + } else if (type == OPF_AI_GOAL) { + state.add_type = OPR_AI_GOAL; + } else if (type == OPF_CONTAINER_VALUE) { + state.can_add_number = true; + } + + if (state.add_type == OPR_STRING && !SexpTreeOPF::is_container_name_opf_type(type)) { + state.can_add_string = true; + } + } +} + +// Determine what can replace the selected node. Sets replace_type, populates +// replace_data_list, enables can_replace_number/can_replace_string, and may +// disable can_delete if this argument can't be removed. Returns the OPF_* type +// for the replace position (needed by ctx_validate_clipboard). +int SexpTreeModel::ctx_compute_replace_type(SexpContextMenuState& state) +{ + state.replace_type = 0; + int parent = tree_nodes[item_index].parent; + int replace_opf_type = 0; + + if (parent < 0) { + // top node - should be Boolean or Null type + state.replace_type = _interface ? _interface->getRootReturnType() : OPR_BOOL; + return replace_opf_type; + } + + state.replace_type = OPR_STRING; + int op = get_operator_index(tree_nodes[parent].text); + Assertion(op >= 0 || tree_nodes[parent].type & SEXPT_CONTAINER_DATA, + "Encountered unknown SEXP operator %s. Please report!", + tree_nodes[parent].text); + int first_arg = tree_nodes[parent].child; + int count = count_args(tree_nodes[parent].child); + + if (op >= 0) { + if (count <= Operators[op].min) { + state.can_delete = false; + } + } else if ((tree_nodes[parent].type & SEXPT_CONTAINER_DATA) && (item_index == first_arg)) { + Assertion(tree_nodes[item_index].type & SEXPT_MODIFIER, + "Container data %s node's first modifier %s is not a modifier. Please report!", + tree_nodes[parent].text, tree_nodes[item_index].text); + state.can_delete = false; + } + + // get arg count of item to replace + state.replace_count = 0; + int temp = first_arg; + while (temp != item_index) { + state.replace_count++; + temp = tree_nodes[temp].next; + if (temp == -1) break; + } + + int type; + if (op >= 0) { + // maybe gray delete + for (int i = state.replace_count + 1; i < count; i++) { + if (query_operator_argument_type(op, i - 1) != query_operator_argument_type(op, i)) { + state.can_delete = false; + break; + } + } + type = query_operator_argument_type(op, state.replace_count); + } else { + Assertion(tree_nodes[parent].type & SEXPT_CONTAINER_DATA, + "Unknown SEXP operator %s. Please report!", + tree_nodes[parent].text); + const auto* p_container = get_sexp_container(tree_nodes[parent].text); + Assertion(p_container != nullptr, + "Found modifier for unknown container %s. Please report!", + tree_nodes[parent].text); + type = p_container->opf_type; + } + + replace_opf_type = type; + + // special case reset type for ambiguous + if (type == OPF_AMBIGUOUS) { + type = get_modify_variable_type(parent); + } + + // Container modifiers use their own list of possible arguments + sexp_list_item* replace_list; + if (tree_nodes[item_index].type & SEXPT_MODIFIER) { + const auto* p_container = get_sexp_container(tree_nodes[parent].text); + Assertion(p_container != nullptr, + "Found modifier for unknown container %s. Please report!", + tree_nodes[parent].text); + const int first_modifier = tree_nodes[parent].child; + if (state.replace_count == 1 && p_container->is_list() && + get_list_modifier(tree_nodes[first_modifier].text) == ListModifier::AT_INDEX) { + replace_list = nullptr; + state.replace_type = OPR_NUMBER; + } else { + replace_list = _opf.get_container_modifiers(parent); + } + } else { + replace_list = _opf.get_listing_opf(type, parent, state.replace_count); + } + + // special case: don't allow replace data for variable or container names + if ((type != OPF_VARIABLE_NAME) && !SexpTreeOPF::is_container_name_opf_type(type) && replace_list) { + state.replace_data_list = replace_list; + sexp_list_item* ptr = replace_list; + while (ptr) { + if (ptr->op >= 0) { + state.replace_enabled_op_indices.push_back(ptr->op); + } + ptr = ptr->next; + } + } else if (replace_list) { + replace_list->destroy(); + } + + if (type == OPF_NONE) { + state.replace_type = 0; + } else if (type == OPF_NUMBER) { + state.replace_type = OPR_NUMBER; + state.can_replace_number = true; + } else if (type == OPF_POSITIVE) { + state.replace_type = OPR_POSITIVE; + state.can_replace_number = true; + } else if (type == OPF_BOOL) { + state.replace_type = OPR_BOOL; + } else if (type == OPF_NULL) { + state.replace_type = OPR_NULL; + } else if (type == OPF_AI_GOAL) { + state.replace_type = OPR_AI_GOAL; + } else if (type == OPF_FLEXIBLE_ARGUMENT) { + state.replace_type = OPR_FLEXIBLE_ARGUMENT; + } else if (type == OPF_GAME_SND || type == OPF_FIREBALL || type == OPF_WEAPON_BANK_NUMBER) { + state.replace_type = OPR_POSITIVE; + state.can_replace_number = true; + } else if (type == OPF_CONTAINER_VALUE) { + state.can_replace_number = true; + } + + if (state.replace_type == OPR_STRING && !SexpTreeOPF::is_container_name_opf_type(type)) { + state.can_replace_string = true; + } + + if (op >= 0) { + if (Operators[op].value == OP_MODIFY_VARIABLE) { + int modify_type = get_modify_variable_type(parent); + if (modify_type == OPF_NUMBER) { + state.can_replace_number = true; + state.can_replace_string = false; + } + } else if (Operators[op].value == OP_SET_VARIABLE_BY_INDEX) { + if (state.replace_count == 0) { + state.can_replace_number = true; + state.can_replace_string = false; + } else { + int modify_type = get_modify_variable_type(parent); + if (modify_type == OPF_NUMBER) { + state.can_replace_number = true; + state.can_replace_string = false; + } + } + } + } + + // Container modifier special cases for replace number/string + if (tree_nodes[item_index].type & SEXPT_MODIFIER) { + Assertion(tree_nodes[parent].type & SEXPT_CONTAINER_DATA, + "Container modifier found whose parent %s is not a container. Please report!", + tree_nodes[parent].text); + const int first_modifier_node = tree_nodes[parent].child; + Assertion(first_modifier_node != -1, + "Container data node named %s has no modifier. Please report!", + tree_nodes[parent].text); + const auto* p_container = get_sexp_container(tree_nodes[parent].text); + Assertion(p_container, + "Attempt to get first modifier for unknown container %s. Please report!", + tree_nodes[parent].text); + const auto& container = *p_container; + + if (state.replace_count == 0) { + if (container.is_list()) { + state.can_replace_number = false; + state.can_replace_string = false; + state.can_edit_text = false; + } else if (container.is_map()) { + if (any(container.type & ContainerType::STRING_KEYS)) { + state.can_replace_number = false; + state.can_replace_string = true; + } else if (any(container.type & ContainerType::NUMBER_KEYS)) { + state.can_replace_number = true; + state.can_replace_string = false; + } else { + UNREACHABLE("Map container with type %d has unknown key type", static_cast(container.type)); + } + } else { + UNREACHABLE("Unknown container type %d", static_cast(container.type)); + } + } else if (state.replace_count == 1 && container.is_list() && + get_list_modifier(tree_nodes[first_modifier_node].text) == ListModifier::AT_INDEX) { + state.can_replace_number = true; + state.can_replace_string = false; + } else { + state.can_replace_number = true; + state.can_replace_string = true; + } + } + + return replace_opf_type; +} + +// Determine the OPF_* argument type for inserting an operator before the selected node. +// The inserted operator must accept the current node's type as its first argument and +// return a type compatible with the parent's expectation at this position. +void SexpTreeModel::ctx_compute_insert_type(SexpContextMenuState& state) const +{ + int z = tree_nodes[item_index].parent; + Assertion(z >= -1, "Invalid parent node"); + if (z != -1) { + int op = get_operator_index(tree_nodes[z].text); + Assertion(op != -1 || tree_nodes[z].type & SEXPT_CONTAINER_DATA, + "Encountered unknown SEXP operator %s. Please report!", + tree_nodes[z].text); + int j = tree_nodes[z].child; + int insert_count = 0; + while (j != item_index) { + insert_count++; + j = tree_nodes[j].next; + } + + if (op >= 0) { + state.insert_opf_type = query_operator_argument_type(op, insert_count); + } else { + Assertion(tree_nodes[z].type & SEXPT_CONTAINER_DATA, + "Unknown SEXP operator %s. Please report!", + tree_nodes[z].text); + const auto* p_container = get_sexp_container(tree_nodes[z].text); + Assertion(p_container != nullptr, + "Found modifier for unknown container %s. Please report!", + tree_nodes[z].text); + state.insert_opf_type = p_container->opf_type; + } + } else { + state.insert_opf_type = (state.replace_type == OPR_NULL) ? OPF_NULL : OPF_BOOL; + } +} + +// Pre-compute per-operator enabled state for add/replace/insert menus. +// Seeds from data list operator matches, then enables based on return type +// compatibility. Disables operators without default arguments available +// and filters out non-campaign operators in campaign mode. +void SexpTreeModel::ctx_compute_operator_enablement(SexpContextMenuState& state) const +{ + int parent = tree_nodes[item_index].parent; + int num_ops = static_cast(Operators.size()); + state.op_add_enabled.assign(num_ops, false); + state.op_replace_enabled.assign(num_ops, false); + state.op_insert_enabled.assign(num_ops, false); + + // Seed from data list matches + for (int op_idx : state.add_enabled_op_indices) { + state.op_add_enabled[op_idx] = true; + } + for (int op_idx : state.replace_enabled_op_indices) { + state.op_replace_enabled[op_idx] = true; + } + + // Enable replace operators for top-level nodes based on return type + if (parent < 0) { + for (int j = 0; j < num_ops; j++) { + if (query_operator_return_type(j) == state.replace_type) + state.op_replace_enabled[j] = true; + } + } + + // Insert: enable based on return type matching and first-arg type matching + for (int j = 0; j < num_ops; j++) { + int ret = query_operator_return_type(j); + int arg0 = query_operator_argument_type(j, 0); + + // Number/positive equivalence hacks + if ((state.insert_opf_type == OPF_NUMBER) && (arg0 == OPF_POSITIVE)) arg0 = OPF_NUMBER; + if ((state.insert_opf_type == OPF_POSITIVE) && (arg0 == OPF_NUMBER)) arg0 = OPF_POSITIVE; + + if (sexp_query_type_match(state.insert_opf_type, ret) && (Operators[j].min >= 1) && (arg0 == state.insert_opf_type)) { + state.op_insert_enabled[j] = true; + } + } + + // Disable operators that don't have default arguments available + for (int j = 0; j < num_ops; j++) { + if (!_opf.query_default_argument_available(j)) { + state.op_add_enabled[j] = false; + state.op_replace_enabled[j] = false; + state.op_insert_enabled[j] = false; + } + } + + // Disable non-campaign operators in campaign mode + if (state.campaign_mode) { + for (int j = 0; j < num_ops; j++) { + if (!usable_in_campaign(Operators[j].value)) { + state.op_add_enabled[j] = false; + state.op_replace_enabled[j] = false; + state.op_insert_enabled[j] = false; + } + } + } +} + +// Check if the sexp clipboard contents can be pasted in the current context. +// Validates return type compatibility for operators, data type for numbers/strings, +// and container type for container data. Sets can_paste and can_paste_add accordingly. +void SexpTreeModel::ctx_validate_clipboard(SexpContextMenuState& state, int replace_opf_type) +{ + if (Sexp_clipboard <= -1 || Sexp_nodes[Sexp_clipboard].type == SEXP_NOT_USED) { + return; + } + + Assertion(Sexp_nodes[Sexp_clipboard].subtype != SEXP_ATOM_LIST, "Invalid SEXP node subtype"); + Assertion(Sexp_nodes[Sexp_clipboard].subtype != SEXP_ATOM_CONTAINER_NAME, + "Attempt to use container name %s from SEXP clipboard. Please report!", + Sexp_nodes[Sexp_clipboard].text); + + if (Sexp_nodes[Sexp_clipboard].subtype == SEXP_ATOM_OPERATOR) { + int j = get_operator_const(CTEXT(Sexp_clipboard)); + Assertion(j, "Invalid operator"); + int ret = query_operator_return_type(j); + + if ((ret == OPR_POSITIVE) && (state.replace_type == OPR_NUMBER)) + ret = OPR_NUMBER; + if ((ret == OPR_NUMBER) && (state.replace_type == OPR_POSITIVE)) + ret = OPR_POSITIVE; + if (state.replace_type == ret) + state.can_paste = true; + + ret = query_operator_return_type(j); + if ((ret == OPR_POSITIVE) && (state.add_type == OPR_NUMBER)) + ret = OPR_NUMBER; + if (state.add_type == ret) + state.can_paste_add = true; + + } else if (Sexp_nodes[Sexp_clipboard].subtype == SEXP_ATOM_CONTAINER_DATA) { + const auto* p_container = get_sexp_container(Sexp_nodes[Sexp_clipboard].text); + if (p_container != nullptr) { + const auto& container = *p_container; + if (any(container.type & ContainerType::NUMBER_DATA)) { + if (state.replace_type == OPR_NUMBER) + state.can_paste = true; + if (state.add_type == OPR_NUMBER) + state.can_paste_add = true; + } else if (any(container.type & ContainerType::STRING_DATA)) { + if (state.replace_type == OPR_STRING && !SexpTreeOPF::is_container_name_opf_type(replace_opf_type)) + state.can_paste = true; + if (state.add_type == OPR_STRING && !SexpTreeOPF::is_container_name_opf_type(replace_opf_type)) + state.can_paste_add = true; + } else { + UNREACHABLE("Unknown container data type %d", static_cast(container.type)); + } + } + + } else if (Sexp_nodes[Sexp_clipboard].subtype == SEXP_ATOM_NUMBER) { + if ((state.replace_type == OPR_POSITIVE) && (atoi(CTEXT(Sexp_clipboard)) > -1)) + state.can_paste = true; + else if (state.replace_type == OPR_NUMBER) + state.can_paste = true; + + if ((state.add_type == OPR_POSITIVE) && (atoi(CTEXT(Sexp_clipboard)) > -1)) + state.can_paste_add = true; + else if (state.add_type == OPR_NUMBER) + state.can_paste_add = true; + + } else if (Sexp_nodes[Sexp_clipboard].subtype == SEXP_ATOM_STRING) { + if (state.replace_type == OPR_STRING && !SexpTreeOPF::is_container_name_opf_type(replace_opf_type)) + state.can_paste = true; + if (state.add_type == OPR_STRING && !SexpTreeOPF::is_container_name_opf_type(replace_opf_type)) + state.can_paste_add = true; + + } else { + Int3(); // unknown sexp type + } +} + +// Apply final cut/copy/paste restrictions based on node type. +// Container name and modifier nodes cannot be cut or copied; container data +// nodes cannot receive paste-add operations. +void SexpTreeModel::ctx_apply_restrictions(SexpContextMenuState& state) const +{ + state.can_cut = state.can_delete; + + if (tree_nodes[item_index].type & (SEXPT_MODIFIER | SEXPT_CONTAINER_NAME)) { + state.can_cut = false; + state.can_copy = false; + state.can_paste = false; + } + if (tree_nodes[item_index].type & SEXPT_CONTAINER_DATA) { + state.can_paste_add = false; + } +} + +// ----------------------------------------------------------------------- +// compute_help_text — extract help/mini-help text for a node +// ----------------------------------------------------------------------- +// Generate the help box and mini-help box text for the given node. +// For operators: displays the operator's help text from the Sexp_help table. +// For data nodes: shows the relevant argument description from the parent operator's +// help text, and also handles special cases like message text preview, ship flag +// descriptions, and wing flag descriptions. +// Appends any user comment (annotation) to the end of the help text. +SexpTreeModel::HelpTextResult SexpTreeModel::compute_help_text(int node_index, const SCP_string& node_comment) const +{ + HelpTextResult result; + + if (!SCP_vector_inbounds(tree_nodes, node_index) || !tree_nodes[node_index].type) { + result.help_text = node_comment; + return result; + } + + int i = node_index; + + // Prepend empty lines if we have a comment (for non-root nodes) + SCP_string adjusted_comment = node_comment; + if (!adjusted_comment.empty()) + adjusted_comment.insert(0, "\r\n\r\n"); + + if (SEXPT_TYPE(tree_nodes[i].type) == SEXPT_OPERATOR) { + // For operators, mini-help stays empty + } else { + int z = tree_nodes[i].parent; + if (z < 0) { + Warning(LOCATION, "Sexp data \"%s\" has no parent!", tree_nodes[i].text); + return result; + } + + int code = get_operator_const(tree_nodes[z].text); + int index = get_operator_index(tree_nodes[z].text); + int sibling_place = get_sibling_place(i) + 1; + + // Mini-help box + if ((SEXPT_TYPE(tree_nodes[i].type) == SEXPT_NUMBER) + || ((SEXPT_TYPE(tree_nodes[i].type) == SEXPT_STRING) && sibling_place > 0)) { + char buffer[10240] = {""}; + + const char* helpstr = help(code); + bool display_number = true; + + if (helpstr != nullptr) { + char searchstr[32]; + const char* loc = nullptr; + const char* loc2 = nullptr; + + sprintf(searchstr, "\n%d:", sibling_place); + loc = strstr(helpstr, searchstr); + + if (loc == nullptr) { + sprintf(searchstr, "\t%d:", sibling_place); + loc = strstr(helpstr, searchstr); + } + if (loc == nullptr) { + sprintf(searchstr, " %d:", sibling_place); + loc = strstr(helpstr, searchstr); + } + if (loc == nullptr) { + sprintf(searchstr, "%d:", sibling_place); + loc = strstr(helpstr, searchstr); + } + if (loc == nullptr) { + loc = strstr(helpstr, "Rest:"); + } + if (loc == nullptr) { + loc = strstr(helpstr, "All:"); + } + + if (loc != nullptr) { + while (*loc == '\r' || *loc == '\n' || *loc == ' ' || *loc == '\t') + loc++; + + loc2 = strpbrk(loc, "\r\n"); + if (loc2 != nullptr) { + size_t size = loc2 - loc; + strncpy(buffer, loc, size); + if (size < sizeof(buffer)) { + buffer[size] = '\0'; + } + display_number = false; + } else { + strcpy_s(buffer, loc); + display_number = false; + } + } + } + + if (display_number) { + sprintf(buffer, "%d:", sibling_place); + } + + result.mini_help_text = buffer; + } + + if (index >= 0) { + int c = 0; + int j = tree_nodes[z].child; + while ((j >= 0) && (j != i)) { + j = tree_nodes[j].next; + c++; + } + + Assertion(j >= 0, "Invalid child node"); + + // Message text display + if (query_operator_argument_type(index, c) == OPF_MESSAGE) { + for (j = 0; j < Num_messages; j++) { + if (!stricmp(Messages[j].name, tree_nodes[i].text)) { + SCP_string text; + sprintf(text, "Message Text:\r\n%s%s", Messages[j].message, adjusted_comment.c_str()); + result.help_text = text; + return result; + } + } + } + + // Ship flag description + if (query_operator_argument_type(index, c) == OPF_SHIP_FLAG) { + Object::Object_Flags object_flag = Object::Object_Flags::NUM_VALUES; + Ship::Ship_Flags ship_flag = Ship::Ship_Flags::NUM_VALUES; + Mission::Parse_Object_Flags parse_obj_flag = Mission::Parse_Object_Flags::NUM_VALUES; + AI::AI_Flags ai_flag = AI::AI_Flags::NUM_VALUES; + SCP_string desc; + + sexp_check_flag_arrays(tree_nodes[i].text, object_flag, ship_flag, parse_obj_flag, ai_flag); + + if (object_flag != Object::Object_Flags::NUM_VALUES) { + for (size_t n = 0; n < (size_t)Num_object_flag_names; n++) { + if (object_flag == Object_flag_descriptions[n].flag) { + desc = Object_flag_descriptions[n].flag_desc; + break; + } + } + } + + if (ship_flag != Ship::Ship_Flags::NUM_VALUES) { + for (size_t n = 0; n < (size_t)Num_ship_flag_names; n++) { + if (ship_flag == Ship_flag_descriptions[n].flag) { + desc = Ship_flag_descriptions[n].flag_desc; + break; + } + } + } + + if (ai_flag != AI::AI_Flags::NUM_VALUES) { + for (size_t n = 0; n < (size_t)Num_ai_flag_names; n++) { + if (ai_flag == Ai_flag_descriptions[n].flag) { + desc = Ai_flag_descriptions[n].flag_desc; + break; + } + } + } + + if (desc.empty()) { + if (parse_obj_flag != Mission::Parse_Object_Flags::NUM_VALUES) { + for (size_t n = 0; n < (size_t)Num_parse_object_flags; n++) { + if (parse_obj_flag == Parse_object_flag_descriptions[n].def) { + desc = Parse_object_flag_descriptions[n].flag_desc; + break; + } + } + } + } + + if (desc.empty()) + desc = "Unknown flag. Let a coder know!"; + + result.help_text = desc; + return result; + } + + // Wing flag description + if (query_operator_argument_type(index, c) == OPF_WING_FLAG) { + Ship::Wing_Flags wing_flag = Ship::Wing_Flags::NUM_VALUES; + SCP_string desc; + + sexp_check_flag_array(tree_nodes[i].text, wing_flag); + + if (wing_flag != Ship::Wing_Flags::NUM_VALUES) { + for (size_t n = 0; n < (size_t)Num_wing_flag_names; n++) { + if (wing_flag == Wing_flag_descriptions[n].flag) { + desc = Wing_flag_descriptions[n].flag_desc; + break; + } + } + } + + if (desc.empty()) + desc = "Unknown flag. Let a coder know!"; + + result.help_text = desc; + return result; + } + } + + i = z; + } + + int code = get_operator_const(tree_nodes[i].text); + auto str = help(code); + if (!str) { + result.help_text = SCP_string("No help available") + adjusted_comment; + } else { + result.help_text = SCP_string(str) + adjusted_comment; + } + + return result; +} + +// ----------------------------------------------------------------------- +// validate_label_edit — validate and resolve edited node text +// ----------------------------------------------------------------------- +// Validate user-entered text for a tree node. For operator nodes, resolves the +// closest matching valid operator name. For number nodes in OPF_POSITIVE positions, +// rejects negative values. Truncates text to TOKEN_LENGTH. +// Returns a result struct indicating whether the edit should be applied. +SexpTreeModel::LabelEditResult SexpTreeModel::validate_label_edit(int node_index, const SCP_string& new_text) const +{ + LabelEditResult result; + result.resolved_text = new_text; + + if (tree_nodes[node_index].type & SEXPT_OPERATOR) { + result.is_operator = true; + auto op = match_closest_operator(new_text, node_index); + if (op.empty()) { + result.update_node = false; + return result; + } + + result.resolved_text = op; + result.operator_index = get_operator_index(op.c_str()); + if (result.operator_index < 0) { + result.update_node = false; + } + } else if (tree_nodes[node_index].type & SEXPT_NUMBER) { + if (query_node_argument_type(node_index) == OPF_POSITIVE) { + int val = atoi(new_text.c_str()); + if (val < 0) { + result.negative_number_error = true; + result.update_node = false; + } + } + } + + // Truncate to TOKEN_LENGTH + if (result.resolved_text.size() >= TOKEN_LENGTH) { + result.resolved_text.resize(TOKEN_LENGTH - 1); + } + + return result; +} + +// ----------------------------------------------------------------------- +// apply_label_edit — apply validated edit to node text +// ----------------------------------------------------------------------- +// Write the validated text into the tree node's text field, truncating to +// TOKEN_LENGTH and sanitizing any invalid characters (Mantis #2893). +void SexpTreeModel::apply_label_edit(int node_index, const SCP_string& resolved_text) +{ + auto len = resolved_text.size(); + if (len >= TOKEN_LENGTH) + len = TOKEN_LENGTH - 1; + + strncpy(tree_nodes[node_index].text, resolved_text.c_str(), len); + tree_nodes[node_index].text[len] = 0; + + // Sanitize for invalid characters (Mantis #2893) + lcl_fred_replace_stuff(tree_nodes[node_index].text, TOKEN_LENGTH - 1); +} + +// ----------------------------------------------------------------------- +// compute_node_visual_info — determine flags and image for a tree node +// ----------------------------------------------------------------------- +// Determine the editability flags (OPERAND/EDITABLE/NOT_EDITABLE) and the icon +// (NodeImage) for a tree node based on its type. Operators get the OPERAND flag, +// variables and containers are NOT_EDITABLE, and plain data nodes are EDITABLE +// with a position-based numbered icon. +SexpTreeModel::NodeVisualInfo SexpTreeModel::compute_node_visual_info(int node_index) const +{ + NodeVisualInfo info; + + if (tree_nodes[node_index].type & SEXPT_OPERATOR) { + info.flags = OPERAND; + info.image = NodeImage::OPERATOR; + } else if (tree_nodes[node_index].type & SEXPT_VARIABLE) { + info.flags = NOT_EDITABLE; + info.image = NodeImage::VARIABLE; + } else if (tree_nodes[node_index].type & SEXPT_CONTAINER_NAME) { + info.flags = NOT_EDITABLE; + info.image = NodeImage::CONTAINER_NAME; + } else if (tree_nodes[node_index].type & SEXPT_CONTAINER_DATA) { + info.flags = NOT_EDITABLE; + info.image = NodeImage::CONTAINER_DATA; + } else { + info.flags = EDITABLE; + info.image = get_data_image(node_index); + } + + return info; +} diff --git a/code/missioneditor/sexp_tree_model.h b/code/missioneditor/sexp_tree_model.h new file mode 100644 index 00000000000..6829b73bdb4 --- /dev/null +++ b/code/missioneditor/sexp_tree_model.h @@ -0,0 +1,602 @@ +#pragma once + +// Shared sexp tree model — UI-independent data structures and logic. +// Used by both FRED2 (MFC) and QtFRED (Qt) sexp tree implementations. + +#include "missioneditor/sexp_tree_opf.h" +#include "globalincs/globals.h" +#include "globalincs/flagset.h" +#include "globalincs/vmallocator.h" + +class SexpAnnotationModel; + +// ----------------------------------------------------------------------- +// SEXPT_* node type/status constants +// ----------------------------------------------------------------------- + +enum : int { + SEXPT_UNUSED = 0x0000, + SEXPT_UNINIT = 0x0001, + SEXPT_UNKNOWN = 0x0002, +}; + +#define SEXPT_VALID 0x1000 +#define SEXPT_TYPE_MASK 0x07ff +#define SEXPT_TYPE(X) (SEXPT_TYPE_MASK & (X)) + +enum : int { + SEXPT_OPERATOR = 0x0010, + SEXPT_NUMBER = 0x0020, + SEXPT_STRING = 0x0040, + SEXPT_VARIABLE = 0x0080, + SEXPT_CONTAINER_NAME = 0x0100, + SEXPT_CONTAINER_DATA = 0x0200, + SEXPT_MODIFIER = 0x0400, +}; + +// ----------------------------------------------------------------------- +// Node flags (editability) +// ----------------------------------------------------------------------- + +enum : int { + NOT_EDITABLE = 0x00, + OPERAND = 0x01, + EDITABLE = 0x02, + COMBINED = 0x04, +}; + +// ----------------------------------------------------------------------- +// NodeImage — unified icon enum for tree nodes +// ----------------------------------------------------------------------- +// Replaces FRED2's BITMAP_* #defines and QtFRED's NodeImage enum class. +// Numeric values match the original BITMAP_* constants for compatibility. + +enum class NodeImage : int { + OPERATOR = 0, + DATA = 1, + VARIABLE = 2, + ROOT = 3, + ROOT_DIRECTIVE = 4, + CHAIN = 5, + CHAIN_DIRECTIVE = 6, + GREEN_DOT = 7, + BLACK_DOT = 8, + // Numbered data icons (DATA_00 through DATA_95, stepping by 5) + DATA_00 = 9, + DATA_05 = 10, + DATA_10 = 11, + DATA_15 = 12, + DATA_20 = 13, + DATA_25 = 14, + DATA_30 = 15, + DATA_35 = 16, + DATA_40 = 17, + DATA_45 = 18, + DATA_50 = 19, + DATA_55 = 20, + DATA_60 = 21, + DATA_65 = 22, + DATA_70 = 23, + DATA_75 = 24, + DATA_80 = 25, + DATA_85 = 26, + DATA_90 = 27, + DATA_95 = 28, + COMMENT = 29, + CONTAINER_NAME = 30, + CONTAINER_DATA = 31, +}; + +// ----------------------------------------------------------------------- +// Tree behavior mode flags and modes +// ----------------------------------------------------------------------- + +FLAG_LIST(TreeFlags) { + LabeledRoot = 0, + RootDeletable, + RootEditable, + AnnotationsAllowed, + + NUM_VALUES +}; + +// Shared operator-menu sizing limits for sexp tree UI implementations (FRED2 + QtFRED) +constexpr int SEXP_TREE_MAX_OP_MENUS = 30; +constexpr int SEXP_TREE_MAX_SUBMENUS = SEXP_TREE_MAX_OP_MENUS * SEXP_TREE_MAX_OP_MENUS; + + +// ----------------------------------------------------------------------- +// sexp_tree_item — a single node in the sexp tree +// ----------------------------------------------------------------------- +// The handle member is a void* that each UI layer casts to its native type: +// FRED2: static_cast(handle) +// QtFRED: static_cast(handle) + +struct sexp_tree_item { + sexp_tree_item() : type(SEXPT_UNUSED), parent(-1), child(-1), next(-1), flags(0), handle(nullptr) { + text[0] = '\0'; + } + + int type; + int parent; // index of parent node (-1 if none) + int child; // index of first child node (-1 if none) + int next; // index of next sibling (-1 if none) + int flags; + char text[2 * TOKEN_LENGTH + 2]; + void* handle; // opaque UI handle — never dereferenced by model code +}; + +// ----------------------------------------------------------------------- +// sexp_list_item — linked list node for building option listings +// ----------------------------------------------------------------------- + +struct sexp_list_item { + int type; + int op; + SCP_string text; + sexp_list_item* next; + + sexp_list_item() : type(0), op(-1), next(nullptr) {} + + // Initialize this item as an operator (converts op value to index if needed) + void set_op(int op_num); + // Initialize this item as a data element with the given text and type flags + void set_data(const char* str, int t = (SEXPT_STRING | SEXPT_VALID)); + // Append a new operator item to the end of this linked list + void add_op(int op_num); + // Append a new data item to the end of this linked list + void add_data(const char* str, int t = (SEXPT_STRING | SEXPT_VALID)); + // Append another linked list to the end of this one + void add_list(sexp_list_item* list); + // Delete this item and all subsequent items in the linked list + void destroy(); +}; + +// ----------------------------------------------------------------------- +// SexpTreeEditorInterface — context interface implemented by editor dialogs +// ----------------------------------------------------------------------- +// Both FRED2 and QtFRED dialogs can implement this to provide context-dependent +// data (messages, goals, events, mission names) to the shared tree model. +// Uses SCP types — UI layers translate at the boundary as needed. + +class SexpTreeEditorInterface { + flagset _flags; + +public: + // Default constructor — uses empty flags (no labeled roots, no root deletion) + SexpTreeEditorInterface(); + // Construct with explicit tree behavior flags + explicit SexpTreeEditorInterface(const flagset& flags); + virtual ~SexpTreeEditorInterface(); + + // Returns true if there are mission-specific (non-builtin) messages available + virtual bool hasDefaultMessageParameter(); + // Returns the list of mission-specific message names + virtual SCP_vector getMessages(); + + // Returns the list of mission goal names (reference_name used by campaign overrides) + virtual SCP_vector getMissionGoals(const SCP_string& reference_name); + // Returns true if a default goal is available for the given operator value + virtual bool hasDefaultGoal(int operator_value); + + // Returns the list of mission event names (reference_name used by campaign overrides) + virtual SCP_vector getMissionEvents(const SCP_string& reference_name); + // Returns true if a default event is available for the given operator value + virtual bool hasDefaultEvent(int operator_value); + + // Returns available mission filenames for the mission-name OPF type + virtual SCP_vector getMissionNames(); + // Returns true if a mission filename is currently set + virtual bool hasDefaultMissionName(); + + // Returns the expected return type for root-level operators (default: OPR_BOOL) + virtual int getRootReturnType() const; + + // Returns the tree behavior flags for this editor interface + const flagset& getFlags() const; + + // Returns true if this editor operates in campaign mode (affects operator filtering) + virtual bool requireCampaignOperators() const; + + // Callbacks for labeled-root tree operations (overridden by dialog classes) + // Called when a labeled root node is deleted; returns the formula node to select next + virtual int onRootDeleted(int formula_node) { return formula_node; } + // Called when a labeled root node's text is renamed + virtual void onRootRenamed(int formula_node, const char* new_name) { (void)formula_node; (void)new_name; } + // Called when a new formula is inserted under a labeled root + virtual void onRootInserted(int old_formula, int new_formula) { (void)old_formula; (void)new_formula; } + // Called when labeled root nodes are reordered via drag-and-drop + virtual void onRootMoved(int node1, int node2, bool insert_before) { (void)node1; (void)node2; (void)insert_before; } +}; + +// ----------------------------------------------------------------------- +// Shared free-function utilities for sexp tree variable text +// ----------------------------------------------------------------------- + +// Extract variable name from "varname(value)" format +void get_variable_name_from_sexp_tree_node_text(const char* text, char* var_name); +// Extract default value from "varname(value)" format +void get_variable_default_text_from_variable_text(char* text, char* default_text); +// Build "varname(value)" combined text for variable display in tree +void get_combined_variable_name(char* combined_name, const char* sexp_var_name); + +// ----------------------------------------------------------------------- +// ISexpTreeUI — callback interface for UI operations +// ----------------------------------------------------------------------- +// Action code in SexpTreeActions calls these to update the UI widget. +// FRED2 implements with MFC CTreeCtrl calls; QtFRED with QTreeWidget calls. +// Handles are opaque void* — each UI layer casts to its native type. + +class ISexpTreeUI { +public: + virtual ~ISexpTreeUI() = default; + + // Tree widget manipulation + + // Insert a new tree item with the given text/icon under parent_handle, after insert_after. + // Returns the newly created opaque handle. + virtual void* ui_insert_item(const char* text, NodeImage image, void* parent_handle, void* insert_after) = 0; + // Remove a tree item from the widget + virtual void ui_delete_item(void* handle) = 0; + // Update the displayed text of a tree item + virtual void ui_set_item_text(void* handle, const char* text) = 0; + // Update the icon of a tree item + virtual void ui_set_item_image(void* handle, NodeImage image) = 0; + // Return the first child handle of the given tree item, or nullptr if none + virtual void* ui_get_child_item(void* handle) = 0; + // Return true if the tree item has any children + virtual bool ui_has_children(void* handle) = 0; + // Expand a single tree item to show its children + virtual void ui_expand_item(void* handle) = 0; + // Set the tree's current selection to this item + virtual void ui_select_item(void* handle) = 0; + // Scroll the tree view so that this item is visible + virtual void ui_ensure_visible(void* handle) = 0; + + // Subtree operations + + // Recursively add child tree items for all children of the given model node + virtual void ui_add_children_visual(int parent_node_index) = 0; + // Move an existing subtree so source_node becomes a child of parent_node + virtual void ui_move_branch(int source_node, int parent_node) = 0; + // Recursively expand the subtree rooted at the given handle + virtual void ui_expand_branch(void* handle) = 0; + + // Notifications + + // Notify the UI that model data has been modified (sets dirty flag) + virtual void ui_notify_modified() = 0; + // Recalculate and display help text for the given tree item + virtual void ui_update_help(void* handle) = 0; +}; + +// ----------------------------------------------------------------------- +// Context menu state — computed by model, consumed by UI to build menus +// ----------------------------------------------------------------------- + +struct SexpContextMenuState { + // Special labeled root case (item_index == -1 with labeled root mode) + bool is_labeled_root = false; + bool is_root_editable = false; + + // Simple boolean flags + bool can_edit_text = false; + bool can_edit_comment = false; + bool can_edit_bg_color = false; + bool can_add_variable = true; + bool can_modify_variable = false; + bool can_copy = true; + bool can_cut = false; + bool can_paste = false; + bool can_paste_add = false; + bool can_delete = true; + bool can_replace_number = false; + bool can_replace_string = false; + bool can_add_number = false; + bool can_add_string = false; + + // Types for operator enabling in menus + int add_type = 0; // OPR_* return type expected for add context + int replace_type = 0; // OPR_* return type expected for replace context + int insert_opf_type = 0; // OPF_* type for insert context + + // State needed by command handlers + int add_count = 0; + int replace_count = 0; + int modify_variable = 0; + + // Campaign mode filtering + bool campaign_mode = false; + + // Data lists for add/replace data submenus + // Entries with op >= 0 are operators to enable; op < 0 are data items for the submenu + sexp_list_item* add_data_list = nullptr; + int add_data_opf_type = 0; // OPF_* type, needed for OPF_VARIABLE_NAME display + sexp_list_item* replace_data_list = nullptr; + + // Operators enabled from data lists (indices into Operators[]) + SCP_vector add_enabled_op_indices; + SCP_vector replace_enabled_op_indices; + + // Per-operator enabled state (indexed by operator index, sized to Operators.size()) + // These incorporate all enable/disable logic: data list matches, default argument + // availability, return type matching, insert type matching, and campaign mode. + SCP_vector op_add_enabled; + SCP_vector op_replace_enabled; + SCP_vector op_insert_enabled; + + // Variable replacement menu entries + struct VariableEntry { + int var_index; + bool enabled; + }; + SCP_vector replace_variables; + + // Container replacement menu entries (index = position in get_all_sexp_containers()) + struct ContainerEntry { + bool enabled; + }; + bool show_container_names = false; + SCP_vector replace_container_names; + bool show_container_data = false; + SCP_vector replace_container_data; + + SexpContextMenuState() = default; + ~SexpContextMenuState() + { + cleanup(); + } + SexpContextMenuState(const SexpContextMenuState&) = delete; + SexpContextMenuState& operator=(const SexpContextMenuState&) = delete; + SexpContextMenuState(SexpContextMenuState&& other) noexcept + { + swap(other); + } + SexpContextMenuState& operator=(SexpContextMenuState&& other) noexcept + { + if (this != &other) { + SexpContextMenuState tmp; + swap(other); // take other's resources + other.swap(tmp); // give other a clean state (tmp gets our old resources and destroys them) + } + return *this; + } + + void swap(SexpContextMenuState& other) noexcept + { + using std::swap; + swap(is_labeled_root, other.is_labeled_root); + swap(is_root_editable, other.is_root_editable); + swap(can_edit_text, other.can_edit_text); + swap(can_edit_comment, other.can_edit_comment); + swap(can_edit_bg_color, other.can_edit_bg_color); + swap(can_add_variable, other.can_add_variable); + swap(can_modify_variable, other.can_modify_variable); + swap(can_copy, other.can_copy); + swap(can_cut, other.can_cut); + swap(can_paste, other.can_paste); + swap(can_paste_add, other.can_paste_add); + swap(can_delete, other.can_delete); + swap(can_replace_number, other.can_replace_number); + swap(can_replace_string, other.can_replace_string); + swap(can_add_number, other.can_add_number); + swap(can_add_string, other.can_add_string); + swap(add_type, other.add_type); + swap(replace_type, other.replace_type); + swap(insert_opf_type, other.insert_opf_type); + swap(add_count, other.add_count); + swap(replace_count, other.replace_count); + swap(modify_variable, other.modify_variable); + swap(campaign_mode, other.campaign_mode); + swap(add_data_list, other.add_data_list); + swap(add_data_opf_type, other.add_data_opf_type); + swap(replace_data_list, other.replace_data_list); + swap(add_enabled_op_indices, other.add_enabled_op_indices); + swap(replace_enabled_op_indices, other.replace_enabled_op_indices); + swap(op_add_enabled, other.op_add_enabled); + swap(op_replace_enabled, other.op_replace_enabled); + swap(op_insert_enabled, other.op_insert_enabled); + swap(replace_variables, other.replace_variables); + swap(show_container_names, other.show_container_names); + swap(replace_container_names, other.replace_container_names); + swap(show_container_data, other.show_container_data); + swap(replace_container_data, other.replace_container_data); + } + + // Free the dynamically allocated data lists (safe to call multiple times) + void cleanup() { + if (add_data_list) { add_data_list->destroy(); add_data_list = nullptr; } + if (replace_data_list) { replace_data_list->destroy(); replace_data_list = nullptr; } + } +}; + +// ----------------------------------------------------------------------- +// SexpTreeModel — shared UI-independent sexp tree model +// ----------------------------------------------------------------------- +// Owns tree node data and provides all pure-logic operations. +// Both FRED2 and QtFRED sexp_tree classes delegate to this model. +// OPF listing functions are in the owned SexpTreeOPF _opf member (see sexp_tree_opf.h). + +class SexpTreeModel { +public: + SexpTreeModel(); + ~SexpTreeModel(); + + // Tree node storage + SCP_vector tree_nodes; + int total_nodes; + int item_index; // currently selected node index, or -1 if none (or labeled root selected) + + // Tree loading state + int root_item; + int select_sexp_node; // translates global sexp node index to tree node during load + int flag; // "found select_sexp_node" flag during load + + // Reset select_sexp_node to -1 if it was not found during load_branch + void post_load(); + + // Editor context interface (set by UI layer) + SexpTreeEditorInterface* _interface; + + // Modification tracking — UI layer sets this to point at its dirty flag. + // Model code sets *modified = 1 when tree data changes. + int* modified; + + // Optional annotation model — when set, free_node2() automatically removes + // annotations referencing freed nodes to prevent stale annotation reuse. + SexpAnnotationModel* annotation_model = nullptr; + + // --- Tree navigation helpers --- + + // Return the 0-based argument position of child_node among parent_node's children, or -1 + int find_argument_number(int parent_node, int child_node) const; + // Walk up ancestors to find parent_op, then return the argument position we traversed through + int find_ancestral_argument_number(int parent_op, int child_node) const; + // Check if a node is inside a when-argument/every-time-argument action list (eligible for ) + bool is_node_eligible_for_special_argument(int parent_node) const; + + // --- Tree node management --- + + // Find the first unused slot in tree_nodes, or return -1 if all are used + int find_free_node() const; + // Allocate a standalone node (no parent linkage), growing tree_nodes if needed + int allocate_node(); + // Allocate a node as a child of 'parent', inserted after 'after' (-1 = append to end) + int allocate_node(int parent, int after = -1); + // Set the type and text of an already-allocated node + void set_node(int node, int type, const char* text); + // Free a node and its children; if cascade!=0 also free all subsequent siblings + void free_node(int node, int cascade = 0); + // Internal recursive free — frees node, all its children, and all its next siblings + void free_node2(int node); + + // --- Tree loading (populate tree_nodes from Sexp_nodes) --- + + // Clear all tree data and optionally create a single root operator node + void clear_tree_data(const char* op = nullptr); + // Load a complete sexp formula into tree_nodes from global Sexp_nodes array + void load_tree_data(int index, const char* deflt = "true"); + // Recursively load a branch of the sexp tree, returning the first node created + int load_branch(int index, int parent); + // Load a sub-tree (used for labeled-root trees like events/goals), returns root node + int load_sub_tree(int index, bool valid, const char* text); + + // --- Tree structure manipulation --- + + // Move a node (and its subtree) from its current parent to a new parent + void move_branch_data(int source, int parent); + + // --- Tree serialization --- + + // Serialize the entire tree back into global Sexp_nodes, returning the root sexp index + int save_tree(int node = -1) const; + // Recursively serialize a branch, returning the starting sexp node index + int save_branch(int cur, int at_root = 0) const; + + // --- Query / analysis functions --- + + // Count the number of sibling nodes starting from 'node' (following next pointers) + int count_args(int node) const; + // Determine the return type of the argument chain starting at 'node' + int identify_arg_type(int node) const; + // Return the OPF_* argument type expected at this node's position under its parent + int query_node_argument_type(int node) const; + // Returns non-zero if the given OPF type has a restricted (enumerated) set of values + static int query_restricted_opf_range(int opf); + // Return the 0-based sibling position of this node among its parent's children + int get_sibling_place(int node) const; + // Return the appropriate numbered data icon for a node based on its sibling position + NodeImage get_data_image(int node) const; + // Return TRUE if the root operator is OP_FALSE + int query_false(int node = -1) const; + // Find the valid operator whose name most closely matches 'str' at position 'node' + SCP_string match_closest_operator(const SCP_string& str, int node) const; + // Look up the help text string for the given sexp operator code + static const char* help(int code); + // Search all editable nodes for matching text; populates 'find' array, returns match count + int find_text(const char* text, int* find, int max_depth) const; + + // --- Help text computation --- + struct HelpTextResult { + SCP_string help_text; // full help text for the help box + SCP_string mini_help_text; // short argument description for the mini-help box + }; + // Compute help and mini-help text for a tree node, including any user comment + HelpTextResult compute_help_text(int node_index, const SCP_string& node_comment) const; + + // --- Label edit validation --- + struct LabelEditResult { + bool update_node = true; + bool is_operator = false; + SCP_string resolved_text; // text after operator matching / validation + int operator_index = -1; // operator index if resolved, -1 otherwise + bool negative_number_error = false; // true if user entered negative for OPF_POSITIVE + }; + // Validate user-entered text for a node; resolves operator names and checks constraints + LabelEditResult validate_label_edit(int node_index, const SCP_string& new_text) const; + // Apply validated text to a node, truncating to TOKEN_LENGTH and sanitizing characters + void apply_label_edit(int node_index, const SCP_string& resolved_text); + + // --- Node visual info for tree building --- + struct NodeVisualInfo { + int flags; // OPERAND, EDITABLE, or NOT_EDITABLE + NodeImage image; // icon to display for this node + }; + // Determine the editability flags and icon for a node based on its type + NodeVisualInfo compute_node_visual_info(int node_index) const; + + // --- Variable / container utilities --- + + // Convert the currently selected item_index to its corresponding sexp variable index + int get_item_index_to_var_index() const; + // Parse a tree node's "varname(value)" text and look up the sexp variable index + static int get_tree_name_to_sexp_variable_index(const char* tree_name); + // For modify-variable operators, determine if the target variable expects OPF_NUMBER or OPF_AMBIGUOUS + int get_modify_variable_type(int parent) const; + // Count how many tree nodes reference the given variable name + int get_variable_count(const char* var_name) const; + // Count how many times a variable is used in player loadout data (Team_data) + static int get_loadout_variable_count(int var_index); + // Count how many tree nodes reference the given container name + int get_container_usage_count(const SCP_string& container_name) const; + // Check if a specific node is a valid, matching container reference for the given name + bool is_matching_container_node(int node, const SCP_string& container_name) const; + // Check if a node's parent operator expects a container name at this argument position + bool is_container_name_argument(int node) const; + + // --- OPF listing and container modifier queries --- + // Owned by this model. Provides get_listing_opf(), get_container_modifiers(), etc. + // See sexp_tree_opf.h for the full API. + SexpTreeOPF _opf; + + // --- Context menu state computation --- + + // Analyze the current selection and compute which context menu actions are available. + // The returned state includes operator enablement, variable/container menus, and + // clipboard paste validation. Returned state owns temporary lists and cleans them up automatically. + SexpContextMenuState compute_context_menu_state(); + // Returns true if the given operator value should be hidden from menus (deprecated/hidden ops) + static bool is_operator_hidden(int op_value); + +private: + // --- Private helpers for compute_context_menu_state() --- + + // Set campaign mode, annotation availability, and variable support + void ctx_init(SexpContextMenuState& state) const; + // Handle the labeled-root special case; returns true if state is complete (early return) + bool ctx_handle_labeled_root(SexpContextMenuState& state) const; + // Build the variable and container replacement menu entries for the selected node + void ctx_compute_variable_menus(SexpContextMenuState& state) const; + // Determine what can be added as a new child of the selected node + void ctx_compute_add_type(SexpContextMenuState& state); + // Determine what can replace the selected node; returns the OPF type for clipboard validation + int ctx_compute_replace_type(SexpContextMenuState& state); + // Determine the OPF type for inserting an operator before the selected node + void ctx_compute_insert_type(SexpContextMenuState& state) const; + // Pre-compute which operators are enabled for add/replace/insert + void ctx_compute_operator_enablement(SexpContextMenuState& state) const; + // Check if the clipboard contents can be pasted in the current context + static void ctx_validate_clipboard(SexpContextMenuState& state, int replace_opf_type); + // Apply final cut/copy/paste restrictions based on node type + void ctx_apply_restrictions(SexpContextMenuState& state) const; +}; diff --git a/code/missioneditor/sexp_tree_opf.cpp b/code/missioneditor/sexp_tree_opf.cpp new file mode 100644 index 00000000000..727ae4847d8 --- /dev/null +++ b/code/missioneditor/sexp_tree_opf.cpp @@ -0,0 +1,3299 @@ +#include "missioneditor/sexp_tree_opf.h" +#include "missioneditor/sexp_tree_model.h" + +#include "parse/sexp.h" +#include "globalincs/linklist.h" +#include "object/object.h" +#include "object/waypoint.h" +#include "ship/ship.h" +#include "prop/prop.h" +#include "iff_defs/iff_defs.h" +#include "ai/ai.h" +#include "ai/aigoals.h" +#include "hud/hudartillery.h" +#include "gamesnd/eventmusic.h" +#include "mission/missionparse.h" +#include "mission/missionmessage.h" +#include "missioneditor/common.h" +#include "model/model.h" +#include "sound/ds.h" +#include "hud/hud.h" +#include "graphics/software/FontManager.h" +#include "hud/hudsquadmsg.h" +#include "controlconfig/controlsconfig.h" +#include "mission/missiongoals.h" +#include "mission/missioncampaign.h" +#include "stats/medals.h" +#include "menuui/techmenu.h" +#include "weapon/weapon.h" +#include "jumpnode/jumpnode.h" +#include "starfield/starfield.h" +#include "nebula/neblightning.h" +#include "nebula/neb.h" +#include "graphics/2d.h" +#include "model/animation/modelanimation.h" +#include "globalincs/alphacolors.h" +#include "asteroid/asteroid.h" +#include "fireball/fireballs.h" +#include "species_defs/species_defs.h" +#include "localization/localize.h" +#include "gamesnd/gamesnd.h" +#include "parse/sexp/sexp_lookup.h" +#include "ai/ailua.h" +#include "stats/scoring.h" +#include "parse/sexp_container.h" +#include "weapon/emp.h" + +// OPF listing functions for the sexp tree. +// All get_listing_opf_*() functions are implemented here as SexpTreeOPF methods. +// Uses _model back-reference for read-only access to tree_nodes[] and _interface. + +SexpTreeOPF::SexpTreeOPF(SexpTreeModel& model) : _model(model) {} + +sexp_list_item *SexpTreeOPF::get_listing_opf_null() +{ + int i; + sexp_list_item head; + + for (i=0; i(Operators.size()); i++) + if (query_operator_return_type(i) == OPR_NULL) + head.add_op(i); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_flexible_argument() +{ + int i; + sexp_list_item head; + + for (i=0; i(Operators.size()); i++) + if (query_operator_return_type(i) == OPR_FLEXIBLE_ARGUMENT) + head.add_op(i); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_bool(int parent_node) +{ + int i, only_basic; + sexp_list_item head; + + // search for the previous goal/event operators. If found, only add the true/false + // sexpressions to the list + only_basic = 0; + if ( parent_node != -1 ) { + int op; + + op = get_operator_const(_model.tree_nodes[parent_node].text); + if ( (op == OP_PREVIOUS_GOAL_TRUE) || (op == OP_PREVIOUS_GOAL_FALSE) || (op == OP_PREVIOUS_EVENT_TRUE) || (op == OP_PREVIOUS_EVENT_FALSE) ) + only_basic = 1; + + } + + for (i=0; i(Operators.size()); i++) { + if (query_operator_return_type(i) == OPR_BOOL) { + if ( !only_basic || (only_basic && ((Operators[i].value == OP_TRUE) || (Operators[i].value == OP_FALSE))) ) { + head.add_op(i); + } + } + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_positive() +{ + int i, z; + sexp_list_item head; + + for (i=0; i(Operators.size()); i++) { + z = query_operator_return_type(i); + // Goober5000's number hack + if ((z == OPR_NUMBER) || (z == OPR_POSITIVE)) + head.add_op(i); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_number() +{ + int i, z; + sexp_list_item head; + + for (i=0; i(Operators.size()); i++) { + z = query_operator_return_type(i); + if ((z == OPR_NUMBER) || (z == OPR_POSITIVE)) + head.add_op(i); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ship(int parent_node) +{ + object *ptr; + sexp_list_item head; + int op = 0, dock_ship = -1, require_cap_ship = 0; + + // look at the parent node and get the operator. Some ship lists should be filtered based + // on what the parent operator is + if ( parent_node >= 0 ) { + op = get_operator_const(_model.tree_nodes[parent_node].text); + + // get the dock_ship number of if this goal is an ai dock goal. used to prune out unwanted ships out + // of the generated ship list + dock_ship = -1; + if ( op == OP_AI_DOCK ) { + int z; + + z = _model.tree_nodes[parent_node].parent; + Assertion(z >= 0, "Invalid parent node"); + Assertion(!stricmp(_model.tree_nodes[z].text, "add-ship-goal") || !stricmp(_model.tree_nodes[z].text, "add-wing-goal") || !stricmp(_model.tree_nodes[z].text, "add-goal"), "Invalid parent node type"); + + z = _model.tree_nodes[z].child; + Assertion(z >= 0, "Invalid child node"); + + dock_ship = ship_name_lookup(_model.tree_nodes[z].text, 1); + Assertion(dock_ship != -1, "Invalid dock ship"); + } + } + + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if ((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) { + if ( op == OP_AI_DOCK ) { + // only include those ships in the list which the given ship can dock with. + if ( (dock_ship != ptr->instance) && ship_docking_valid(dock_ship , ptr->instance) ) + head.add_data(Ships[ptr->instance].ship_name ); + + } + else if (op == OP_CAP_SUBSYS_CARGO_KNOWN_DELAY) { + if ( ((Ship_info[Ships[ptr->instance].ship_info_index].is_huge_ship()) && // big ship + !(Ships[ptr->instance].flags[Ship::Ship_Flags::Toggle_subsystem_scanning]) )|| // which is not flagged OR + ((!(Ship_info[Ships[ptr->instance].ship_info_index].is_huge_ship())) && // small ship + (Ships[ptr->instance].flags[Ship::Ship_Flags::Toggle_subsystem_scanning]) ) ) { // which is flagged + + head.add_data(Ships[ptr->instance].ship_name); + } + } + else { + if ( !require_cap_ship || Ship_info[Ships[ptr->instance].ship_info_index].is_huge_ship() ) { + head.add_data(Ships[ptr->instance].ship_name); + } + } + } + + ptr = GET_NEXT(ptr); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_prop() +{ + object *ptr; + sexp_list_item head; + + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (ptr->type == OBJ_PROP) { + auto p = prop_id_lookup(ptr->instance); + if (p != nullptr) { + head.add_data(p->prop_name); + } + } + + ptr = GET_NEXT(ptr); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_wing() +{ + int i; + sexp_list_item head; + + for (i=0; i(Iff_info.size()); i++) + head.add_data(Iff_info[i].iff_name); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ai_class() +{ + int i; + sexp_list_item head; + + for (i=0; i"); + + for (const auto& ship_info : Ship_info) { + if (ship_info.flags[Ship::Info_Flags::Support]) { + head.add_data(ship_info.name); + } + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ssm_class() +{ + sexp_list_item head; + + for (const auto& ssmp : Ssm_info) { + head.add_data(ssmp.name); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ship_with_bay() +{ + object *objp; + sexp_list_item head; + + head.add_data(""); + + for ( objp = GET_FIRST(&obj_used_list); objp != END_OF_LIST(&obj_used_list); objp = GET_NEXT(objp) ) + { + if ( (objp->type == OBJ_SHIP) || (objp->type == OBJ_START) ) + { + // determine if this ship has a hangar bay + if (ship_has_hangar_bay(objp->instance)) + { + head.add_data(Ships[objp->instance].ship_name); + } + } + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_soundtrack_name() +{ + sexp_list_item head; + + head.add_data(""); + + for (auto &st: Soundtracks) + head.add_data(st.name); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_arrival_location() +{ + int i; + sexp_list_item head; + + for (i=0; i(Iff_info.size()); i++) + { + char tmp[NAME_LENGTH + 15]; + stuff_special_arrival_anchor_name(tmp, i, restrict_to_players, false); + + head.add_data(tmp); + } + } + + for ( objp = GET_FIRST(&obj_used_list); objp != END_OF_LIST(&obj_used_list); objp = GET_NEXT(objp) ) + { + if ( (objp->type == OBJ_SHIP) || (objp->type == OBJ_START) ) + { + head.add_data(Ships[objp->instance].ship_name); + } + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ai_goal(int parent_node) +{ + int i, n, w, z, child; + sexp_list_item head; + + Assertion(parent_node >= 0, "Invalid parent node"); + child = _model.tree_nodes[parent_node].child; + if (child < 0) + return nullptr; + + n = ship_name_lookup(_model.tree_nodes[child].text, 1); + if (n >= 0) { + // add operators if it's an ai-goal and ai-goal is allowed for that ship + for (i=0; i(Operators.size()); i++) { + if ( (query_operator_return_type(i) == OPR_AI_GOAL) && query_sexp_ai_goal_valid(Operators[i].value, n) ) + head.add_op(i); + } + + } else { + z = wing_name_lookup(_model.tree_nodes[child].text); + if (z >= 0) { + for (w=0; w(Operators.size()); i++) { + if ( (query_operator_return_type(i) == OPR_AI_GOAL) && query_sexp_ai_goal_valid(Operators[i].value, n) ) + head.add_op(i); + } + } + // when dealing with the special argument add them all. It's up to the FREDder to ensure invalid orders aren't given + } else if (!strcmp(_model.tree_nodes[child].text, SEXP_ARGUMENT_STRING)) { + for (i=0; i(Operators.size()); i++) { + if (query_operator_return_type(i) == OPR_AI_GOAL) { + head.add_op(i); + } + } + } else + return nullptr; // no valid ship or wing to check against, make nothing available + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_message() const +{ + sexp_list_item head; + + for (auto& msg : _model._interface->getMessages()) { + head.add_data(msg.c_str()); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_docker_point(int parent_node, int arg_num) +{ + int z; + sexp_list_item head; + int sh = -1; + + Assertion(parent_node >= 0, "Invalid parent node"); + Assertion(!stricmp(_model.tree_nodes[parent_node].text, "ai-dock") || !stricmp(_model.tree_nodes[parent_node].text, "set-docked") || + get_operator_const(_model.tree_nodes[parent_node].text) >= static_cast(First_available_operator_id), "Invalid node type"); + + if (!stricmp(_model.tree_nodes[parent_node].text, "ai-dock")) + { + z = _model.tree_nodes[parent_node].parent; + if (z < 0) + return nullptr; + Assertion(!stricmp(_model.tree_nodes[z].text, "add-ship-goal") || !stricmp(_model.tree_nodes[z].text, "add-wing-goal") || !stricmp(_model.tree_nodes[z].text, "add-goal"), "Invalid parent node type"); + + z = _model.tree_nodes[z].child; + if (z < 0) + return nullptr; + sh = ship_name_lookup(_model.tree_nodes[z].text, 1); + } + else if (!stricmp(_model.tree_nodes[parent_node].text, "set-docked")) + { + //Docker ship should be the first child node + z = _model.tree_nodes[parent_node].child; + if (z < 0) + return nullptr; + sh = ship_name_lookup(_model.tree_nodes[z].text, 1); + } + // for Lua sexps + else if (get_operator_const(_model.tree_nodes[parent_node].text) >= static_cast(First_available_operator_id)) + { + int this_index = get_dynamic_parameter_index(_model.tree_nodes[parent_node].text, arg_num); + + if (this_index >= 0) { + z = _model.tree_nodes[parent_node].child; + + for (int j = 0; j < this_index; j++) { + z = _model.tree_nodes[z].next; + } + + sh = ship_name_lookup(_model.tree_nodes[z].text, 1); + } else { + error_display(1, "Expected to find a dynamic lua parent parameter for node %i in operator %s but found nothing!", + arg_num, + _model.tree_nodes[parent_node].text); + } + } + + if (sh >= 0) + { + auto model_num = Ship_info[Ships[sh].ship_info_index].model_num; + if (model_num >= 0) { + polymodel* pm = model_get(model_num); + if (pm != nullptr) { + for (int i = 0; i < pm->n_docks; i++) { + head.add_data(pm->docking_bays[i].name); + } + } + } + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_dockee_point(int parent_node) +{ + int z; + sexp_list_item head; + int sh = -1; + + Assertion(parent_node >= 0, "Invalid parent node"); + Assertion(!stricmp(_model.tree_nodes[parent_node].text, "ai-dock") || !stricmp(_model.tree_nodes[parent_node].text, "set-docked"), "Invalid node type"); + + if (!stricmp(_model.tree_nodes[parent_node].text, "ai-dock")) + { + z = _model.tree_nodes[parent_node].child; + if (z < 0) + return nullptr; + + sh = ship_name_lookup(_model.tree_nodes[z].text, 1); + } + else if (!stricmp(_model.tree_nodes[parent_node].text, "set-docked")) + { + //Dockee ship should be the third child node + z = _model.tree_nodes[parent_node].child; // 1 + if (z < 0) return nullptr; + z = _model.tree_nodes[z].next; // 2 + if (z < 0) return nullptr; + z = _model.tree_nodes[z].next; // 3 + if (z < 0) return nullptr; + + sh = ship_name_lookup(_model.tree_nodes[z].text, 1); + } + + if (sh >= 0) + { + auto model_num = Ship_info[Ships[sh].ship_info_index].model_num; + if (model_num >= 0) { + polymodel* pm = model_get(model_num); + if (pm != nullptr) { + for (int i = 0; i < pm->n_docks; i++) { + head.add_data(pm->docking_bays[i].name); + } + } + } + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_persona() +{ + sexp_list_item head; + + for (const auto &persona: Personas) { + if (persona.flags & PERSONA_FLAG_WINGMAN) { + head.add_data(persona.name); + } + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_font() +{ + int i; + sexp_list_item head; + + for (i = 0; i < font::FontManager::numberOfFonts(); i++) { + head.add_data(font::FontManager::getFont(i)->getName().c_str()); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_who_from() +{ + object *ptr; + sexp_list_item head; + + //head.add_data(""); + head.add_data("#Command"); + head.add_data(""); + head.add_data(""); + + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if ((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) + if (Ship_info[Ships[ptr->instance].ship_info_index].is_flyable()) + head.add_data(Ships[ptr->instance].ship_name); + + ptr = GET_NEXT(ptr); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_priority() +{ + sexp_list_item head; + + head.add_data("High"); + head.add_data("Normal"); + head.add_data("Low"); + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_sound_environment() +{ + sexp_list_item head; + + head.add_data(SEXP_NONE_STRING); + for (const auto& efx_preset : EFX_presets) { + head.add_data(efx_preset.name.c_str()); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_sound_environment_option() +{ + sexp_list_item head; + + for (int i = 0; i < Num_sound_environment_options; i++) + head.add_data(Sound_environment_option[i]); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_adjust_audio_volume() +{ + sexp_list_item head; + + for (int i = 0; i < Num_adjust_audio_options; i++) + head.add_data(Adjust_audio_options[i]); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_builtin_hud_gauge() +{ + sexp_list_item head; + + for (int i = 0; i < Num_hud_gauge_types; i++) + head.add_data(Hud_gauge_types[i].name); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_custom_hud_gauge() +{ + sexp_list_item head; + // prevent duplicate names, comparing case-insensitively + SCP_unordered_set all_gauges; + + for (auto &gauge : default_hud_gauges) + { + SCP_string name = gauge->getCustomGaugeName(); + if (!name.empty() && all_gauges.count(name) == 0) + { + head.add_data(name.c_str()); + all_gauges.insert(std::move(name)); + } + } + + for (auto &si : Ship_info) + { + for (auto &gauge : si.hud_gauges) + { + SCP_string name = gauge->getCustomGaugeName(); + if (!name.empty() && all_gauges.count(name) == 0) + { + head.add_data(name.c_str()); + all_gauges.insert(std::move(name)); + } + } + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_any_hud_gauge() +{ + sexp_list_item head; + + head.add_list(get_listing_opf_builtin_hud_gauge()); + head.add_list(get_listing_opf_custom_hud_gauge()); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ship_effect() +{ + sexp_list_item head; + + for (const auto& sp_effect : Ship_effects) { + head.add_data(sp_effect.name); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_explosion_option() +{ + sexp_list_item head; + + for (int i = 0; i < Num_explosion_options; i++) + head.add_data(Explosion_option[i]); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_waypoint_path() +{ + sexp_list_item head; + + for (const auto &ii: Waypoint_lists) + head.add_data(ii.get_name()); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ship_point() +{ + sexp_list_item head; + + head.add_list(get_listing_opf_ship()); + head.add_list(get_listing_opf_point()); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ship_wing_wholeteam() +{ + int i; + sexp_list_item head; + + for (i = 0; i < static_cast(Iff_info.size()); i++) + head.add_data(Iff_info[i].iff_name); + + head.add_list(get_listing_opf_ship_wing()); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ship_wing_shiponteam_point() +{ + int i; + sexp_list_item head; + + for (i = 0; i < static_cast(Iff_info.size()); i++) + { + char tmp[NAME_LENGTH + 7]; + sprintf(tmp, "", Iff_info[i].iff_name); + strlwr(tmp); + head.add_data(tmp); + } + + head.add_list(get_listing_opf_ship_wing_point()); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ship_wing_point() +{ + sexp_list_item head; + + head.add_list(get_listing_opf_ship()); + head.add_list(get_listing_opf_wing()); + head.add_list(get_listing_opf_point()); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ship_wing_point_or_none() +{ + sexp_list_item head; + + head.add_data(SEXP_NONE_STRING); + head.add_list(get_listing_opf_ship_wing_point()); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_mission_name() const +{ + sexp_list_item head; + + for (auto& mission_name : _model._interface->getMissionNames()) { + head.add_data(mission_name.c_str()); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_goal_name(int parent_node) +{ + sexp_list_item head; + + Assertion(parent_node >= 0, "Invalid parent node"); + int child = _model.tree_nodes[parent_node].child; + + // reference_name is used by campaign editor to filter goals for a specific mission + SCP_string reference_name = (child >= 0) ? _model.tree_nodes[child].text : ""; + + for (auto& entry : _model._interface->getMissionGoals(reference_name)) { + head.add_data(entry.c_str()); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ship_wing() +{ + sexp_list_item head; + + head.add_list(get_listing_opf_ship()); + head.add_list(get_listing_opf_wing()); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ship_prop() +{ + sexp_list_item head; + + head.add_list(get_listing_opf_ship()); + head.add_list(get_listing_opf_prop()); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_order_recipient() +{ + sexp_list_item head; + + head.add_data(""); + + head.add_list(get_listing_opf_ship()); + head.add_list(get_listing_opf_wing()); + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ship_type() +{ + unsigned int i; + sexp_list_item head; + + for (i=0; i= 0) && !Control_config[i].disabled) { + head.add_data(textify_scancode_universal(btn)); + } + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_event_name(int parent_node) +{ + sexp_list_item head; + + Assertion(parent_node >= 0, "Invalid parent node"); + int child = _model.tree_nodes[parent_node].child; + + // reference_name is used by campaign editor to filter events for a specific mission + SCP_string reference_name = (child >= 0) ? _model.tree_nodes[child].text : ""; + + for (auto& entry : _model._interface->getMissionEvents(reference_name)) { + head.add_data(entry.c_str()); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ai_order() +{ + sexp_list_item head; + + for (const auto& order : Player_orders) + head.add_data(order.hud_name.c_str()); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_skill_level() +{ + int i; + sexp_list_item head; + + for (i=0; i(Medals.size()); i++) + { + // don't add Rank or the Ace badges + if ((i == Rank_medal_index) || (Medals[i].kills_needed > 0)) + continue; + head.add_data(Medals[i].name); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_weapon_name() +{ + sexp_list_item head; + + for (auto &wi : Weapon_info) + head.add_data(wi.name); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_intel_name() +{ + sexp_list_item head; + + for (auto &ii : Intel_info) + head.add_data(ii.name); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ship_class_name() +{ + sexp_list_item head; + + for (auto &si : Ship_info) + head.add_data(si.name); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_prop_class_name() +{ + sexp_list_item head; + + for (auto& pi : Prop_info) + head.add_data(pi.name.c_str()); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_huge_weapon() +{ + sexp_list_item head; + + for (auto &wi : Weapon_info) { + if (wi.wi_flags[Weapon::Info_Flags::Huge]) + head.add_data(wi.name); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ship_not_player() +{ + object *ptr; + sexp_list_item head; + + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (ptr->type == OBJ_SHIP) + head.add_data(Ships[ptr->instance].ship_name); + + ptr = GET_NEXT(ptr); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ship_or_none() +{ + sexp_list_item head; + + head.add_data(SEXP_NONE_STRING); + head.add_list(get_listing_opf_ship()); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_subsystem_or_none(int parent_node, int arg_index) +{ + sexp_list_item head; + + head.add_data(SEXP_NONE_STRING); + head.add_list(get_listing_opf_subsystem(parent_node, arg_index)); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_subsys_or_generic(int parent_node, int arg_index) +{ + sexp_list_item head; + char buffer[NAME_LENGTH]; + + for (int i = 0; i < SUBSYSTEM_MAX; ++i) + { + // it's not clear what the "activator" subsystem was intended to do, so let's not display it by default + if (i != SUBSYSTEM_NONE && i != SUBSYSTEM_UNKNOWN && i != SUBSYSTEM_ACTIVATION) + { + sprintf(buffer, SEXP_ALL_GENERIC_SUBSYSTEM_STRING, Subsystem_types[i]); + SCP_tolower(buffer); + head.add_data(buffer); + } + } + head.add_data(SEXP_ALL_SUBSYSTEMS_STRING); + head.add_list(get_listing_opf_subsystem(parent_node, arg_index)); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_jump_nodes() +{ + sexp_list_item head; + + SCP_list::iterator jnp; + for (jnp = Jump_nodes.begin(); jnp != Jump_nodes.end(); ++jnp) { + head.add_data( jnp->GetName()); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_variable_names() +{ + int i; + sexp_list_item head; + + for (i=0; i ppe_names; + gr_get_post_process_effect_names(ppe_names); + for (i=0; i < ppe_names.size(); i++) { + head.add_data(ppe_names[i].c_str()); + } + head.add_data("lightshafts"); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_turret_target_priorities() +{ + size_t t; + sexp_list_item head; + + for(t = 0; t < Ai_tp_list.size(); t++) { + head.add_data(Ai_tp_list[t].name); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_armor_type() +{ + size_t t; + sexp_list_item head; + head.add_data(SEXP_NONE_STRING); + for (t=0; t +static void add_flag_name_helper(M& flag_name_map, sexp_list_item& head, T flag_name_array[], PTM T::* member, size_t flag_name_count) +{ + for (size_t i = 0; i < flag_name_count; i++) + { + auto name = flag_name_array[i].*member; + if (flag_name_map.count(name) == 0) + { + head.add_data(name); + flag_name_map.insert(name); + } + } +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_ship_flags() +{ + sexp_list_item head; + // prevent duplicate names, comparing case-insensitively + SCP_unordered_set all_flags; + + add_flag_name_helper(all_flags, head, Object_flag_names, &obj_flag_name::flag_name, (size_t)Num_object_flag_names); + add_flag_name_helper(all_flags, head, Ship_flag_names, &ship_flag_name::flag_name, Num_ship_flag_names); + add_flag_name_helper(all_flags, head, Parse_object_flags, &flag_def_list_new::name, Num_parse_object_flags); + add_flag_name_helper(all_flags, head, Ai_flag_names, &ai_flag_name::flag_name, (size_t)Num_ai_flag_names); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_wing_flags() +{ + size_t i; + sexp_list_item head; + // wing flags + for ( i = 0; i < Num_wing_flag_names; i++) { + head.add_data(Wing_flag_names[i].flag_name); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_team_colors() +{ + sexp_list_item head; + head.add_data("None"); // Deliberately not SEXP_NONE_STRING + for (const auto& tc: Team_Colors) { + head.add_data(tc.first.c_str()); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_nebula_patterns() +{ + sexp_list_item head; + + head.add_data(SEXP_NONE_STRING); + + for (const auto& neb2_bitmap_filename : Neb2_bitmap_filenames) { + head.add_data(neb2_bitmap_filename.c_str()); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_asteroid_types() +{ + sexp_list_item head; + + head.add_data(SEXP_NONE_STRING); + + auto list = get_list_valid_asteroid_subtypes(); + + for (const auto& this_asteroid : list) { + head.add_data(this_asteroid.c_str()); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_debris_types() +{ + sexp_list_item head; + + head.add_data(SEXP_NONE_STRING); + + for (const auto& this_asteroid : Asteroid_info) { + if (this_asteroid.type == ASTEROID_TYPE_DEBRIS) { + head.add_data(this_asteroid.name); + } + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_motion_debris() +{ + sexp_list_item head; + + head.add_data(SEXP_NONE_STRING); + + for (const auto& motion_debris_info : Motion_debris_info) { + head.add_data(motion_debris_info.name.c_str()); + } + + return head.next; +} + +extern SCP_vector Snds; + +sexp_list_item *SexpTreeOPF::get_listing_opf_game_snds() +{ + sexp_list_item head; + + head.add_data(SEXP_NONE_STRING); + + for (const auto& snd : Snds) { + if (!can_construe_as_integer(snd.name.c_str())) { + head.add_data(snd.name.c_str()); + } + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_fireball() +{ + sexp_list_item head; + + for (const auto &fi: Fireball_info) + { + auto unique_id = fi.unique_id; + + if (strlen(unique_id) > 0) + head.add_data(unique_id); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_species() // NOLINT +{ + sexp_list_item head; + + for (auto &species : Species_info) + head.add_data(species.species_name); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_language() // NOLINT +{ + sexp_list_item head; + + for (auto &lang: Lcl_languages) + head.add_data(lang.lang_name); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_functional_when_eval_type() // NOLINT +{ + sexp_list_item head; + + for (int i = 0; i < Num_functional_when_eval_types; i++) + head.add_data(Functional_when_eval_type[i]); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_animation_name(int parent_node) +{ + int op, child, sh; + sexp_list_item head; + + Assertion(parent_node >= 0, "Invalid parent node"); + + // get the operator type of the node + op = get_operator_const(_model.tree_nodes[parent_node].text); + + // first child node + child = _model.tree_nodes[parent_node].child; + if (child < 0) + return nullptr; + sh = ship_name_lookup(_model.tree_nodes[child].text, 1); + if (sh < 0) { + return nullptr; + } + + switch(op) { + case OP_TRIGGER_ANIMATION_NEW: + case OP_STOP_LOOPING_ANIMATION: { + child = _model.tree_nodes[child].next; + if (child < 0) { + return head.next; + } + auto triggerType = animation::anim_match_type(_model.tree_nodes[child].text); + + for (const auto& anim_ref : Ship_info[Ships[sh].ship_info_index].animations.getRegisteredTriggers()) { + if (anim_ref.type != triggerType) + continue; + + if (anim_ref.subtype != animation::ModelAnimationSet::SUBTYPE_DEFAULT) { + int animationSubtype = anim_ref.subtype; + + if (anim_ref.type == animation::ModelAnimationTriggerType::DockBayDoor) { + //Because of the old system, this is this weird exception. Don't explicitly suggest the NOT doors, as they cannot be explicitly targeted anyways + if (anim_ref.subtype < 0) + continue; + + animationSubtype--; + } + + head.add_data(std::to_string(animationSubtype).c_str()); + } + else + head.add_data(anim_ref.name.c_str()); + } + + break; + } + + case OP_UPDATE_MOVEABLE: + for(const auto& moveable : Ship_info[Ships[sh].ship_info_index].animations.getRegisteredMoveables()) + head.add_data(moveable.c_str()); + + break; + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_sexp_containers(ContainerType con_type) +{ + sexp_list_item head; + + for (const auto &container : get_all_sexp_containers()) { + if (any(container.type & con_type)) { + head.add_data(container.container_name.c_str(), (SEXPT_CONTAINER_NAME | SEXPT_STRING | SEXPT_VALID)); + } + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_wing_formation() // NOLINT +{ + sexp_list_item head; + + head.add_data("Default"); + for (const auto &formation : Wing_formations) + head.add_data(formation.name); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_bolt_types() +{ + sexp_list_item head; + + head.add_data(SEXP_NONE_STRING); + + for (const auto& b_type : Bolt_types) { + head.add_data(b_type.name); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_traitor_overrides() +{ + sexp_list_item head; + + head.add_data(SEXP_NONE_STRING); + + for (const auto& traitor_override : Traitor_overrides) { + head.add_data(traitor_override.name.c_str()); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_lua_general_orders() +{ + sexp_list_item head; + + SCP_vector orders = ai_lua_get_general_orders(); + + for (const auto& val : orders) { + head.add_data(val.c_str()); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_message_types() +{ + sexp_list_item head; + + for (const auto& val : Builtin_messages) { + head.add_data(val.name); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_mission_custom_strings() +{ + sexp_list_item head; + + for (const auto& val : The_mission.custom_strings) { + head.add_data(val.name.c_str()); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_lua_enum(int parent_node, int arg_index) +{ + // first child node + int child = _model.tree_nodes[parent_node].child; + if (child < 0) + return nullptr; + + int this_index = get_dynamic_parameter_index(_model.tree_nodes[parent_node].text, arg_index); + + if (this_index >= 0) { + for (int count = 0; count < this_index; count++) { + child = _model.tree_nodes[child].next; + } + } else { + error_display(1, + "Expected to find an enum parent parameter for node %i in operator %s but found nothing!", + arg_index, + _model.tree_nodes[parent_node].text); + return nullptr; + } + + // Append the suffix if it exists + SCP_string enum_name = _model.tree_nodes[child].text + get_child_enum_suffix(_model.tree_nodes[parent_node].text, arg_index); + + sexp_list_item head; + + int item = get_dynamic_enum_position(enum_name); + + if (item >= 0 && item < static_cast(Dynamic_enums.size())) { + + for (const SCP_string& enum_item : Dynamic_enums[item].list) { + head.add_data(enum_item.c_str()); + } + } else { + // else if enum is invalid do this + mprintf(("Could not find Lua Enum %s! Using instead!", enum_name.c_str())); + head.add_data(""); + } + return head.next; +} + +// specific types of subsystems we're looking for +enum : int { + OPS_CAP_CARGO = 1, + OPS_STRENGTH = 2, + OPS_BEAM_TURRET = 3, + OPS_AWACS = 4, + OPS_ROTATE = 5, + OPS_TRANSLATE = 6, + OPS_ARMOR = 7, +}; + +sexp_list_item *SexpTreeOPF::get_listing_opf_subsystem(int parent_node, int arg_index) +{ + int op, child, sh; + int special_subsys = 0; + sexp_list_item head; + ship_subsys *subsys; + + // determine if the parent is one of the set subsystem strength items. If so, + // we want to append the "Hull" name onto the end of the menu + Assertion(parent_node >= 0, "Invalid parent node"); + + // get the operator type of the node + op = get_operator_const(_model.tree_nodes[parent_node].text); + + // first child node + child = _model.tree_nodes[parent_node].child; + if (child < 0) + return nullptr; + + switch(op) + { + // where we care about hull strength + case OP_REPAIR_SUBSYSTEM: + case OP_SABOTAGE_SUBSYSTEM: + case OP_SET_SUBSYSTEM_STRNGTH: + special_subsys = OPS_STRENGTH; + break; + + // Armor types need Hull and Shields but not Simulated Hull + case OP_SET_ARMOR_TYPE: + case OP_HAS_ARMOR_TYPE: + special_subsys = OPS_ARMOR; + break; + + // awacs subsystems + case OP_AWACS_SET_RADIUS: + special_subsys = OPS_AWACS; + break; + + // rotating + case OP_LOCK_ROTATING_SUBSYSTEM: + case OP_FREE_ROTATING_SUBSYSTEM: + case OP_REVERSE_ROTATING_SUBSYSTEM: + case OP_ROTATING_SUBSYS_SET_TURN_TIME: + special_subsys = OPS_ROTATE; + break; + + // translating + case OP_LOCK_TRANSLATING_SUBSYSTEM: + case OP_FREE_TRANSLATING_SUBSYSTEM: + case OP_REVERSE_TRANSLATING_SUBSYSTEM: + case OP_TRANSLATING_SUBSYS_SET_SPEED: + special_subsys = OPS_TRANSLATE; + break; + + // where we care about capital ship subsystem cargo + case OP_CAP_SUBSYS_CARGO_KNOWN_DELAY: + special_subsys = OPS_CAP_CARGO; + + // get the next sibling + child = _model.tree_nodes[child].next; + break; + + // where we care about turrets carrying beam weapons + case OP_BEAM_FIRE: + special_subsys = OPS_BEAM_TURRET; + + // if this is arg index 3 (targeted ship) + if(arg_index == 3) + { + special_subsys = OPS_STRENGTH; + + // iterate to the next field two times + child = _model.tree_nodes[child].next; + if (child < 0) return nullptr; + child = _model.tree_nodes[child].next; + } + else + { + Assertion(arg_index == 1, "Invalid argument index"); + } + break; + + case OP_BEAM_FIRE_COORDS: + special_subsys = OPS_BEAM_TURRET; + break; + + // these sexps check the subsystem of the *second entry* on the list, not the first + case OP_DISTANCE_CENTER_SUBSYSTEM: + case OP_DISTANCE_BBOX_SUBSYSTEM: + case OP_SET_CARGO: + case OP_IS_CARGO: + case OP_CHANGE_AI_CLASS: + case OP_IS_AI_CLASS: + case OP_MISSILE_LOCKED: + case OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD: + case OP_IS_IN_TURRET_FOV: + case OP_TURRET_SET_FORCED_TARGET: + // iterate to the next field + child = _model.tree_nodes[child].next; + break; + + // this sexp checks the subsystem of the *fourth entry* on the list + case OP_QUERY_ORDERS: + // iterate to the next field three times + child = _model.tree_nodes[child].next; + if (child < 0) return nullptr; + child = _model.tree_nodes[child].next; + if (child < 0) return nullptr; + child = _model.tree_nodes[child].next; + break; + + // this sexp checks the subsystem of the *seventh entry* on the list + case OP_BEAM_FLOATING_FIRE: + // iterate to the next field six times + child = _model.tree_nodes[child].next; + if (child < 0) return nullptr; + child = _model.tree_nodes[child].next; + if (child < 0) return nullptr; + child = _model.tree_nodes[child].next; + if (child < 0) return nullptr; + child = _model.tree_nodes[child].next; + if (child < 0) return nullptr; + child = _model.tree_nodes[child].next; + if (child < 0) return nullptr; + child = _model.tree_nodes[child].next; + break; + + // this sexp checks the subsystem of the *ninth entry* on the list + case OP_WEAPON_CREATE: + // iterate to the next field eight times + child = _model.tree_nodes[child].next; + if (child < 0) return nullptr; + child = _model.tree_nodes[child].next; + if (child < 0) return nullptr; + child = _model.tree_nodes[child].next; + if (child < 0) return nullptr; + child = _model.tree_nodes[child].next; + if (child < 0) return nullptr; + child = _model.tree_nodes[child].next; + if (child < 0) return nullptr; + child = _model.tree_nodes[child].next; + if (child < 0) return nullptr; + child = _model.tree_nodes[child].next; + if (child < 0) return nullptr; + child = _model.tree_nodes[child].next; + break; + + // this sexp checks the third entry, but only for the 4th argument + case OP_TURRET_SET_FORCED_SUBSYS_TARGET: + if (arg_index >= 3) { + child = _model.tree_nodes[child].next; + if (child < 0) return nullptr; + child = _model.tree_nodes[child].next; + } + break; + + default: + if (op < First_available_operator_id) { + break; + } else { + int this_index = get_dynamic_parameter_index(_model.tree_nodes[parent_node].text, arg_index); + + if (this_index >= 0) { + for (int count = 0; count < this_index; count++) { + child = _model.tree_nodes[child].next; + } + } else { + error_display(1, "Expected to find a dynamic lua parent parameter for node %i in operator %s but found nothing!", + arg_index, + _model.tree_nodes[parent_node].text); + } + } + } + + if (child < 0) + return nullptr; + + + // if one of the subsystem strength operators, append the Hull string and the Simulated Hull string + if (special_subsys == OPS_STRENGTH) { + head.add_data(SEXP_HULL_STRING); + head.add_data(SEXP_SIM_HULL_STRING); + } + + // if setting armor type we only need Hull and Shields + if (special_subsys == OPS_ARMOR) { + head.add_data(SEXP_HULL_STRING); + head.add_data(SEXP_SHIELD_STRING); + } + + + // now find the ship and add all relevant subsystems + sh = ship_name_lookup(_model.tree_nodes[child].text, 1); + if (sh >= 0) { + subsys = GET_FIRST(&Ships[sh].subsys_list); + while (subsys != END_OF_LIST(&Ships[sh].subsys_list)) { + // add stuff + switch(special_subsys){ + // subsystem cargo + case OPS_CAP_CARGO: + head.add_data(subsys->system_info->subobj_name); + break; + + // beam fire + case OPS_BEAM_TURRET: + head.add_data(subsys->system_info->subobj_name); + break; + + // awacs level + case OPS_AWACS: + if (subsys->system_info->flags[Model::Subsystem_Flags::Awacs]) { + head.add_data(subsys->system_info->subobj_name); + } + break; + + // rotating + case OPS_ROTATE: + if (subsys->system_info->flags[Model::Subsystem_Flags::Rotates]) { + head.add_data(subsys->system_info->subobj_name); + } + break; + + // translating + case OPS_TRANSLATE: + if (subsys->system_info->flags[Model::Subsystem_Flags::Translates]) { + head.add_data(subsys->system_info->subobj_name); + } + break; + + // everything else + default: + head.add_data(subsys->system_info->subobj_name); + break; + } + + // next subsystem + subsys = GET_NEXT(subsys); + } + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_listing_opf_subsystem_type(int parent_node) +{ + int i, child, shipnum, num_added = 0; + sexp_list_item head; + ship_subsys *subsys; + + // first child node + child = _model.tree_nodes[parent_node].child; + if (child < 0) + return nullptr; + + // now find the ship + shipnum = ship_name_lookup(_model.tree_nodes[child].text, 1); + if (shipnum < 0) { + return head.next; + } + + // add all relevant subsystem types + for (i = 0; i < SUBSYSTEM_MAX; i++) { + // don't allow these two + if (i == SUBSYSTEM_NONE || i == SUBSYSTEM_UNKNOWN) + continue; + + // loop through all ship subsystems + subsys = GET_FIRST(&Ships[shipnum].subsys_list); + while (subsys != END_OF_LIST(&Ships[shipnum].subsys_list)) { + // check if this subsystem is of this type + if (i == subsys->system_info->type) { + // subsystem type is applicable, so add it + head.add_data(Subsystem_types[i]); + num_added++; + break; + } + + // next subsystem + subsys = GET_NEXT(subsys); + } + } + + // if no subsystem types, go ahead and add NONE (even though it won't be checked) + if (num_added == 0) { + head.add_data(Subsystem_types[SUBSYSTEM_NONE]); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_container_modifiers(int con_data_node) const +{ + Assertion(con_data_node != -1, "Attempt to get modifiers for invalid container node. Please report!"); + Assertion(_model.tree_nodes[con_data_node].type & SEXPT_CONTAINER_DATA, + "Attempt to get modifiers for non-container data node %s. Please report!", + _model.tree_nodes[con_data_node].text); + + const auto *p_container = get_sexp_container(_model.tree_nodes[con_data_node].text); + Assertion(p_container, + "Attempt to get modifiers for unknown container %s. Please report!", + _model.tree_nodes[con_data_node].text); + const auto &container = *p_container; + + sexp_list_item head; + sexp_list_item *list = nullptr; + + if (container.is_list()) { + list = get_list_container_modifiers(); + } else if (container.is_map()) { + // start the list with "" if relevant + if (_model.is_node_eligible_for_special_argument(con_data_node) && + any(container.type & ContainerType::STRING_KEYS)) { + head.add_data(SEXP_ARGUMENT_STRING, (SEXPT_VALID | SEXPT_STRING | SEXPT_MODIFIER)); + } + + list = get_map_container_modifiers(con_data_node); + } else { + UNREACHABLE("Unknown container type %d", static_cast(p_container->type)); + } + + if (list) { + head.add_list(list); + } + + return head.next; +} + +sexp_list_item *SexpTreeOPF::get_list_container_modifiers() +{ + sexp_list_item head; + + for (const auto &modifier : get_all_list_modifiers()) { + head.add_data(modifier.name, SEXPT_VALID | SEXPT_MODIFIER | SEXPT_STRING); + } + + return head.next; +} + +// FIXME TODO: if you use this function with remove-from-map SEXP, don't use SEXPT_MODIFIER +sexp_list_item *SexpTreeOPF::get_map_container_modifiers(int con_data_node) const +{ + sexp_list_item head; + + Assertion(_model.tree_nodes[con_data_node].type & SEXPT_CONTAINER_DATA, + "Found map modifier for non-container data node %s. Please report!", + _model.tree_nodes[con_data_node].text); + + const auto *p_container = get_sexp_container(_model.tree_nodes[con_data_node].text); + Assertion(p_container != nullptr, + "Found map modifier for unknown container %s. Please report!", + _model.tree_nodes[con_data_node].text); + + const auto &container = *p_container; + Assertion(container.is_map(), + "Found map modifier for non-map container %s with type %d. Please report!", + _model.tree_nodes[con_data_node].text, + static_cast(container.type)); + + int type = SEXPT_VALID | SEXPT_MODIFIER; + if (any(container.type & ContainerType::STRING_KEYS)) { + type |= SEXPT_STRING; + } else if (any(container.type & ContainerType::NUMBER_KEYS)) { + type |= SEXPT_NUMBER; + } else { + UNREACHABLE("Unknown map container key type %d", static_cast(container.type)); + } + + for (const auto &kv_pair : container.map_data) { + head.add_data(kv_pair.first.c_str(), type); + } + + return head.next; +} + +// get potential options for container multidimensional modifiers +// the value could be either string or number, checked in-mission +sexp_list_item *SexpTreeOPF::get_container_multidim_modifiers(int con_data_node) const +{ + Assertion(con_data_node != -1, + "Attempt to get multidimensional modifiers for invalid container node. Please report!"); + Assertion(_model.tree_nodes[con_data_node].type & SEXPT_CONTAINER_DATA, + "Attempt to get multidimensional modifiers for non-container data node %s. Please report!", + _model.tree_nodes[con_data_node].text); + + sexp_list_item head; + + if (_model.is_node_eligible_for_special_argument(con_data_node)) { + head.add_data(SEXP_ARGUMENT_STRING, (SEXPT_VALID | SEXPT_STRING | SEXPT_MODIFIER)); + } + + // the FREDder might want to use a list modifier + sexp_list_item *list = get_list_container_modifiers(); + + head.add_list(list); + + return head.next; +} + +sexp_list_item *SexpTreeOPF::check_for_dynamic_sexp_enum(int opf) +{ + sexp_list_item head; + + int item = opf - First_available_opf_id; + + if (item < static_cast(Dynamic_enums.size())) { + + for (const SCP_string& enum_item : Dynamic_enums[item].list) { + head.add_data(enum_item.c_str()); + } + return head.next; + } else { + // else if opf is invalid do this + UNREACHABLE("Unhandled SEXP argument type!"); // unknown OPF code + return nullptr; + } +} + +bool SexpTreeOPF::is_container_name_opf_type(const int op_type) +{ + return (op_type == OPF_CONTAINER_NAME) || + (op_type == OPF_LIST_CONTAINER_NAME) || + (op_type == OPF_MAP_CONTAINER_NAME); +} + +// ----------------------------------------------------------------------- +// Main OPF dispatch function +// ----------------------------------------------------------------------- + +sexp_list_item *SexpTreeOPF::get_listing_opf(int opf, int parent_node, int arg_index) +{ + sexp_list_item head; + sexp_list_item *list = nullptr; + + switch (opf) { + case OPF_NONE: + list = nullptr; + break; + + case OPF_NULL: + list = get_listing_opf_null(); + break; + + case OPF_BOOL: + list = get_listing_opf_bool(parent_node); + break; + + case OPF_NUMBER: + list = get_listing_opf_number(); + break; + + case OPF_SHIP: + list = get_listing_opf_ship(parent_node); + break; + + case OPF_PROP: + list = get_listing_opf_prop(); + break; + + case OPF_WING: + list = get_listing_opf_wing(); + break; + + case OPF_AWACS_SUBSYSTEM: + case OPF_ROTATING_SUBSYSTEM: + case OPF_TRANSLATING_SUBSYSTEM: + case OPF_SUBSYSTEM: + list = get_listing_opf_subsystem(parent_node, arg_index); + break; + + case OPF_SUBSYSTEM_TYPE: + list = get_listing_opf_subsystem_type(parent_node); + break; + + case OPF_POINT: + list = get_listing_opf_point(); + break; + + case OPF_IFF: + list = get_listing_opf_iff(); + break; + + case OPF_AI_CLASS: + list = get_listing_opf_ai_class(); + break; + + case OPF_SUPPORT_SHIP_CLASS: + list = get_listing_opf_support_ship_class(); + break; + + case OPF_SSM_CLASS: + list = get_listing_opf_ssm_class(); + break; + + case OPF_ARRIVAL_LOCATION: + list = get_listing_opf_arrival_location(); + break; + + case OPF_DEPARTURE_LOCATION: + list = get_listing_opf_departure_location(); + break; + + case OPF_ARRIVAL_ANCHOR_ALL: + list = get_listing_opf_arrival_anchor_all(); + break; + + case OPF_SHIP_WITH_BAY: + list = get_listing_opf_ship_with_bay(); + break; + + case OPF_SOUNDTRACK_NAME: + list = get_listing_opf_soundtrack_name(); + break; + + case OPF_AI_GOAL: + list = get_listing_opf_ai_goal(parent_node); + break; + + case OPF_FLEXIBLE_ARGUMENT: + list = get_listing_opf_flexible_argument(); + break; + + case OPF_DOCKER_POINT: + list = get_listing_opf_docker_point(parent_node, arg_index); + break; + + case OPF_DOCKEE_POINT: + list = get_listing_opf_dockee_point(parent_node); + break; + + case OPF_MESSAGE: + list = get_listing_opf_message(); + break; + + case OPF_WHO_FROM: + list = get_listing_opf_who_from(); + break; + + case OPF_PRIORITY: + list = get_listing_opf_priority(); + break; + + case OPF_WAYPOINT_PATH: + list = get_listing_opf_waypoint_path(); + break; + + case OPF_POSITIVE: + list = get_listing_opf_positive(); + break; + + case OPF_MISSION_NAME: + list = get_listing_opf_mission_name(); + break; + + case OPF_SHIP_POINT: + list = get_listing_opf_ship_point(); + break; + + case OPF_GOAL_NAME: + list = get_listing_opf_goal_name(parent_node); + break; + + case OPF_SHIP_WING: + list = get_listing_opf_ship_wing(); + break; + + case OPF_SHIP_PROP: + list = get_listing_opf_ship_prop(); + break; + + case OPF_SHIP_WING_WHOLETEAM: + list = get_listing_opf_ship_wing_wholeteam(); + break; + + case OPF_SHIP_WING_SHIPONTEAM_POINT: + list = get_listing_opf_ship_wing_shiponteam_point(); + break; + + case OPF_SHIP_WING_POINT: + list = get_listing_opf_ship_wing_point(); + break; + + case OPF_SHIP_WING_POINT_OR_NONE: + list = get_listing_opf_ship_wing_point_or_none(); + break; + + case OPF_ORDER_RECIPIENT: + list = get_listing_opf_order_recipient(); + break; + + case OPF_SHIP_TYPE: + list = get_listing_opf_ship_type(); + break; + + case OPF_KEYPRESS: + list = get_listing_opf_keypress(); + break; + + case OPF_EVENT_NAME: + list = get_listing_opf_event_name(parent_node); + break; + + case OPF_AI_ORDER: + list = get_listing_opf_ai_order(); + break; + + case OPF_SKILL_LEVEL: + list = get_listing_opf_skill_level(); + break; + + case OPF_CARGO: + list = get_listing_opf_cargo(); + break; + + case OPF_STRING: + list = get_listing_opf_string(); + break; + + case OPF_MEDAL_NAME: + list = get_listing_opf_medal_name(); + break; + + case OPF_WEAPON_NAME: + list = get_listing_opf_weapon_name(); + break; + + case OPF_INTEL_NAME: + list = get_listing_opf_intel_name(); + break; + + case OPF_SHIP_CLASS_NAME: + list = get_listing_opf_ship_class_name(); + break; + + case OPF_PROP_CLASS_NAME: + list = get_listing_opf_prop_class_name(); + break; + + case OPF_HUGE_WEAPON: + list = get_listing_opf_huge_weapon(); + break; + + case OPF_SHIP_NOT_PLAYER: + list = get_listing_opf_ship_not_player(); + break; + + case OPF_SHIP_OR_NONE: + list = get_listing_opf_ship_or_none(); + break; + + case OPF_SUBSYSTEM_OR_NONE: + list = get_listing_opf_subsystem_or_none(parent_node, arg_index); + break; + + case OPF_SUBSYS_OR_GENERIC: + list = get_listing_opf_subsys_or_generic(parent_node, arg_index); + break; + + case OPF_JUMP_NODE_NAME: + list = get_listing_opf_jump_nodes(); + break; + + case OPF_VARIABLE_NAME: + list = get_listing_opf_variable_names(); + break; + + case OPF_AMBIGUOUS: + list = nullptr; + break; + + case OPF_ANYTHING: + list = nullptr; + break; + + case OPF_SKYBOX_MODEL_NAME: + list = get_listing_opf_skybox_model(); + break; + + case OPF_SKYBOX_FLAGS: + list = get_listing_opf_skybox_flags(); + break; + + case OPF_BACKGROUND_BITMAP: + list = get_listing_opf_background_bitmap(); + break; + + case OPF_SUN_BITMAP: + list = get_listing_opf_sun_bitmap(); + break; + + case OPF_NEBULA_STORM_TYPE: + list = get_listing_opf_nebula_storm_type(); + break; + + case OPF_NEBULA_POOF: + list = get_listing_opf_nebula_poof(); + break; + + case OPF_TURRET_TARGET_ORDER: + list = get_listing_opf_turret_target_order(); + break; + + case OPF_TURRET_TYPE: + list = get_listing_opf_turret_types(); + break; + + case OPF_TARGET_PRIORITIES: + list = get_listing_opf_turret_target_priorities(); + break; + + case OPF_ARMOR_TYPE: + list = get_listing_opf_armor_type(); + break; + + case OPF_DAMAGE_TYPE: + list = get_listing_opf_damage_type(); + break; + + case OPF_ANIMATION_TYPE: + list = get_listing_opf_animation_type(); + break; + + case OPF_PERSONA: + list = get_listing_opf_persona(); + break; + + case OPF_POST_EFFECT: + list = get_listing_opf_post_effect(); + break; + + case OPF_FONT: + list = get_listing_opf_font(); + break; + + case OPF_HUD_ELEMENT: + list = get_listing_opf_hud_elements(); + break; + + case OPF_SOUND_ENVIRONMENT: + list = get_listing_opf_sound_environment(); + break; + + case OPF_SOUND_ENVIRONMENT_OPTION: + list = get_listing_opf_sound_environment_option(); + break; + + case OPF_AUDIO_VOLUME_OPTION: + list = get_listing_opf_adjust_audio_volume(); + break; + + case OPF_EXPLOSION_OPTION: + list = get_listing_opf_explosion_option(); + break; + + case OPF_WEAPON_BANK_NUMBER: + list = get_listing_opf_weapon_banks(); + break; + + case OPF_MESSAGE_OR_STRING: + list = get_listing_opf_message(); + break; + + case OPF_BUILTIN_HUD_GAUGE: + list = get_listing_opf_builtin_hud_gauge(); + break; + + case OPF_CUSTOM_HUD_GAUGE: + list = get_listing_opf_custom_hud_gauge(); + break; + + case OPF_ANY_HUD_GAUGE: + list = get_listing_opf_any_hud_gauge(); + break; + + case OPF_SHIP_EFFECT: + list = get_listing_opf_ship_effect(); + break; + + case OPF_MISSION_MOOD: + list = get_listing_opf_mission_moods(); + break; + + case OPF_SHIP_FLAG: + list = get_listing_opf_ship_flags(); + break; + + case OPF_WING_FLAG: + list = get_listing_opf_wing_flags(); + break; + + case OPF_TEAM_COLOR: + list = get_listing_opf_team_colors(); + break; + + case OPF_NEBULA_PATTERN: + list = get_listing_opf_nebula_patterns(); + break; + + case OPF_GAME_SND: + list = get_listing_opf_game_snds(); + break; + + case OPF_FIREBALL: + list = get_listing_opf_fireball(); + break; + + case OPF_SPECIES: + list = get_listing_opf_species(); + break; + + case OPF_LANGUAGE: + list = get_listing_opf_language(); + break; + + case OPF_FUNCTIONAL_WHEN_EVAL_TYPE: + list = get_listing_opf_functional_when_eval_type(); + break; + + case OPF_ANIMATION_NAME: + list = get_listing_opf_animation_name(parent_node); + break; + + case OPF_CONTAINER_NAME: + list = get_listing_opf_sexp_containers(ContainerType::LIST | ContainerType::MAP); + break; + + case OPF_LIST_CONTAINER_NAME: + list = get_listing_opf_sexp_containers(ContainerType::LIST); + break; + + case OPF_MAP_CONTAINER_NAME: + list = get_listing_opf_sexp_containers(ContainerType::MAP); + break; + + case OPF_CONTAINER_VALUE: + list = nullptr; + break; + + case OPF_DATA_OR_STR_CONTAINER: + list = nullptr; + break; + + case OPF_ASTEROID_TYPES: + list = get_listing_opf_asteroid_types(); + break; + + case OPF_DEBRIS_TYPES: + list = get_listing_opf_debris_types(); + break; + + case OPF_WING_FORMATION: + list = get_listing_opf_wing_formation(); + break; + + case OPF_MOTION_DEBRIS: + list = get_listing_opf_motion_debris(); + break; + + case OPF_BOLT_TYPE: + list = get_listing_opf_bolt_types(); + break; + + case OPF_TRAITOR_OVERRIDE: + list = get_listing_opf_traitor_overrides(); + break; + + case OPF_LUA_GENERAL_ORDER: + list = get_listing_opf_lua_general_orders(); + break; + + case OPF_MESSAGE_TYPE: + list = get_listing_opf_message_types(); + break; + + case OPF_CHILD_LUA_ENUM: + list = get_listing_opf_lua_enum(parent_node, arg_index); + break; + + case OPF_MISSION_CUSTOM_STRING: + list = get_listing_opf_mission_custom_strings(); + break; + + default: + //We're at the end of the list so check for any dynamic enums + list = check_for_dynamic_sexp_enum(opf); + break; + } + + + // skip OPF_NONE, also skip for OPF_NULL, because it takes no data (though it can take plenty of operators) + if (opf == OPF_NULL || opf == OPF_NONE) { + return list; + } + + // skip the special argument if we aren't at the right spot in when-argument or + // every-time-argument + if (!_model.is_node_eligible_for_special_argument(parent_node)) { + return list; + } + + // the special item is a string and should not be added for numeric lists + if (opf != OPF_NUMBER && opf != OPF_POSITIVE) { + head.add_data(SEXP_ARGUMENT_STRING); + } + + if (list != nullptr) { + // append other list + head.add_list(list); + } + + // return listing + return head.next; +} + +// ----------------------------------------------------------------------- +// Default argument availability and values +// ----------------------------------------------------------------------- + + +// Returns non-zero if all minimum required arguments of operator 'op' have default values. +// Checks each argument position from 0 to Operators[op].min-1. +int SexpTreeOPF::query_default_argument_available(int op) const +{ + int i; + + Assertion(op >= 0, "Invalid operator index"); + for (i = 0; i < Operators[op].min; i++) + if (!query_default_argument_available(op, i)) + return 0; + + return 1; +} + +// Returns non-zero if argument position 'i' of operator 'op' has a default value available. +// Checks based on the OPF_* type: some types always have defaults, others depend on +// whether ships, wings, goals, events, or other game data currently exist. +int SexpTreeOPF::query_default_argument_available(int op, int i) const +{ + int j, type; + object* ptr; + + type = query_operator_argument_type(op, i); + switch (type) { + case OPF_NONE: + case OPF_NULL: + case OPF_BOOL: + case OPF_NUMBER: + case OPF_POSITIVE: + case OPF_IFF: + case OPF_AI_CLASS: + case OPF_WHO_FROM: + case OPF_PRIORITY: + case OPF_SHIP_TYPE: + case OPF_SUBSYSTEM: + case OPF_AWACS_SUBSYSTEM: + case OPF_ROTATING_SUBSYSTEM: + case OPF_TRANSLATING_SUBSYSTEM: + case OPF_SUBSYSTEM_TYPE: + case OPF_DOCKER_POINT: + case OPF_DOCKEE_POINT: + case OPF_AI_GOAL: + case OPF_KEYPRESS: + case OPF_AI_ORDER: + case OPF_SKILL_LEVEL: + case OPF_MEDAL_NAME: + case OPF_WEAPON_NAME: + case OPF_INTEL_NAME: + case OPF_SHIP_CLASS_NAME: + case OPF_PROP_CLASS_NAME: + case OPF_HUGE_WEAPON: + case OPF_JUMP_NODE_NAME: + case OPF_AMBIGUOUS: + case OPF_CARGO: + case OPF_ARRIVAL_LOCATION: + case OPF_DEPARTURE_LOCATION: + case OPF_ARRIVAL_ANCHOR_ALL: + case OPF_SUPPORT_SHIP_CLASS: + case OPF_SHIP_WITH_BAY: + case OPF_SOUNDTRACK_NAME: + case OPF_STRING: + case OPF_FLEXIBLE_ARGUMENT: + case OPF_ANYTHING: + case OPF_DATA_OR_STR_CONTAINER: + case OPF_SKYBOX_MODEL_NAME: + case OPF_SKYBOX_FLAGS: + case OPF_SHIP_OR_NONE: + case OPF_SUBSYSTEM_OR_NONE: + case OPF_SHIP_WING_POINT_OR_NONE: + case OPF_SUBSYS_OR_GENERIC: + case OPF_BACKGROUND_BITMAP: + case OPF_SUN_BITMAP: + case OPF_NEBULA_STORM_TYPE: + case OPF_NEBULA_POOF: + case OPF_TURRET_TARGET_ORDER: + case OPF_TURRET_TYPE: + case OPF_POST_EFFECT: + case OPF_TARGET_PRIORITIES: + case OPF_ARMOR_TYPE: + case OPF_DAMAGE_TYPE: + case OPF_FONT: + case OPF_HUD_ELEMENT: + case OPF_SOUND_ENVIRONMENT: + case OPF_SOUND_ENVIRONMENT_OPTION: + case OPF_EXPLOSION_OPTION: + case OPF_AUDIO_VOLUME_OPTION: + case OPF_WEAPON_BANK_NUMBER: + case OPF_MESSAGE_OR_STRING: + case OPF_BUILTIN_HUD_GAUGE: + case OPF_CUSTOM_HUD_GAUGE: + case OPF_ANY_HUD_GAUGE: + case OPF_SHIP_EFFECT: + case OPF_ANIMATION_TYPE: + case OPF_SHIP_FLAG: + case OPF_WING_FLAG: + case OPF_NEBULA_PATTERN: + case OPF_NAV_POINT: + case OPF_TEAM_COLOR: + case OPF_GAME_SND: + case OPF_FIREBALL: + case OPF_SPECIES: + case OPF_LANGUAGE: + case OPF_FUNCTIONAL_WHEN_EVAL_TYPE: + case OPF_ANIMATION_NAME: + case OPF_CONTAINER_VALUE: + case OPF_WING_FORMATION: + case OPF_CHILD_LUA_ENUM: + case OPF_MESSAGE_TYPE: + return 1; + + case OPF_SHIP: + case OPF_SHIP_WING: + case OPF_SHIP_POINT: + case OPF_SHIP_WING_POINT: + case OPF_SHIP_WING_WHOLETEAM: + case OPF_SHIP_WING_SHIPONTEAM_POINT: + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (ptr->type == OBJ_SHIP || ptr->type == OBJ_START) + return 1; + + ptr = GET_NEXT(ptr); + } + + return 0; + + case OPF_SHIP_PROP: + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (ptr->type == OBJ_SHIP || ptr->type == OBJ_START || ptr->type == OBJ_PROP) + return 1; + + ptr = GET_NEXT(ptr); + } + + return 0; + + case OPF_PROP: + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (ptr->type == OBJ_PROP) + return 1; + + ptr = GET_NEXT(ptr); + } + return 0; + + case OPF_SHIP_NOT_PLAYER: + case OPF_ORDER_RECIPIENT: + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (ptr->type == OBJ_SHIP) + return 1; + + ptr = GET_NEXT(ptr); + } + + return 0; + + case OPF_WING: + for (j = 0; j < MAX_WINGS; j++) + if (Wings[j].wave_count) + return 1; + + return 0; + + case OPF_PERSONA: + return Personas.empty() ? 0 : 1; + + case OPF_POINT: + case OPF_WAYPOINT_PATH: + return Waypoint_lists.empty() ? 0 : 1; + + case OPF_MISSION_NAME: + if (!_model._interface || !_model._interface->requireCampaignOperators()) { + if (_model._interface && !_model._interface->hasDefaultMissionName()) + return 0; + + return 1; + } + + if (Campaign.num_missions > 0) + return 1; + + return 0; + + case OPF_GOAL_NAME: { + int value; + + value = Operators[op].value; + + if (_model._interface && _model._interface->requireCampaignOperators()) + return 1; + + else if (_model._interface && _model._interface->hasDefaultGoal(value)) + return 1; + + return 0; + } + + case OPF_EVENT_NAME: { + int value; + + value = Operators[op].value; + + if (_model._interface && _model._interface->requireCampaignOperators()) + return 1; + + else if (_model._interface && _model._interface->hasDefaultEvent(value)) + return 1; + + return 0; + } + + case OPF_MESSAGE: + if (_model._interface && _model._interface->hasDefaultMessageParameter()) + return 1; + + return 0; + + case OPF_VARIABLE_NAME: + return (sexp_variable_count() > 0) ? 1 : 0; + + case OPF_SSM_CLASS: + return Ssm_info.empty() ? 0 : 1; + + case OPF_MISSION_MOOD: + return Builtin_moods.empty() ? 0 : 1; + + case OPF_CONTAINER_NAME: + return get_all_sexp_containers().empty() ? 0 : 1; + + case OPF_LIST_CONTAINER_NAME: + for (const auto& container : get_all_sexp_containers()) { + if (container.is_list()) { + return 1; + } + } + return 0; + + case OPF_MAP_CONTAINER_NAME: + for (const auto& container : get_all_sexp_containers()) { + if (container.is_map()) { + return 1; + } + } + return 0; + + case OPF_ASTEROID_TYPES: + if (!get_list_valid_asteroid_subtypes().empty()) { + return 1; + } + return 0; + + case OPF_DEBRIS_TYPES: + for (const auto& this_asteroid : Asteroid_info) { + if (this_asteroid.type == ASTEROID_TYPE_DEBRIS) { + return 1; + } + } + return 0; + + case OPF_MOTION_DEBRIS: + if (!Motion_debris_info.empty()) { + return 1; + } + return 0; + + case OPF_BOLT_TYPE: + if (!Bolt_types.empty()) { + return 1; + } + return 0; + + case OPF_TRAITOR_OVERRIDE: + return Traitor_overrides.empty() ? 0 : 1; + + case OPF_LUA_GENERAL_ORDER: + return (ai_lua_get_num_general_orders() > 0) ? 1 : 0; + + case OPF_MISSION_CUSTOM_STRING: + return The_mission.custom_strings.empty() ? 0 : 1; + + default: + if (!Dynamic_enums.empty()) { + if ((type - First_available_opf_id) < static_cast(Dynamic_enums.size())) { + return 1; + } else { + UNREACHABLE("Unhandled SEXP argument type!"); + } + } else { + UNREACHABLE("Unhandled SEXP argument type!"); + } + } + + return 0; +} + +// Determine and return the default value for operator argument position i. +// Returns 0 on success, -1 if no default available. +int SexpTreeOPF::get_default_value(sexp_list_item* item, char* text_buf, int op, int i) +{ + const char* str = nullptr; + int type, index; + sexp_list_item* list; + + index = _model.item_index; + type = query_operator_argument_type(op, i); + switch (type) + { + case OPF_NULL: + item->set_op(OP_NOP); + return 0; + + case OPF_BOOL: + item->set_op(OP_TRUE); + return 0; + + case OPF_ANYTHING: + if (Operators[op].value == OP_INVALIDATE_ARGUMENT || Operators[op].value == OP_VALIDATE_ARGUMENT) + item->set_data(SEXP_ARGUMENT_STRING); + else + item->set_data(""); + return 0; + + case OPF_DATA_OR_STR_CONTAINER: + item->set_data(""); + return 0; + + case OPF_NUMBER: + case OPF_POSITIVE: + case OPF_AMBIGUOUS: + // if the top level operator is an AI goal, and we are adding the last number required, + // assume that this number is a priority and make it 89 instead of 1. + if ((query_operator_return_type(op) == OPR_AI_GOAL) && (i == (Operators[op].min - 1))) + { + item->set_data("89", (SEXPT_NUMBER | SEXPT_VALID)); + } + else if (((Operators[op].value == OP_HAS_DOCKED_DELAY) || (Operators[op].value == OP_HAS_UNDOCKED_DELAY) || (Operators[op].value == OP_TIME_DOCKED) || (Operators[op].value == OP_TIME_UNDOCKED)) && (i == 2)) + { + item->set_data("1", (SEXPT_NUMBER | SEXPT_VALID)); + } + else if ((Operators[op].value == OP_SHIP_TYPE_DESTROYED) || (Operators[op].value == OP_GOOD_SECONDARY_TIME)) + { + item->set_data("100", (SEXPT_NUMBER | SEXPT_VALID)); + } + else if (Operators[op].value == OP_SET_SUPPORT_SHIP) + { + item->set_data("-1", (SEXPT_NUMBER | SEXPT_VALID)); + } + else if (((Operators[op].value == OP_SHIP_TAG) && (i == 1)) || ((Operators[op].value == OP_TRIGGER_SUBMODEL_ANIMATION) && (i == 3))) + { + item->set_data("1", (SEXPT_NUMBER | SEXPT_VALID)); + } + else if (Operators[op].value == OP_EXPLOSION_EFFECT) + { + int temp; + char sexp_str_token[TOKEN_LENGTH]; + + switch (i) + { + case 3: + temp = 10; + break; + case 4: + temp = 10; + break; + case 5: + temp = 100; + break; + case 6: + temp = 10; + break; + case 7: + temp = 100; + break; + case 11: + temp = static_cast(EMP_DEFAULT_INTENSITY); + break; + case 12: + temp = static_cast(EMP_DEFAULT_TIME); + break; + default: + temp = 0; + break; + } + + sprintf(sexp_str_token, "%d", temp); + item->set_data(sexp_str_token, (SEXPT_NUMBER | SEXPT_VALID)); + } + else if (Operators[op].value == OP_WARP_EFFECT) + { + int temp; + char sexp_str_token[TOKEN_LENGTH]; + + switch (i) + { + case 6: + temp = 100; + break; + case 7: + temp = 10; + break; + default: + temp = 0; + break; + } + + sprintf(sexp_str_token, "%d", temp); + item->set_data(sexp_str_token, (SEXPT_NUMBER | SEXPT_VALID)); + } + else if (Operators[op].value == OP_CHANGE_BACKGROUND) + { + item->set_data("1", (SEXPT_NUMBER | SEXPT_VALID)); + } + else if (Operators[op].value == OP_ADD_BACKGROUND_BITMAP || Operators[op].value == OP_ADD_BACKGROUND_BITMAP_NEW) + { + int temp = 0; + char sexp_str_token[TOKEN_LENGTH]; + + switch (i) + { + case 4: + case 5: + temp = 100; + break; + + case 6: + case 7: + temp = 1; + break; + } + + sprintf(sexp_str_token, "%d", temp); + item->set_data(sexp_str_token, (SEXPT_NUMBER | SEXPT_VALID)); + } + else if (Operators[op].value == OP_ADD_SUN_BITMAP || Operators[op].value == OP_ADD_SUN_BITMAP_NEW) + { + int temp = 0; + char sexp_str_token[TOKEN_LENGTH]; + + if (i == 4) + temp = 100; + + sprintf(sexp_str_token, "%d", temp); + item->set_data(sexp_str_token, (SEXPT_NUMBER | SEXPT_VALID)); + } + else if (Operators[op].value == OP_MISSION_SET_NEBULA) + { + if (i == 0) + item->set_data("1", (SEXPT_NUMBER | SEXPT_VALID)); + else + item->set_data("3000", (SEXPT_NUMBER | SEXPT_VALID)); + } + else if (Operators[op].value == OP_MODIFY_VARIABLE) + { + if (_model.get_modify_variable_type(index) == OPF_NUMBER) + item->set_data("0", (SEXPT_NUMBER | SEXPT_VALID)); + else + item->set_data("", (SEXPT_STRING | SEXPT_VALID)); + } + else if (Operators[op].value == OP_MODIFY_VARIABLE_XSTR) + { + if (i == 1) + item->set_data("", (SEXPT_STRING | SEXPT_VALID)); + else + item->set_data("-1", (SEXPT_NUMBER | SEXPT_VALID)); + } + else if (Operators[op].value == OP_SET_VARIABLE_BY_INDEX) + { + if (i == 0) + item->set_data("0", (SEXPT_NUMBER | SEXPT_VALID)); + else + item->set_data("", (SEXPT_STRING | SEXPT_VALID)); + } + else if (Operators[op].value == OP_JETTISON_CARGO_NEW) + { + item->set_data("25", (SEXPT_NUMBER | SEXPT_VALID)); + } + else if (Operators[op].value == OP_TECH_ADD_INTEL_XSTR || Operators[op].value == OP_TECH_REMOVE_INTEL_XSTR) + { + item->set_data("-1", (SEXPT_NUMBER | SEXPT_VALID)); + } + else + { + item->set_data("0", (SEXPT_NUMBER | SEXPT_VALID)); + } + + return 0; + + // Goober5000 - special cases that used to be numbers but are now hybrids + case OPF_GAME_SND: + { + gamesnd_id sound_index; + + if (Operators[op].value == OP_EXPLOSION_EFFECT) + { + sound_index = GameSounds::SHIP_EXPLODE_1; + } + else if (Operators[op].value == OP_WARP_EFFECT) + { + sound_index = (i == 8) ? GameSounds::CAPITAL_WARP_IN : GameSounds::CAPITAL_WARP_OUT; + } + + if (sound_index.isValid()) + { + game_snd* snd = gamesnd_get_game_sound(sound_index); + if (can_construe_as_integer(snd->name.c_str())) + item->set_data(snd->name.c_str(), (SEXPT_NUMBER | SEXPT_VALID)); + else + item->set_data(snd->name.c_str(), (SEXPT_STRING | SEXPT_VALID)); + return 0; + } + + // if no hardcoded default, just use the listing default + break; + } + + // Goober5000 - ditto + case OPF_FIREBALL: + { + int fireball_index = -1; + + if (Operators[op].value == OP_EXPLOSION_EFFECT) + { + fireball_index = FIREBALL_MEDIUM_EXPLOSION; + } + else if (Operators[op].value == OP_WARP_EFFECT) + { + fireball_index = FIREBALL_WARP; + } + + if (fireball_index >= 0) + { + char* unique_id = Fireball_info[fireball_index].unique_id; + if (strlen(unique_id) > 0) + item->set_data(unique_id, (SEXPT_STRING | SEXPT_VALID)); + else + { + char num_str[NAME_LENGTH]; + sprintf(num_str, "%d", fireball_index); + item->set_data(num_str, (SEXPT_NUMBER | SEXPT_VALID)); + } + return 0; + } + + // if no hardcoded default, just use the listing default + break; + } + + // new default value + case OPF_PRIORITY: + item->set_data("Normal", (SEXPT_STRING | SEXPT_VALID)); + return 0; + } + + list = get_listing_opf(type, index, i); + + // Goober5000 - the way this is done is really stupid, so stupid hacks are needed to deal with it + // this particular hack is necessary because the argument string should never be a default + if (list && list->text == SEXP_ARGUMENT_STRING) + { + sexp_list_item* first_ptr; + + first_ptr = list; + list = list->next; + + delete first_ptr; + } + + if (list) + { + // copy the information from the list to the passed-in item + *item = *list; + + // but use the provided text buffer + strcpy(text_buf, list->text.c_str()); + item->text = text_buf; + + // get rid of the list, since we're done with it + list->destroy(); + item->next = nullptr; + + return 0; + } + + // catch anything that doesn't have a default value. Just describe what should be here instead + switch (type) + { + case OPF_SHIP: + case OPF_SHIP_NOT_PLAYER: + case OPF_SHIP_POINT: + case OPF_SHIP_WING: + case OPF_SHIP_PROP: + case OPF_SHIP_WING_WHOLETEAM: + case OPF_SHIP_WING_SHIPONTEAM_POINT: + case OPF_SHIP_WING_POINT: + str = ""; + break; + + case OPF_PROP: + str = ""; + break; + + case OPF_ORDER_RECIPIENT: + str = ""; + break; + + case OPF_SHIP_OR_NONE: + case OPF_SUBSYSTEM_OR_NONE: + case OPF_SHIP_WING_POINT_OR_NONE: + str = SEXP_NONE_STRING; + break; + + case OPF_WING: + str = ""; + break; + + case OPF_DOCKER_POINT: + str = ""; + break; + + case OPF_DOCKEE_POINT: + str = ""; + break; + + case OPF_SUBSYSTEM: + case OPF_AWACS_SUBSYSTEM: + case OPF_ROTATING_SUBSYSTEM: + case OPF_TRANSLATING_SUBSYSTEM: + case OPF_SUBSYS_OR_GENERIC: + str = ""; + break; + + case OPF_SUBSYSTEM_TYPE: + str = Subsystem_types[SUBSYSTEM_NONE]; + break; + + case OPF_POINT: + str = ""; + break; + + case OPF_MESSAGE: + str = ""; + break; + + case OPF_WHO_FROM: + str = ""; + break; + + case OPF_WAYPOINT_PATH: + str = ""; + break; + + case OPF_MISSION_NAME: + str = ""; + break; + + case OPF_GOAL_NAME: + str = ""; + break; + + case OPF_SHIP_TYPE: + str = ""; + break; + + case OPF_EVENT_NAME: + str = ""; + break; + + case OPF_HUGE_WEAPON: + str = ""; + break; + + case OPF_JUMP_NODE_NAME: + str = ""; + break; + + case OPF_NAV_POINT: + str = "