Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
4e788b3
long project will be long
MjnMixael Sep 22, 2025
a1afb09
add shared foundation files
MjnMixael Sep 22, 2025
56a84d5
unify some shared types across both editors
MjnMixael Sep 23, 2025
2c882f5
sexptreemodel framework
MjnMixael Sep 24, 2025
40bcf16
move opf functions to the model
MjnMixael Sep 25, 2025
dc21402
move shared functions into the model
MjnMixael Sep 25, 2025
9e6f1f9
move variable/container stuff to the model
MjnMixael Oct 4, 2025
03fec25
sexptreeactions and ui interface
MjnMixael Oct 4, 2025
24aea05
wire the model into the ui
MjnMixael Oct 4, 2025
01a2685
move some shared logic to the model
MjnMixael Oct 4, 2025
3c10d40
move more methods to the model
MjnMixael Oct 18, 2025
580699f
move clipboard to model
MjnMixael Oct 18, 2025
6f0475c
create new right click menu model
MjnMixael Oct 18, 2025
2727e5a
begin connecting the new right click menu
MjnMixael Nov 8, 2025
465b38c
wire up the new right click menu in fred
MjnMixael Nov 8, 2025
9c60655
wire up the new right click menu in qtfred
MjnMixael Nov 8, 2025
aec3455
move help text and visuals to model
MjnMixael Nov 22, 2025
2871db2
move menu option enable disable to model
MjnMixael Nov 22, 2025
ce2b50e
wire up sexp_tree to the model
MjnMixael Dec 6, 2025
8b4d10d
rename sexp_tree files to sexp_tree_ui
MjnMixael Dec 6, 2025
2c244d4
fix build errors
MjnMixael Dec 6, 2025
af5961a
fix warning
MjnMixael Dec 6, 2025
e1f6347
remove all opf forwarders
MjnMixael Dec 19, 2025
7e7af41
remove some forwarder methods
MjnMixael Dec 20, 2025
0938164
connect qtfred events editor directly to model
MjnMixael Jan 9, 2026
029361f
wite plan for unifying tree types
MjnMixael Jan 9, 2026
df045f1
replace legacy m_mode with treeflags
MjnMixael Jan 9, 2026
a91560e
fix qtfred campaign dialog
MjnMixael Jan 9, 2026
1794072
fix qtfred ship/wing dialog trees
MjnMixael Jan 24, 2026
c58b8af
build error
MjnMixael Jan 24, 2026
0d89e9a
documentation
MjnMixael Feb 7, 2026
88197c5
more documentation
MjnMixael Feb 8, 2026
d0f95aa
even more documentation
MjnMixael Feb 8, 2026
5937c38
move more opf functions to opf file
MjnMixael Feb 8, 2026
cc65f49
clean up context menu function
MjnMixael Feb 21, 2026
9f2d01c
move default argument to opf file
MjnMixael Feb 21, 2026
570e740
build errors
MjnMixael Feb 21, 2026
c4cc44d
fix qtfred dialogs and annoations
MjnMixael Mar 1, 2026
0c28ec8
new shared annotations model
MjnMixael Mar 11, 2026
f6bb317
sexp_tree_ui to sexp_tree_view
MjnMixael Mar 11, 2026
8d0451d
documentation pass
MjnMixael Mar 18, 2026
26e0375
document annotations model
MjnMixael Mar 19, 2026
eeacce8
delete the plan
MjnMixael Mar 19, 2026
bdc1c1f
move event annotations to the new model
MjnMixael Mar 19, 2026
aad4b5e
generic cleanup
MjnMixael Mar 19, 2026
32d0868
remove some old defines
MjnMixael Mar 19, 2026
ad8010b
cleanup includes
MjnMixael Mar 19, 2026
e25d757
more cleanup
MjnMixael Mar 19, 2026
ddb91f7
parenthesis
MjnMixael Mar 19, 2026
8ab055e
more parenthesis
MjnMixael Mar 19, 2026
da578ea
variable collisions
MjnMixael Mar 19, 2026
69f417e
too many parenthesis this time
MjnMixael Mar 19, 2026
2ae4cbc
clang modernization
MjnMixael Mar 19, 2026
cd19692
variable names again
MjnMixael Mar 19, 2026
2158f26
more clang and assertions
MjnMixael Mar 19, 2026
5bfd5e6
static cast
MjnMixael Mar 19, 2026
a120282
clang again
MjnMixael Mar 19, 2026
4d6a8a4
fix some bugs and issues
MjnMixael Mar 20, 2026
5d201e7
Refactor sexp tree action duplication into SexpTreeActions
MjnMixael Mar 20, 2026
2c68207
fix a few more minor issues
MjnMixael Mar 20, 2026
eb647b8
avoid clang false positive
MjnMixael Mar 20, 2026
cf04bd3
rebase tweaks
MjnMixael Mar 24, 2026
abf5b67
fix annotations issue
MjnMixael Mar 31, 2026
c0e3f53
fix typo and two small bugs claude found
MjnMixael Apr 9, 2026
ce73dd2
fix event editor tree widget loading
MjnMixael Apr 9, 2026
4bf703c
fix ship and wing sexp tree editors in qtfred
MjnMixael Apr 9, 2026
06601bc
fix qtfred briefing and debriefing trees
MjnMixael Apr 9, 2026
bbac3b9
fix a regression Claude found
MjnMixael Apr 11, 2026
25d55e9
Merge branch 'master' into qtfred-sexp-tree-refactor
MjnMixael Apr 15, 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
279 changes: 279 additions & 0 deletions code/missioneditor/sexp_annotation_model.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
#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 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<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