Files
Goober5000andClaude Fable 5.1 acf613c13c Fix FastDebug builds linking the non-redistributable debug CRT (#7919)
* fix FastDebug QtFRED linking the debug CRT

FastDebug builds in the DLL-runtime tree (used whenever qtFRED is
enabled) compiled with /MDd and, since #7450, linked Qt's debug
libraries.  Both import ucrtbased.dll, MSVCP140D.dll and
VCRUNTIME140D.dll, which Microsoft does not license for redistribution,
so end users could not run the FastDebug qtFRED shipped in nightlies
and releases.  Static Qt is ruled out by the LGPL, so the only shippable
shape is the release CRT plus release Qt.

On MSVC, _DEBUG is the CRT's own contract: /MTd and /MDd define it
themselves, and the STL headers select msvcprtd.lib and
_ITERATOR_DEBUG_LEVEL=2 from it.  Stop force-defining it for FastDebug
on MSVC; the engine keys its debug behaviour on NDEBUG, and the few
_DEBUG guards in code/ are switched to NDEBUG so they keep working.
The static tree is unchanged: /MTd still defines _DEBUG there.

Fixes #7915

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* derive the MSVC runtime choice once

The "DLL runtime or not" condition was spelled out in the toolchain and
twice in fred2 with "keep in sync" comments, and gtest_force_shared_crt
and antlr4's WITH_STATIC_CRT had already drifted: they only looked at
MSVC_USE_RUNTIME_DLL, so a qtFRED tree with tests enabled would have
asked both for the static CRT.  That is harmless today only because our
flag strings carry no literal /MD for them to rewrite.

Compute FSO_MSVC_RUNTIME_DLL once in toolchain-msvc.cmake and use it
everywhere.  InstallRequiredSystemLibraries deliberately stays keyed on
MSVC_USE_RUNTIME_DLL: the qtFRED deploy ships no compiler runtime.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* remove FastDebug runtime leftovers

The /MD -> /MDd substitution on the FastDebug flag strings has done
nothing since the runtime moved to CMAKE_MSVC_RUNTIME_LIBRARY: the
Release flags it copies contain no runtime switch.  The commented-out
_ITERATOR_DEBUG_LEVEL=0 lines for gtest and the OpenXR loader belong to
the scheme #7450 removed from the toolchain.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* use the release CRT for all FastDebug builds

Microsoft's documentation marks the static debug libraries (libcmtd,
libucrtd, libvcruntimed) "Not redistributable" and states that debug
versions of an application may only be copied to machines used for
testing, and the license grants distribution rights only for code on
the REDIST list, which contains no debug library in any form.  So the
statically linked FastDebug game and FRED2 are no more shippable than
the /MDd qtFRED was.

Link every FastDebug build against the release runtime.  The engine's
debug behaviour keys on NDEBUG, which FastDebug still leaves undefined,
so asserts, warnings and mprintf are unaffected; only the debug CRT's
heap checking goes, and that remains available in Debug builds.  FRED2's
static MFC library follows the CRT: the debug variant requires the debug
runtime.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* keep the debug icon, version flag and FRED2 asserts without _DEBUG

The resource compiler only ever sees the explicit definitions, so with
_DEBUG no longer defined for FastDebug the game lost its debug icon and
both editors lost the VS_FF_DEBUG version flag.  Key those on NDEBUG,
which FastDebug still leaves undefined.

FRED2 also used MFC's ASSERT, VERIFY, ASSERT_VALID and TRACE0, which are
all compiled out without _DEBUG.  Convert them to the FSO equivalents
FRED2 already uses nearly everywhere else: Assert, Verify, a null check
in place of ASSERT_VALID (AssertValid only exists in debug MFC), and
mprintf.  The AssertValid/Dump overrides and DEBUG_NEW stay under
_DEBUG, as they must.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-10-24 03:09:57 -04:00

1385 lines
36 KiB
C++

