Skip to content

Commit a55775e

Browse files
Goober5000claude
andcommitted
add Ships[] slot reassignment utilities for FRED/qtFRED
reassign_ship_slot moves a ship between Ships[] slots, fixing up every back-reference: Objects[].instance, Ai_info[].shipnum, Wings[].ship_index[], Player_start_shipnum, Ship_registry's cached shipnum, and the FRED-side parallel arrays Fred_alt_names/Fred_callsigns plus cur_ship (passed via FredShipSlotConfig so non-FRED callers can opt out). swap_ship_slots wraps it as a three-leg swap through a temporary empty slot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1c079a7 commit a55775e

2 files changed

Lines changed: 240 additions & 0 deletions

File tree

code/missioneditor/common.cpp

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
// methods and members common to any mission editor FSO may have
22
#include "common.h"
3+
#include "ai/ai.h"
4+
#include "globalincs/linklist.h"
35
#include "mission/missionparse.h"
46
#include "iff_defs/iff_defs.h"
7+
#include "object/object.h"
58
#include "ship/ship.h"
69

10+
#include <algorithm>
11+
712
// to keep track of data
813
char Voice_abbrev_briefing[NAME_LENGTH];
914
char Voice_abbrev_campaign[NAME_LENGTH];
@@ -145,3 +150,209 @@ void generate_weaponry_usage_list_wing(int wing_num, int* arr)
145150
}
146151
}
147152
}
153+
154+
void reassign_ship_slot(int from, int to, const FredShipSlotConfig& cfg, bool resort_obj_list)
155+
{
156+
Assertion(from != to, "reassign_ship_slot: from == to (%d)", from);
157+
Assertion(from >= 0 && from < MAX_SHIPS, "reassign_ship_slot: 'from' slot %d out of range", from);
158+
Assertion(to >= 0 && to < MAX_SHIPS, "reassign_ship_slot: 'to' slot %d out of range", to);
159+
Assertion(Ships[from].objnum >= 0, "reassign_ship_slot: source slot %d is empty", from);
160+
Assertion(Ships[to].objnum < 0, "reassign_ship_slot: destination slot %d is occupied", to);
161+
162+
// Move the ship struct itself. Per the engine's convention, a slot with
163+
// objnum < 0 is considered empty; other fields in the vacated slot are
164+
// left as-is (unreachable through the "is this slot used" guard).
165+
// Move (not copy) because ship contains a unique_ptr member.
166+
Ships[to] = std::move(Ships[from]);
167+
Ships[from].objnum = -1;
168+
169+
// subsys_list is an intrusive doubly-linked list whose head sentinel's
170+
// address is meaningful: real nodes' prev/next bookend back to &head, and
171+
// an empty list is self-referential. The move copied those pointers
172+
// verbatim, so they still reference the old (vacated) sentinel address.
173+
{
174+
auto old_head = &Ships[from].subsys_list;
175+
auto new_head = &Ships[to].subsys_list;
176+
if (new_head->next == old_head)
177+
{
178+
// Empty list: re-init self-referential on the new head.
179+
new_head->next = new_head;
180+
new_head->prev = new_head;
181+
}
182+
else
183+
{
184+
// Non-empty: repoint the first node's prev and the last node's next.
185+
new_head->next->prev = new_head;
186+
new_head->prev->next = new_head;
187+
}
188+
}
189+
190+
// Move FRED-side parallel arrays if the caller supplied them.
191+
if (cfg.fred_alt_names != nullptr)
192+
{
193+
strcpy_s(cfg.fred_alt_names[to], cfg.fred_alt_names[from]);
194+
cfg.fred_alt_names[from][0] = '\0';
195+
}
196+
if (cfg.fred_callsigns != nullptr)
197+
{
198+
strcpy_s(cfg.fred_callsigns[to], cfg.fred_callsigns[from]);
199+
cfg.fred_callsigns[from][0] = '\0';
200+
}
201+
202+
// Object back-reference.
203+
Objects[Ships[to].objnum].instance = to;
204+
205+
// Keep obj_used_list iteration order in sync with Ships[] slot order. The
206+
// re-sort is a total rebuild rather than an incremental fix, so a caller
207+
// making a batch of reassignments may defer it to the final call.
208+
if (resort_obj_list)
209+
resort_ships_in_obj_used_list();
210+
211+
// AI back-reference (the one invariant codified by internal_integrity_check).
212+
Ai_info[Ships[to].ai_index].shipnum = to;
213+
214+
// Wing membership: scan every wing and re-point any reference to the old slot.
215+
// (wing.special_ship is wing-relative, NOT a Ships[] index, so it is intentionally
216+
// not touched here.)
217+
for (auto &w: Wings)
218+
{
219+
if (w.wave_count == 0)
220+
continue;
221+
for (int k = 0; k < w.wave_count; ++k)
222+
{
223+
if (w.ship_index[k] == from)
224+
w.ship_index[k] = to;
225+
}
226+
}
227+
228+
// Single-player start.
229+
if (Player_start_shipnum == from)
230+
Player_start_shipnum = to;
231+
232+
// Ship_registry caches the shipnum on its entries (lookup is by name, but the
233+
// cached integer would otherwise go stale).
234+
int reg = ship_registry_get_index(Ships[to].ship_name);
235+
if (reg >= 0)
236+
Ship_registry[reg].shipnum = to;
237+
238+
// FRED's current-ship pointer, if the caller is tracking one.
239+
if (cfg.cur_ship != nullptr && *cfg.cur_ship == from)
240+
*cfg.cur_ship = to;
241+
}
242+
243+
static bool ship_slot_is_empty(int i)
244+
{
245+
return Ships[i].objnum < 0;
246+
}
247+
248+
static int find_free_slot(int max_slots, bool (*slot_is_empty)(int), const char* caller)
249+
{
250+
for (int i = 0; i < max_slots; ++i)
251+
{
252+
if (slot_is_empty(i))
253+
return i;
254+
}
255+
Assertion(false, "%s: no free slot available for the temporary leg", caller);
256+
return -1;
257+
}
258+
259+
template <typename TConfig>
260+
static void swap_slots(int a, int b, const TConfig& cfg, int max_slots,
261+
bool (*slot_is_empty)(int), void (*reassign)(int, int, const TConfig&, bool), const char* caller)
262+
{
263+
if (a == b)
264+
return;
265+
266+
Assertion(a >= 0 && a < max_slots, "%s: slot 'a' %d out of range", caller, a);
267+
Assertion(b >= 0 && b < max_slots, "%s: slot 'b' %d out of range", caller, b);
268+
Assertion(!slot_is_empty(a) && !slot_is_empty(b),
269+
"%s: both slots must be valid (a=%d, b=%d)", caller, a, b);
270+
271+
// Find a free temporary slot.
272+
int tmp = find_free_slot(max_slots, slot_is_empty, caller);
273+
274+
// Three-leg swap; each call's preconditions hold by construction. The
275+
// total-rebuild fixups are deferred to the final leg.
276+
reassign(a, tmp, cfg, false);
277+
reassign(b, a, cfg, false);
278+
reassign(tmp, b, cfg, true);
279+
}
280+
281+
template <typename TConfig>
282+
static void rotate_slots(const SCP_vector<int>& slots, int from_pos, int to_pos, const TConfig& cfg,
283+
int max_slots, bool (*slot_is_empty)(int), void (*reassign)(int, int, const TConfig&, bool), const char* caller)
284+
{
285+
if (from_pos == to_pos)
286+
return;
287+
288+
int count = (int)slots.size();
289+
Assertion(from_pos >= 0 && from_pos < count, "%s: 'from' position %d out of range", caller, from_pos);
290+
Assertion(to_pos >= 0 && to_pos < count, "%s: 'to' position %d out of range", caller, to_pos);
291+
292+
// Find a free temporary slot.
293+
int tmp = find_free_slot(max_slots, slot_is_empty, caller);
294+
295+
// Park the moving item in the free slot, shift everything between the two
296+
// positions over by one, then drop the item into the slot vacated at the
297+
// far end. Preserves the relative order of everything else, in K+2
298+
// reassignments for a move of K positions (vs 3K for a bubble of swaps).
299+
// The total-rebuild fixups are deferred to the final leg.
300+
int step = (to_pos > from_pos) ? 1 : -1;
301+
reassign(slots[from_pos], tmp, cfg, false);
302+
for (int j = from_pos; j != to_pos; j += step)
303+
reassign(slots[j + step], slots[j], cfg, false);
304+
reassign(tmp, slots[to_pos], cfg, true);
305+
}
306+
307+
void swap_ship_slots(int a, int b, const FredShipSlotConfig& cfg)
308+
{
309+
swap_slots(a, b, cfg, MAX_SHIPS, ship_slot_is_empty, reassign_ship_slot, "swap_ship_slots");
310+
}
311+
312+
void rotate_ship_slots(const SCP_vector<int>& slots, int from_pos, int to_pos, const FredShipSlotConfig& cfg)
313+
{
314+
rotate_slots(slots, from_pos, to_pos, cfg, MAX_SHIPS, ship_slot_is_empty, reassign_ship_slot, "rotate_ship_slots");
315+
}
316+
317+
// Bulk-re-sort one type's subset of obj_used_list while keeping non-matching
318+
// entries in their original relative positions. Each callsite supplies a
319+
// type matcher and a key function; the i-th matching slot (in original list
320+
// order) receives the i-th smallest matching node by key.
321+
static void resort_obj_used_list_subset(
322+
bool (*matches_type)(int),
323+
int (*key)(const object*))
324+
{
325+
SCP_vector<object*> all;
326+
SCP_vector<object*> matched;
327+
for (auto o : list_range(&obj_used_list))
328+
{
329+
all.push_back(o);
330+
if (matches_type(o->type))
331+
matched.push_back(o);
332+
}
333+
334+
std::sort(matched.begin(), matched.end(),
335+
[&](const object* a, const object* b) { return key(a) < key(b); });
336+
337+
list_init(&obj_used_list);
338+
auto it = matched.begin();
339+
for (auto o : all)
340+
{
341+
if (matches_type(o->type))
342+
{
343+
list_append(&obj_used_list, *it);
344+
++it;
345+
}
346+
else
347+
{
348+
list_append(&obj_used_list, o);
349+
}
350+
}
351+
}
352+
353+
void resort_ships_in_obj_used_list()
354+
{
355+
resort_obj_used_list_subset(
356+
[](int t) { return t == OBJ_SHIP || t == OBJ_START; },
357+
[](const object* o) { return o->instance; });
358+
}

