Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
078a20d
long project will be long
MjnMixael Sep 22, 2025
545bd54
add shared foundation files
MjnMixael Sep 22, 2025
4f28685
unify some shared types across both editors
MjnMixael Sep 23, 2025
2f3f2c2
sexptreemodel framework
MjnMixael Sep 24, 2025
f91a08f
move opf functions to the model
MjnMixael Sep 25, 2025
f2af263
move shared functions into the model
MjnMixael Sep 25, 2025
fda241c
move variable/container stuff to the model
MjnMixael Oct 4, 2025
8027ce5
sexptreeactions and ui interface
MjnMixael Oct 4, 2025
b2526f6
wire the model into the ui
MjnMixael Oct 4, 2025
203a047
move some shared logic to the model
MjnMixael Oct 4, 2025
221a698
move more methods to the model
MjnMixael Oct 18, 2025
18342b6
move clipboard to model
MjnMixael Oct 18, 2025
d7d6682
create new right click menu model
MjnMixael Oct 18, 2025
caddeb0
begin connecting the new right click menu
MjnMixael Nov 8, 2025
32ba443
wire up the new right click menu in fred
MjnMixael Nov 8, 2025
a0477ab
wire up the new right click menu in qtfred
MjnMixael Nov 8, 2025
d9b1f8d
move help text and visuals to model
MjnMixael Nov 22, 2025
6c635e4
move menu option enable disable to model
MjnMixael Nov 22, 2025
77986f7
wire up sexp_tree to the model
MjnMixael Dec 6, 2025
b26bb51
rename sexp_tree files to sexp_tree_ui
MjnMixael Dec 6, 2025
dc6fdce
fix build errors
MjnMixael Dec 6, 2025
078808b
fix warning
MjnMixael Dec 6, 2025
ac88a3f
remove all opf forwarders
MjnMixael Dec 19, 2025
2be08c7
remove some forwarder methods
MjnMixael Dec 20, 2025
f8aae42
connect qtfred events editor directly to model
MjnMixael Jan 9, 2026
7b9850d
wite plan for unifying tree types
MjnMixael Jan 9, 2026
5c06a9b
replace legacy m_mode with treeflags
MjnMixael Jan 9, 2026
c12a495
fix qtfred campaign dialog
MjnMixael Jan 9, 2026
1978617
fix qtfred ship/wing dialog trees
MjnMixael Jan 24, 2026
518bcae
build error
MjnMixael Jan 24, 2026
e823254
documentation
MjnMixael Feb 7, 2026
59e27f2
more documentation
MjnMixael Feb 8, 2026
41b99e6
even more documentation
MjnMixael Feb 8, 2026
64cd9ea
move more opf functions to opf file
MjnMixael Feb 8, 2026
86820a3
clean up context menu function
MjnMixael Feb 21, 2026
6c3bbef
move default argument to opf file
MjnMixael Feb 21, 2026
590135c
build errors
MjnMixael Feb 21, 2026
7b667f7
fix qtfred dialogs and annoations
MjnMixael Mar 1, 2026
a72de8b
new shared annotations model
MjnMixael Mar 11, 2026
ec73182
sexp_tree_ui to sexp_tree_view
MjnMixael Mar 11, 2026
91bc5b0
documentation pass
MjnMixael Mar 18, 2026
7060423
document annotations model
MjnMixael Mar 19, 2026
257921f
delete the plan
MjnMixael Mar 19, 2026
d432acc
move event annotations to the new model
MjnMixael Mar 19, 2026
4fdcd2d
generic cleanup
MjnMixael Mar 19, 2026
04038da
remove some old defines
MjnMixael Mar 19, 2026
d6c42e0
cleanup includes
MjnMixael Mar 19, 2026
4ace2ee
more cleanup
MjnMixael Mar 19, 2026
2e492f0
parenthesis
MjnMixael Mar 19, 2026
3dba3e4
more parenthesis
MjnMixael Mar 19, 2026
0820918
variable collisions
MjnMixael Mar 19, 2026
984ed3a
too many parenthesis this time
MjnMixael Mar 19, 2026
3a1c5bd
clang modernization
MjnMixael Mar 19, 2026
492abae
variable names again
MjnMixael Mar 19, 2026
9beaaf9
more clang and assertions
MjnMixael Mar 19, 2026
48656c2
static cast
MjnMixael Mar 19, 2026
5db8cb9
clang again
MjnMixael Mar 19, 2026
f9118f8
fix some bugs and issues
MjnMixael Mar 20, 2026
dc2b29f
Refactor sexp tree action duplication into SexpTreeActions
MjnMixael Mar 20, 2026
ee03a65
fix a few more minor issues
MjnMixael Mar 20, 2026
ec76cc1
avoid clang false positive
MjnMixael Mar 20, 2026
573a211
rebase tweaks
MjnMixael Mar 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions code/mission/missiongoals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,6 @@ SCP_vector<mission_event> Mission_events;
SCP_vector<mission_goal> Mission_goals; // structure for the goals of this mission
static goal_text Goal_text;

SCP_vector<event_annotation> 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

Expand Down
16 changes: 0 additions & 16 deletions code/mission/missiongoals.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> 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_annotation> Event_annotations;


// prototypes
void mission_goals_and_events_init( void );
void mission_show_goals_init();
Expand Down
1 change: 1 addition & 0 deletions code/mission/missionparse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions code/missioneditor/missionsave.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
270 changes: 270 additions & 0 deletions code/missioneditor/sexp_annotation_model.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
#include "missioneditor/sexp_annotation_model.h"
#include "missioneditor/sexp_tree_model.h"

SCP_vector<event_annotation> 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<sexp_tree_item>& tree_nodes, const SCP_vector<mission_event>& events, const SCP_vector<int>& 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<sexp_tree_item>& tree_nodes, const SCP_vector<mission_event>& events, const SCP_vector<int>& sig)
{
for (auto& ea : m_annotations) {
int key = ea.node_index;
SCP_list<int> old_path = ea.path;
ea.path.clear();

if (key >= 0 && key < static_cast<int>(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<int>(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 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<int> SexpAnnotationModel::buildPath(int key, const SCP_vector<sexp_tree_item>& tree_nodes, const SCP_vector<mission_event>& events)
{
SCP_list<int> path;

// --- Root key: path is just [event_index] ---
if (isRootKey(key)) {
int formula = formulaFromRootKey(key);
for (int i = 0; i < static_cast<int>(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<int>(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<int>(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<int>(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<int> 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<int>& path, const SCP_vector<sexp_tree_item>& tree_nodes, const SCP_vector<mission_event>& events, const SCP_vector<int>& 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<int>(sig.size()); ++i) {
if (sig[i] == orig_event_idx) {
if (i < static_cast<int>(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<int>(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
}
Loading
Loading