/*
* Copyright (C) Volition, Inc. 1999. All rights reserved.
*
* All source code herein is the property of Volition, Inc. You may not sell
* or otherwise commercially exploit the source or things you created based on the
* source.
*
*/
// CampaignTreeView.cpp : implementation file
//
#include "stdafx.h"
#include "FRED.h"
#include "CampaignTreeView.h"
#include "CampaignEditorDlg.h"
#include "CampaignTreeWnd.h"
#include "mission/missioncampaign.h"
#include "mission/missionparse.h"
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
LOCAL int Bx, By, Mission_dragging = -1, Mission_dropping = -1, Context_mission;
int Total_links = 0;
int CTV_button_down = 0;
int Level_counts[MAX_LEVELS];
int Sorted[MAX_CAMPAIGN_MISSIONS];
campaign_tree_element Elements[MAX_CAMPAIGN_MISSIONS];
campaign_tree_link Links[MAX_CAMPAIGN_TREE_LINKS];
LOCAL CRect Dragging_rect;
LOCAL CSize Rect_offset, Last_draw_size;
void init_link(campaign_tree_link &link, int from, int to)
{
link.from = from;
link.to = to;
link.sexp = Locked_sexp_true;
link.node = -1;
link.from_pos = -1;
link.to_pos = -1;
link.is_mission_loop = false;
link.is_mission_fork = false;
link.mission_branch_txt = nullptr;
link.mission_branch_brief_anim = nullptr;
link.mission_branch_brief_sound = nullptr;
link.p1 = CPoint();
link.p2 = CPoint();
}
void init_element(campaign_tree_element &element)
{
element.from_links = 0;
element.to_links = 0;
element.box = CRect();
}
/////////////////////////////////////////////////////////////////////////////
// campaign_tree_view
IMPLEMENT_DYNCREATE(campaign_tree_view, CScrollView)
campaign_tree_view *Campaign_tree_viewp;
campaign_tree_view::campaign_tree_view()
{
total_levels = 1;
total_width = 1;
}
campaign_tree_view::~campaign_tree_view()
{
}
BEGIN_MESSAGE_MAP(campaign_tree_view, CScrollView)
//{{AFX_MSG_MAP(campaign_tree_view)
ON_WM_LBUTTONDOWN()
ON_WM_MOUSEMOVE()
ON_WM_LBUTTONUP()
ON_WM_CREATE()
ON_WM_CONTEXTMENU()
ON_COMMAND(ID_REMOVE_MISSION, OnRemoveMission)
ON_COMMAND(ID_DELETE_ROW, OnDeleteRow)
ON_COMMAND(ID_INSERT_ROW, OnInsertRow)
ON_COMMAND(ID_ADD_REPEAT, OnAddRepeat)
ON_COMMAND(ID_END_OF_CAMPAIGN, OnEndOfCampaign)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// campaign_tree_view drawing
#define LEVEL_HEIGHT 75
#define CELL_WIDTH 150
#define CELL_TEXT_WIDTH 130
int campaign_tree_view::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CScrollView::OnCreate(lpCreateStruct) == -1)
return -1;
return 0;
}
void campaign_tree_view::OnDraw(CDC* pDC)
{
char str[256];
int i, x, y, f, t;
BOOL r;
CSize size;
CRect rect;
CPen black_pen, white_pen, red_pen, blue_pen, green_pen;
CBrush gray_brush;
TEXTMETRIC tm;
// setup text drawing stuff
pDC->SetTextAlign(TA_TOP | TA_CENTER);
pDC->SetTextColor(RGB(0, 0, 0));
pDC->SetBkMode(TRANSPARENT);
// figure out text box sizes
r = pDC->GetTextMetrics(&tm);
Assert(r);
Bx = CELL_TEXT_WIDTH + 4;
By = tm.tmHeight + 4;
r = gray_brush.CreateSolidBrush(RGB(192, 192, 192));
Assert(r);
pDC->FillRect(CRect(0, 0, total_width * CELL_WIDTH, total_levels * LEVEL_HEIGHT), &gray_brush);
// create pens
r = black_pen.CreatePen(PS_SOLID, 1, RGB(0, 0, 0));
Assert(r);
r = white_pen.CreatePen(PS_SOLID, 1, RGB(255, 255, 255));
Assert(r);
r = red_pen.CreatePen(PS_SOLID, 1, RGB(192, 0, 0));
Assert(r);
r = blue_pen.CreatePen(PS_SOLID, 1, RGB(0, 0, 192));
Assert(r);
r = green_pen.CreatePen(PS_SOLID, 1, RGB(0, 192, 0));
Assert(r);
pDC->SelectObject(&black_pen);
// draw level separators
for (i=1; i<total_levels; i++) {
pDC->MoveTo(0, i * LEVEL_HEIGHT - 1);
pDC->LineTo(total_width * CELL_WIDTH, i * LEVEL_HEIGHT - 1);
}
pDC->SelectObject(&white_pen);
for (i=1; i<total_levels; i++) {
pDC->MoveTo(0, i * LEVEL_HEIGHT);
pDC->LineTo(total_width * CELL_WIDTH, i * LEVEL_HEIGHT);
}
// draw edges of the whole tree rectangle
pDC->SelectObject(&black_pen);
pDC->MoveTo(0, total_levels * LEVEL_HEIGHT);
pDC->LineTo(total_width * CELL_WIDTH, total_levels * LEVEL_HEIGHT);
pDC->LineTo(total_width * CELL_WIDTH, -1);
// draw text boxes and text
for (i=0; i<Campaign.num_missions; i++) {
x = (Campaign.missions[i].pos + 1) * CELL_WIDTH / 2;
y = Campaign.missions[i].level * LEVEL_HEIGHT + LEVEL_HEIGHT / 2;
Elements[i].box.left = x - Bx / 2;
Elements[i].box.right = Elements[i].box.left + Bx;
Elements[i].box.top = y - By / 2;
Elements[i].box.bottom = Elements[i].box.top + By;
strcpy_s(str, Campaign.missions[i].name);
str[strlen(str) - 4] = 0; // strip extension from filename
GetTextExtentPoint32(pDC->m_hDC, str, (int)strlen(str), &size);
if (size.cx > CELL_TEXT_WIDTH) {
strcpy(str + strlen(str) - 1, "...");
GetTextExtentPoint32(pDC->m_hDC, str, (int)strlen(str), &size);
while (size.cx > CELL_TEXT_WIDTH) {
strcpy(str + strlen(str) - 4, "...");
GetTextExtentPoint32(pDC->m_hDC, str, (int)strlen(str), &size);
}
}
if (i == Cur_campaign_mission) {
pDC->SetTextColor(RGB(192, 0, 0));
pDC->Draw3dRect(x - Bx / 2, y - By / 2, Bx, By, RGB(0, 0, 0), RGB(255, 255, 255));
} else {
pDC->SetTextColor(RGB(0, 0, 0));
pDC->Draw3dRect(x - Bx / 2, y - By / 2, Bx, By, RGB(255, 255, 255), RGB(0, 0, 0));
}
pDC->TextOut(x, y - By / 2 + 2, str, (int)strlen(str));
}
for (i=0; i<Total_links; i++) {
f = Links[i].from;
t = Links[i].to;
if (f == -1 || t == -1) {
continue;
}
Links[i].p1.x = Elements[f].box.left + Links[i].from_pos * Bx / (Elements[f].from_links + 1);
Links[i].p1.y = Elements[f].box.bottom;
Links[i].p2.x = Elements[t].box.left + Links[i].to_pos * Bx / (Elements[t].to_links + 1);
Links[i].p2.y = Elements[t].box.top;
// if special mission link, select blue pen
if (Links[i].is_mission_loop || Links[i].is_mission_fork) {
pDC->SelectObject(&blue_pen);
}
// if active link, select highlight pen (red - normal, green - special)
if (i == Cur_campaign_link) {
if (Links[i].is_mission_loop || Links[i].is_mission_fork) {
pDC->SelectObject(&green_pen);
} else {
pDC->SelectObject(&red_pen);
}
}
// draw a line between 'from' and 'to' mission. to might be -1 in the case of end-campaign, so
// don't draw line if so.
if ( (f != t) && ( t != -1) ) {
pDC->MoveTo(Links[i].p1);
pDC->LineTo(Links[i].p2);
}
// select (defalt) black pen
pDC->SelectObject(&black_pen);
}
pDC->SelectObject(&black_pen);
}
/////////////////////////////////////////////////////////////////////////////
// campaign_tree_view diagnostics
#ifdef _DEBUG
void campaign_tree_view::AssertValid() const
{
CScrollView::AssertValid();
}
void campaign_tree_view::Dump(CDumpContext& dc) const
{
CScrollView::Dump(dc);
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// campaign_tree_view message handlers
void campaign_tree_view::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
SetScrollSizes(MM_TEXT, CSize(320, 320));
}
void stuff_link_with_formula(int *link_idx, int formula, int mission_num)
{
int j, node, node2, node3;
Assert(mission_num >= 0 && mission_num < Campaign.num_missions);
if (formula >= 0) {
if (!stricmp(CTEXT(formula), "cond")) {
// sexp is valid
node = CDR(formula);
free_one_sexp(formula);
while (node != -1) {
init_link(Links[*link_idx], mission_num);
node2 = CAR(node);
Links[*link_idx].sexp = CAR(node2);
sexp_mark_persistent(CAR(node2));
free_one_sexp(node2);
node3 = CADR(node2);
if ( !stricmp( CTEXT(node3), "next-mission") ) {
node3 = CDR(node3);
for (j=0; j<Campaign.num_missions; j++)
if (!stricmp(CTEXT(node3), Campaign.missions[j].name))
break;
if (j < Campaign.num_missions) { // mission is in campaign (you never know..)
Links[(*link_idx)++].to = j;
Elements[mission_num].from_links++;
Elements[j].to_links++;
}
} else if ( !stricmp( CTEXT(node3), "end-of-campaign") ) {
(*link_idx)++;
Elements[mission_num].from_links++;
} else
Int3(); // bogus operator in campaign file
free_sexp(Sexp_nodes[node2].rest, node2);
free_one_sexp(node);
node = CDR(node);
}
}
}
}
// this function should only be called right after a campaign is loaded. Calling it a second
// time without having loaded a campaign again will result in undefined behavior.
void campaign_tree_view::construct_tree()
{
int i;
free_links();
// initialize mission link counts
for (i=0; i<Campaign.num_missions; i++) {
init_element(Elements[i]);
}
// analyze branching sexps and build links from them.
int link_idx = 0;
for (i=0; i<Campaign.num_missions; i++) {
// do main campaign path
stuff_link_with_formula(&link_idx, Campaign.missions[i].formula, i);
// do special mission path
if ( Campaign.missions[i].flags & CMISSION_FLAG_HAS_LOOP ) {
stuff_link_with_formula(&link_idx, Campaign.missions[i].mission_loop_formula, i);
Links[link_idx-1].mission_branch_txt = Campaign.missions[i].mission_branch_desc;
Links[link_idx-1].mission_branch_brief_anim = Campaign.missions[i].mission_branch_brief_anim;
Links[link_idx-1].mission_branch_brief_sound = Campaign.missions[i].mission_branch_brief_sound;
Links[link_idx-1].is_mission_loop = true;
}
else if ( Campaign.missions[i].flags & CMISSION_FLAG_HAS_FORK ) {
Campaign.missions[i].mission_loop_formula = -1;
Links[link_idx-1].mission_branch_txt = Campaign.missions[i].mission_branch_desc;
Links[link_idx-1].mission_branch_brief_anim = Campaign.missions[i].mission_branch_brief_anim;
Links[link_idx-1].mission_branch_brief_sound = Campaign.missions[i].mission_branch_brief_sound;
Links[link_idx-1].is_mission_fork = true;
}
}
for (i=0; i<Campaign.num_missions; i++) {
Sorted[i] = i;
}
Total_links = link_idx;
if (Campaign.realign_required) {
realign_tree();
Campaign.realign_required = 0;
}
}
void campaign_tree_view::initialize()
{
int i, z;
total_levels = total_width = 1;
for (i=0; i<MAX_LEVELS; i++)
Level_counts[i] = 0;
for (i=0; i<Campaign.num_missions; i++) {
z = Campaign.missions[i].level;
if (z + 2 > total_levels)
total_levels = z + 2;
Level_counts[z]++;
z = (Campaign.missions[i].pos + 3) / 2;
if (z > total_width)
total_width = z;
}
sort_links();
SetScrollSizes(MM_TEXT, CSize(total_width * CELL_WIDTH, total_levels * LEVEL_HEIGHT));
Invalidate();
}
void campaign_tree_view::free_links()
{
int i;
for (i=0; i<Total_links; i++) {
sexp_unmark_persistent(Links[i].sexp);
free_sexp2(Links[i].sexp);
}
Total_links = 0;
}
void campaign_tree_view::realign_tree()
{
int i, j, z, offset, level, pos;
// figure out what level each mission lies on and an initial position on that level
level = pos = total_width = 0;
for (i=0; i<Campaign.num_missions; i++) {
z = Sorted[i];
for (j=0; j<Total_links; j++)
if (Links[j].to == z) {
Assert(Campaign.missions[Links[j].from].level <= Campaign.missions[z].level); // links can't go up the tree, only down
if (Campaign.missions[Links[j].from].level == level) {
level++; // force to next level in tree
pos = 0;
break;
}
}
Campaign.missions[z].level = level;
Campaign.missions[z].pos = pos++;
if (pos > total_width)
total_width = pos;
Level_counts[level] = pos;
if (!z) { // topmost node must always be alone on level
level++;
pos = 0;
}
}
// now calculate the true x position of each mission
for (i=0; i<Campaign.num_missions; i++) {
offset = total_width - Level_counts[Campaign.missions[i].level];
Campaign.missions[i].pos = Campaign.missions[i].pos * 2 + offset;
}
}
void campaign_tree_view::sort_links()
{
int i, j, k, z, to_count, from_count, swap;
int to_list[MAX_CAMPAIGN_TREE_LINKS];
int from_list[MAX_CAMPAIGN_TREE_LINKS];
for (i=0; i<Campaign.num_missions; i++) {
// build list of all to and from links for one mission at a time
to_count = from_count = 0;
for (j=0; j<Total_links; j++) {
if ((Links[j].to == i) && (Links[j].from == i))
continue; // ignore 'repeat mission' links
if (Links[j].to == i)
to_list[to_count++] = j;
if (Links[j].from == i)
from_list[from_count++] = j;
}
// sort to links, left to right and top to bottom
for (j=0; j<to_count-1; j++)
for (k=j+1; k<to_count; k++) {
swap = 0;
z = Campaign.missions[Links[to_list[j]].from].pos -
Campaign.missions[Links[to_list[k]].from].pos;
if (z > 0) // sort left to right
swap = 1;
else if (!z) { // both have same position?
z = Campaign.missions[Links[to_list[j]].from].level -
Campaign.missions[Links[to_list[k]].from].level;
// see where from link position is relative to to link position
if (Campaign.missions[i].pos < Campaign.missions[Links[to_list[j]].from].pos) {
if (z > 0) // sort bottom to top
swap = 1;
} else {
if (z < 0) // sort top to bottom
swap = 1;
}
}
if (swap) {
z = to_list[j];
to_list[j] = to_list[k];
to_list[k] = z;
}
}
// set all links to positions
for (j=0; j<to_count; j++)
Links[to_list[j]].to_pos = j + 1;
// sort from links, left to right and bottom to top
for (j=0; j<from_count-1; j++)
for (k=j+1; k<from_count; k++) {
swap = 0;
z = Campaign.missions[Links[from_list[j]].to].pos -
Campaign.missions[Links[from_list[k]].to].pos;
if (z > 0)
swap = 1;
else if (!z) {
z = Campaign.missions[Links[from_list[j]].to].level -
Campaign.missions[Links[from_list[k]].to].level;
if (Campaign.missions[i].pos < Campaign.missions[Links[from_list[j]].to].pos) {
if (z < 0)
swap = 1;
} else {
if (z > 0)
swap = 1;
}
}
if (swap) {
z = from_list[j];
from_list[j] = from_list[k];
from_list[k] = z;
}
}
// set all links from positions
for (j=0; j<from_count; j++)
Links[from_list[j]].from_pos = j + 1;
}
}
void campaign_tree_view::OnLButtonDown(UINT nFlags, CPoint point)
{
int i;
CString str;
CEdit *box;
CListBox *listbox;
CClientDC dc(this);
OnPrepareDC(&dc);
dc.DPtoLP(&point);
if (nFlags & MK_CONTROL) {
listbox = (CListBox *) &Campaign_tree_formp->m_filelist;
i = listbox->GetCurSel();
Mission_dropping = -1;
if (i != LB_ERR) {
Mission_dropping = i;
SetCapture();
}
Last_draw_size = CSize(0, 0);
Dragging_rect.SetRect(0, 0, 1, 1);
dc.DrawDragRect(Dragging_rect, Last_draw_size, NULL, CSize(0, 0));
} else {
if ( (Cur_campaign_link >= 0) && (Links[Cur_campaign_link].is_mission_loop || Links[Cur_campaign_link].is_mission_fork)) {
// HACK!! UPDATE mission loop/fork desc before changing selections
// save mission loop/fork desc
char buffer[MISSION_DESC_LENGTH];
box = (CEdit *) Campaign_tree_formp->GetDlgItem(IDC_MISSION_LOOP_DESC);
box->GetWindowText(buffer, MISSION_DESC_LENGTH);
if (strlen(buffer)) {
if (Links[Cur_campaign_link].mission_branch_txt) {
free(Links[Cur_campaign_link].mission_branch_txt);
}
Links[Cur_campaign_link].mission_branch_txt = strdup(buffer);
} else {
Links[Cur_campaign_link].mission_branch_txt = NULL;
}
// HACK!! UPDATE mission loop/fork desc before changing selections
// save mission loop/fork desc
box = (CEdit *) Campaign_tree_formp->GetDlgItem(IDC_LOOP_BRIEF_ANIM);
box->GetWindowText(buffer, MISSION_DESC_LENGTH);
if (strlen(buffer)) {
if (Links[Cur_campaign_link].mission_branch_brief_anim) {
free(Links[Cur_campaign_link].mission_branch_brief_anim);
}
Links[Cur_campaign_link].mission_branch_brief_anim = strdup(buffer);
} else {
Links[Cur_campaign_link].mission_branch_brief_anim = NULL;
}
// HACK!! UPDATE mission loop/fork desc before changing selections
// save mission loop/fork desc
box = (CEdit *) Campaign_tree_formp->GetDlgItem(IDC_LOOP_BRIEF_SOUND);
box->GetWindowText(buffer, MISSION_DESC_LENGTH);
if (strlen(buffer)) {
if (Links[Cur_campaign_link].mission_branch_brief_sound) {
free(Links[Cur_campaign_link].mission_branch_brief_sound);
}
Links[Cur_campaign_link].mission_branch_brief_sound = strdup(buffer);
} else {
Links[Cur_campaign_link].mission_branch_brief_sound = NULL;
}
}
Mission_dragging = Cur_campaign_mission = Cur_campaign_link = -1;
for (i=0; i<Campaign.num_missions; i++)
if (Elements[i].box.PtInRect(point)) {
SetCapture();
Mission_dragging = Cur_campaign_mission = i;
Dragging_rect = Elements[i].box;
Rect_offset = Dragging_rect.TopLeft() - point;
Last_draw_size = CSize(4, 4);
if (Campaign.missions[Cur_campaign_mission].flags & CMISSION_FLAG_FRED_LOAD_PENDING) // haven't loaded goal names yet (or notes)
{
read_mission_goal_list(Cur_campaign_mission);
Campaign.missions[Cur_campaign_mission].flags &= ~CMISSION_FLAG_FRED_LOAD_PENDING;
}
if (Campaign.missions[Cur_campaign_mission].notes) {
convert_multiline_string(str, Campaign.missions[Cur_campaign_mission].notes);
box = (CEdit *) Campaign_tree_formp->GetDlgItem(IDC_HELP_BOX);
if (box)
box->SetWindowText(str);
}
Campaign_tree_formp->mission_selected(Cur_campaign_mission);
break;
}
}
Invalidate();
UpdateWindow();
Campaign_tree_formp->load_tree();
if (Mission_dragging != -1) {
CRect rect = Dragging_rect;
dc.LPtoDP(&rect);
dc.DrawDragRect(rect, Last_draw_size, NULL, CSize(0, 0));
}
CScrollView::OnLButtonDown(nFlags, point);
}
void campaign_tree_view::OnMouseMove(UINT nFlags, CPoint point)
{
int i, level, pos, x, y;
CSize draw_size;
CRect rect, r1;
CClientDC dc(this);
OnPrepareDC(&dc);
dc.DPtoLP(&point);
if ((Mission_dragging >= 0) || (Mission_dropping >= 0)) {
if (GetCapture() != this) {
rect = Dragging_rect;
dc.LPtoDP(&rect);
dc.DrawDragRect(rect, CSize(0, 0), rect, Last_draw_size);
Mission_dragging = Mission_dropping = -1;
} else {
for (i=0; i<Campaign.num_missions; i++)
if (Elements[i].box.PtInRect(point))
break;
if ((i < Campaign.num_missions) && (Mission_dropping < 0)) { // on a mission box?
draw_size = CSize(4, 4);
rect = Elements[i].box;
} else {
level = query_level(point);
pos = query_pos(point);
if ((level < 0) || (pos < 0)) { // off table?
draw_size = CSize(0, 0);
rect = Dragging_rect;
} else {
draw_size = CSize(2, 2);
for (i=0; i<Campaign.num_missions; i++)
if ((Campaign.missions[i].level == level) && (Campaign.missions[i].pos + 1 == pos)) {
pos = query_alternate_pos(point);
break;
}
x = pos * CELL_WIDTH / 2 - Bx / 2;
y = level * LEVEL_HEIGHT + LEVEL_HEIGHT / 2 - By / 2;
rect.SetRect(x, y, x + Bx, y + By);
}
}
r1 = rect;
dc.LPtoDP(&r1);
dc.LPtoDP(&Dragging_rect);
dc.DrawDragRect(r1, draw_size, Dragging_rect, Last_draw_size);
Dragging_rect = rect;
Last_draw_size = draw_size;
}
}
CScrollView::OnMouseMove(nFlags, point);
}
void campaign_tree_view::OnLButtonUp(UINT nFlags, CPoint point)
{
int i, j, z, level, pos;
CClientDC dc(this);
OnPrepareDC(&dc);
dc.DPtoLP(&point);
if (Mission_dropping >= 0) { // dropping a new mission into campaign?
z = Mission_dropping;
Mission_dropping = -1;
if (GetCapture() == this) {
ReleaseCapture();
dc.LPtoDP(&Dragging_rect);
dc.DrawDragRect(Dragging_rect, CSize(0, 0), Dragging_rect, Last_draw_size);
drop_mission(z, point);
return;
}
} else if (Mission_dragging >= 0) { // moving position of a mission?
z = Mission_dragging;
Mission_dragging = -1;
if (GetCapture() == this) {
ReleaseCapture();
dc.LPtoDP(&Dragging_rect);
dc.DrawDragRect(Dragging_rect, CSize(0, 0), Dragging_rect, Last_draw_size);
for (i=0; i<Campaign.num_missions; i++)
if (Elements[i].box.PtInRect(point)) { // see if released on another mission
if (i == z) // released on the same mission?
return;
for (j=0; j<Total_links; j++)
if ((Links[j].from == z) && (Links[j].to == i))
return; // already linked
if (Total_links >= MAX_CAMPAIGN_TREE_LINKS) {
MessageBox("Too many links exist. Can't add any more.");
return;
}
if (Campaign.missions[z].level >= Campaign.missions[i].level) {
MessageBox("A branch can only be set to a mission on a lower level");
return;
}
add_link(z, i);
return;
}
// at this point, we are dragging a mission to a new place
level = query_level(point);
pos = query_pos(point);
if ((level < 0) || (pos < 0))
return;
if (!level && (get_root_mission() >= 0)) {
MessageBox("Can't move mission to this level. There is already a top level mission");
return;
}
for (i=0; i<Campaign.num_missions; i++)
if ((Campaign.missions[i].level == level) && (Campaign.missions[i].pos + 1 == pos)) {
pos = query_alternate_pos(point);
break;
}
for (i=0; i<Total_links; i++)
if (Links[i].to == z)
if (level <= Campaign.missions[Links[i].from].level) {
MessageBox("Can't move mission to that level, as it would be\n"
"above a parent mission", "Error");
return;
}
Campaign.missions[z].level = level;
Campaign.missions[z].pos = pos - 1;
correct_position(z);
sort_links();
SetScrollSizes(MM_TEXT, CSize(total_width * CELL_WIDTH, total_levels * LEVEL_HEIGHT));
Invalidate();
Campaign_modified = 1;
return;
}
}
CScrollView::OnLButtonUp(nFlags, point);
}
int campaign_tree_view::add_link(int from, int to)
{
if (Total_links >= MAX_CAMPAIGN_TREE_LINKS)
return -1;
Campaign_tree_formp->load_tree(1);
init_link(Links[Total_links], from, to);
Total_links++;
if (from != to) {
if (from >= 0)
Elements[from].from_links++;
if (to >= 0)
Elements[to].to_links++;
}
sort_links();
Campaign_tree_formp->load_tree(0);
Invalidate();
Campaign_modified = 1;
return 0;
}
int campaign_tree_view::query_level(const CPoint& p)
{
int level;
if ((p.y < 0) || (p.y >= total_levels * LEVEL_HEIGHT))
return -1;
level = p.y / LEVEL_HEIGHT;
Assert((level >= 0) && (level < total_levels));
return level;
}
int campaign_tree_view::query_pos(const CPoint& p)
{
int pos;
if ((p.x < 0) || (p.x >= total_width * CELL_WIDTH))
return -1;
pos = ((p.x * 4 / CELL_WIDTH) + 1) / 2;
Assert((pos >= 0) && (pos <= total_width * 2));
return pos;
}
int campaign_tree_view::query_alternate_pos(const CPoint& p)
{
int x, pos;
if ((p.x < 0) || (p.x >= total_width * CELL_WIDTH))
return -1;
x = p.x * 4 / CELL_WIDTH;
pos = (x + 1) / 2;
if (x & 1) // odd number
pos--;
else // even number
pos++;
Assert((pos >= 0) && (pos <= total_width * 2));
return pos;
}
/*
DROPEFFECT campaign_tree_view::OnDragEnter(COleDataObject* pDataObject, DWORD dwKeyState, CPoint point)
{
CClientDC dc(this);
OnPrepareDC(&dc);
if (!pDataObject->IsDataAvailable((unsigned short)Mission_filename_cb_format))
return DROPEFFECT_NONE; // data isn't a mission filename, the only valid data to drop here
Mission_dropping = 1;
Last_draw_size = CSize(0, 0);
Dragging_rect.SetRect(0, 0, 1, 1);
dc.DrawDragRect(Dragging_rect, Last_draw_size, NULL, CSize(0, 0));
return DROPEFFECT_MOVE;
}
void campaign_tree_view::OnDragLeave()
{
CScrollView::OnDragLeave();
if (Mission_dropping >= 0) {
CClientDC dc(this);
OnPrepareDC(&dc);
dc.LPtoDP(&Dragging_rect);
dc.DrawDragRect(Dragging_rect, CSize(0, 0), Dragging_rect, Last_draw_size);
Mission_dropping = -1;
}
}
DROPEFFECT campaign_tree_view::OnDragOver(COleDataObject* pDataObject, DWORD dwKeyState, CPoint point)
{
int i, level, pos, x, y;
CSize draw_size;
CRect rect, r1;
DROPEFFECT r = DROPEFFECT_MOVE;
if (Mission_dropping < 0)
return DROPEFFECT_NONE;
CClientDC dc(this);
OnPrepareDC(&dc);
dc.DPtoLP(&point);
level = query_level(point);
pos = query_pos(point);
if ((level < 0) || (pos < 0)) { // off table?
draw_size = CSize(0, 0);
rect = Dragging_rect;
r = DROPEFFECT_NONE;
} else {
draw_size = CSize(2, 2);
for (i=0; i<Campaign.num_missions; i++)
if ((Campaign.missions[i].level == level) && (Campaign.missions[i].pos + 1 == pos)) {
pos = query_alternate_pos(point);
break;
}
x = pos * CELL_WIDTH / 2 - Dragging_rect.Width() / 2;
y = level * LEVEL_HEIGHT + LEVEL_HEIGHT / 2 - Dragging_rect.Height() / 2;
rect.SetRect(x, y, x + Bx, y + By);
}
r1 = rect;
dc.LPtoDP(&r1);
dc.LPtoDP(&Dragging_rect);
dc.DrawDragRect(r1, draw_size, Dragging_rect, Last_draw_size);
Dragging_rect = rect;
Last_draw_size = draw_size;
return r;
}
BOOL campaign_tree_view::OnDrop(COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point)
{
int i, level, pos;
cmission *cm;
HGLOBAL hGlobal;
LPCSTR pData;
mission a_mission;
if (Mission_dropping < 0)
return FALSE;
// If the dropEffect requested is not a MOVE, return FALSE to
// signal no drop. (No COPY into trashcan)
if ((dropEffect & DROPEFFECT_MOVE) != DROPEFFECT_MOVE)
return FALSE;
CClientDC dc(this);
OnPrepareDC(&dc);
dc.DPtoLP(&point);
dc.LPtoDP(&Dragging_rect);
dc.DrawDragRect(Dragging_rect, CSize(0, 0), Dragging_rect, Last_draw_size);
Mission_dropping = -1;
if (!pDataObject->IsDataAvailable((unsigned short)Mission_filename_cb_format))
return FALSE; // data isn't a mission filename, the only valid data to drop here
// Get text data from COleDataObject
hGlobal = pDataObject->GetGlobalData((unsigned short)Mission_filename_cb_format);
// Get pointer to data
pData = (LPCSTR) GlobalLock(hGlobal);
Assert(pData);
if (Campaign.num_missions >= MAX_CAMPAIGN_MISSIONS) { // Can't add any more
GlobalUnlock(hGlobal);
MessageBox("Too many missions. Can't add more to Campaign.", "Error");
return FALSE;
}
level = query_level(point);
pos = query_pos(point);
Assert((level >= 0) && (pos >= 0)); // this should be impossible
if (!level && (get_root_mission() >= 0)) {
GlobalUnlock(hGlobal);
MessageBox("Only 1 mission may be in the top level");
return FALSE;
}
// check the number of players in a multiplayer campaign against the number of players
// in the mission that was just dropped
if ( Campaign.type != CAMPAIGN_TYPE_SINGLE ) {
get_mission_info((char *)pData, &a_mission);
if ( !(a_mission.game_type & MISSION_TYPE_MULTI) ) {
char buf[256];
sprintf( buf, "Mission \"%s\" is not a multiplayer mission", pData );
MessageBox(buf, "Error");
GlobalUnlock(hGlobal);
return FALSE;
}
if ( Campaign.num_players != 0 ) {
if ( Campaign.num_players != a_mission.num_players ) {
char buf[512];
sprintf(buf, "Mission \"%s\" has %d players. Campaign has %d players. Campaign will not play properly in FreeSpace", pData, a_mission.num_players, Campaign.num_players );
MessageBox(buf, "Warning");
}
} else {
Campaign.num_players = a_mission.num_players;
}
}
init_element(Elements[Campaign.num_missions]);
cm = &(Campaign.missions[Campaign.num_missions++]);
cm->name = strdup(pData);
cm->formula = Locked_sexp_true;
cm->flags |= CMISSION_FLAG_FRED_LOAD_PENDING;
cm->notes = NULL;
cm->briefing_cutscene[0] = 0;
for (i=0; i<Campaign.num_missions - 1; i++)
if ((Campaign.missions[i].level == level) && (Campaign.missions[i].pos + 1 == pos)) {
pos = query_alternate_pos(point);
break;
}
cm->level = level;
cm->pos = pos - 1;
correct_position(Campaign.num_missions - 1);
sort_links();
SetScrollSizes(MM_TEXT, CSize(total_width * CELL_WIDTH, total_levels * LEVEL_HEIGHT));
Invalidate();
// update and reinitialize dialog items
if ( Campaign.type != CAMPAIGN_TYPE_SINGLE ) {
Campaign_tree_formp->update();
Campaign_tree_formp->initialize(0);
}
// Unlock memory - Send dropped text into the "bit-bucket"
GlobalUnlock(hGlobal);
Campaign_modified = 1;
return TRUE;
}
*/
void campaign_tree_view::drop_mission(int m, CPoint point)
{
char name[MAX_FILENAME_LEN + 1];
int i, item, level, pos;
cmission *cm;
mission a_mission;
CListBox *listbox;
level = query_level(point);
pos = query_pos(point);
Assert((level >= 0) && (pos >= 0)); // this should be impossible
listbox = (CListBox *) &Campaign_tree_formp->m_filelist;
item = listbox->GetCurSel();
if (item == LB_ERR) {
MessageBox("Select a mission from listbox to add.", "Error");
return;
}
if (listbox->GetTextLen(item) > MAX_FILENAME_LEN) {
char buf[256];
sprintf(buf, "Filename is too long. Must be %d or less characters.", MAX_FILENAME_LEN);
MessageBox(buf, "Error");
return; // filename is too long. Would overflow our buffer
}
// grab the filename selected from the listbox
listbox->GetText(item, name);
if (Campaign.num_missions >= MAX_CAMPAIGN_MISSIONS) { // Can't add any more
MessageBox("Too many missions. Can't add more to Campaign.", "Error");
return;
}
if (!level && (get_root_mission() >= 0)) {
MessageBox("Only 1 mission may be in the top level");
return;
}
// check the number of players in a multiplayer campaign against the number of players
// in the mission that was just dropped
if ( Campaign.type != CAMPAIGN_TYPE_SINGLE ) {
get_mission_info(name, &a_mission);
if ( !(a_mission.game_type & MISSION_TYPE_MULTI) ) {
char buf[256];
sprintf(buf, "Mission \"%s\" is not a multiplayer mission", name);
MessageBox(buf, "Error");
return;
}
if (Campaign.num_players != 0) {
if (Campaign.num_players != a_mission.num_players) {
char buf[512];
sprintf(buf, "Mission \"%s\" has %d players. Campaign has %d players. Campaign will not play properly in FreeSpace", name, a_mission.num_players, Campaign.num_players );
MessageBox(buf, "Warning");
}
} else {
Campaign.num_players = a_mission.num_players;
}
}
init_element(Elements[Campaign.num_missions]);
cm = &(Campaign.missions[Campaign.num_missions++]);
cm->name = strdup(name);
cm->formula = Locked_sexp_true;
cm->flags |= CMISSION_FLAG_FRED_LOAD_PENDING;
cm->notes = NULL;
cm->briefing_cutscene[0] = 0;
for (i=0; i<Campaign.num_missions - 1; i++)
if ((Campaign.missions[i].level == level) && (Campaign.missions[i].pos + 1 == pos)) {
pos = query_alternate_pos(point);
break;
}
cm->level = level;
cm->pos = pos - 1;
correct_position(Campaign.num_missions - 1);
sort_links();
SetScrollSizes(MM_TEXT, CSize(total_width * CELL_WIDTH, total_levels * LEVEL_HEIGHT));
Invalidate();
// update and reinitialize dialog items
if ( Campaign.type != CAMPAIGN_TYPE_SINGLE ) {
Campaign_tree_formp->update();
Campaign_tree_formp->initialize(false, false);
}
listbox->DeleteString(item);
Campaign_modified = 1;
return;
}
void campaign_tree_view::sort_elements()
{
int i, j, s1, s2;
for (i=0; i<Campaign.num_missions; i++)
Sorted[i] = i;
// sort the tree, so realignment will work property
for (i=1; i<Campaign.num_missions; i++) {
s1 = Sorted[i];
for (j=i-1; j>=0; j--) {
s2 = Sorted[j];
if ((Campaign.missions[s1].level > Campaign.missions[s2].level) ||
((Campaign.missions[s1].level == Campaign.missions[s2].level) &&
(Campaign.missions[s1].pos > Campaign.missions[s2].pos)))
break;
Sorted[j + 1] = s2;
}
Sorted[j + 1] = s1;
}
}
void campaign_tree_view::correct_position(int num)
{
int i, z;
// move missions down if required
if (Campaign.missions[num].level + 2 > total_levels)
total_levels = Campaign.missions[num].level + 2;
for (i=0; i<Total_links; i++)
if (Links[i].from == num) {
z = Links[i].to;
if ( (num != z) && (Campaign.missions[num].level >= Campaign.missions[z].level) ) {
Campaign.missions[z].level = Campaign.missions[num].level + 1;
correct_position(z);
}
}
// space out horizontally to avoid overlap of missions
horizontally_align_mission(num, -1);
horizontally_align_mission(num, 1);
}
void campaign_tree_view::horizontally_align_mission(int num, int dir)
{
int i, z;
if ((Campaign.missions[num].pos == -1) || (Campaign.missions[num].pos + 1 == total_width * 2)) { // need to expand total_width
for (i=0; i<Campaign.num_missions; i++)
Campaign.missions[i].pos++;
total_width++;
}
for (i=0; i<Campaign.num_missions; i++) {
if (i == num)
continue;
if (Campaign.missions[i].level == Campaign.missions[num].level) {
z = Campaign.missions[i].pos - Campaign.missions[num].pos;
if (dir < 0) {
if (!z || (z == -1)) {
Campaign.missions[i].pos = Campaign.missions[num].pos - 2;
horizontally_align_mission(i, -1);
}
} else {
if (!z || (z == 1)) {
Campaign.missions[i].pos = Campaign.missions[num].pos + 2;
horizontally_align_mission(i, 1);
}
}
}
}
}
void campaign_tree_view::delete_link(int num)
{
Assert((num >= 0) && (num < Total_links));
int from = Links[num].from;
int to = Links[num].to;
if (from != to) {
if (from >= 0)
Elements[from].from_links--;
if (to >= 0)
Elements[to].to_links--;
}
sexp_unmark_persistent(Links[num].sexp);
free_sexp2(Links[num].sexp);
while (num < Total_links - 1) {
Links[num] = Links[num + 1];
num++;
}
Total_links--;
sort_links();
Invalidate();
Campaign_modified = 1;
return;
}
int campaign_tree_view::get_root_mission()
{
int i;
for (i=0; i<Campaign.num_missions; i++)
if (!Campaign.missions[i].level)
return i;
return -1;
}
void campaign_tree_view::OnContextMenu(CWnd* pWnd, CPoint point)
{
int i;
CMenu menu, *popup;
CPoint p = point;
CClientDC dc(this);
OnPrepareDC(&dc);
dc.DPtoLP(&p);
ScreenToClient(&p);
for (i=0; i<Campaign.num_missions; i++)
if (Elements[i].box.PtInRect(p))
break;
if (i < Campaign.num_missions) { // clicked on a mission
Context_mission = i;
if (menu.LoadMenu(IDR_CPGN_VIEW_ON)) {
popup = menu.GetSubMenu(0);
Assert(popup);
popup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
}
} else {
Context_mission = query_level(p);
if ((Context_mission >= 0) && (Context_mission < total_levels))
if (menu.LoadMenu(IDR_CPGN_VIEW_OFF)) {
popup = menu.GetSubMenu(0);
Assert(popup);
popup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
}
}
}
void campaign_tree_view::OnRemoveMission()
{
remove_mission(Context_mission);
Invalidate();
UpdateWindow();
// for multiplayer missions, update the data and reiniailize the dialog -- the number of player
// in the mission might have changed because of deletion of the first mission
if ( Campaign.type != CAMPAIGN_TYPE_SINGLE ) {
Campaign_tree_formp->update();
Campaign_tree_formp->initialize(true, false);
}
}
void campaign_tree_view::remove_mission(int m)
{
int i, z;
CEdit *box;
Assert(m >= 0);
Campaign_tree_formp->m_filelist.AddString(Campaign.missions[m].name);
z = --Campaign.num_missions;
i = Total_links;
while (i--) {
if ((Links[i].from == m) || (Links[i].to == m))
delete_link(i);
if (Links[i].from == z)
Links[i].from = m;
if (Links[i].to == z)
Links[i].to = m;
}
Elements[m] = Elements[z];
// free the removed mission's strings before its slot is overwritten, then
// null the vacated slot's pointers, which now alias slot m's strings. (If
// m == z, the assignment is a self-assign of freed pointers, and the
// nulling below leaves the slot clean.)
mission_campaign_free_mission_strings(Campaign.missions[m]);
Campaign.missions[m] = Campaign.missions[z];
Campaign.missions[z].name = nullptr;
Campaign.missions[z].notes = nullptr;
Campaign.missions[z].mission_branch_desc = nullptr;
Campaign.missions[z].mission_branch_brief_anim = nullptr;
Campaign.missions[z].mission_branch_brief_sound = nullptr;
if (m == Cur_campaign_mission) {
Cur_campaign_mission = -1;
box = (CEdit *) Campaign_tree_formp->GetDlgItem(IDC_HELP_BOX);
if (box)
box->SetWindowText("");
Campaign_tree_formp->load_tree(0);
}
Campaign_modified = 1;
}
void campaign_tree_view::OnDeleteRow()
{
int i, z;
if (!Context_mission) {
MessageBox("Can't delete the top level");
return;
}
for (i=z=0; i<Campaign.num_missions; i++)
if (Campaign.missions[i].level == Context_mission)
z++;
if (z) {
z = MessageBox("Deleting row will remove all missions on this row", "Notice", MB_ICONEXCLAMATION | MB_OKCANCEL);
if (z == IDCANCEL)
return;
}
while (i--)
if (Campaign.missions[i].level == Context_mission)
remove_mission(i);
for (i=0; i<Campaign.num_missions; i++)
if (Campaign.missions[i].level > Context_mission)
Campaign.missions[i].level--;
total_levels--;
SetScrollSizes(MM_TEXT, CSize(total_width * CELL_WIDTH, total_levels * LEVEL_HEIGHT));
Invalidate();
UpdateWindow();
Campaign_modified = 1;
}
void campaign_tree_view::OnInsertRow()
{
int i;
for (i=0; i<Campaign.num_missions; i++)
if (Campaign.missions[i].level >= Context_mission)
Campaign.missions[i].level++;
total_levels++;
SetScrollSizes(MM_TEXT, CSize(total_width * CELL_WIDTH, total_levels * LEVEL_HEIGHT));
Invalidate();
UpdateWindow();
Campaign_modified = 1;
}
void campaign_tree_view::OnAddRepeat()
{
if (add_link(Context_mission, Context_mission)) {
MessageBox("Too many links exist. Can't add any more.");
return;
}
}
void campaign_tree_view::OnEndOfCampaign()
{
if ( add_link(Context_mission, -1) ) {
MessageBox("Too many links exist. Cannot add any more.");
return;
}
}