code/missioneditor/common.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,32 @@ anchor_t target_to_anchor(int target);
4848
void generate_weaponry_usage_list_team(int team, int* arr);
4949

5050
void generate_weaponry_usage_list_wing(int wing_num, int* arr);
51+
52+
struct FredShipSlotConfig
53+
{
54+
char (*fred_alt_names)[NAME_LENGTH + 1] = nullptr;
55+
char (*fred_callsigns)[NAME_LENGTH + 1] = nullptr;
56+
57+
int *cur_ship = nullptr;
58+
};
59+
60+
// Move the ship currently in Ships[from] into Ships[to], updating every
61+
// back-reference (Objects, Ai_info, Wings, Player_start_shipnum, Ship_registry,
62+
// and editor-side fields supplied via cfg). Leaves Ships[from] empty.
63+
// Preconditions: from != to, Ships[from].objnum >= 0, Ships[to].objnum < 0.
64+
// No caller may hold a ship* to either slot across this call.
65+
// Fields in cfg whose pointers are nullptr are skipped.
66+
void reassign_ship_slot(int from, int to, const FredShipSlotConfig& cfg, bool resort_obj_list = true);
67+
68+
// Swap the contents of two slots. Both must be valid (Ships[a].objnum >= 0
69+
// and Ships[b].objnum >= 0). Implemented as three calls to reassign_ship_slot
70+
// via a temporary empty slot.
71+
void swap_ship_slots(int a, int b, const FredShipSlotConfig& cfg);
72+
73+
// Move the item at position from_pos in slots to position to_pos, shifting the
74+
// items in between by one position.
75+
void rotate_ship_slots(const SCP_vector<int>& slots, int from_pos, int to_pos, const FredShipSlotConfig& cfg);
76+
77+
// Restore the obj_used_list invariant for the OBJ_SHIP/OBJ_START subset:
78+
// among ship-type entries, list order matches Ships[] index order.
79+
void resort_ships_in_obj_used_list();

0 commit comments

Comments
 (0)