1 /**
2 * @file
3 * @brief Deals with the Transfer stuff.
4 * @note Transfer menu functions prefix: TR_
5 * @todo Remove direct access to nodes
6 */
7
8 /*
9 Copyright (C) 2002-2013 UFO: Alien Invasion.
10
11 This program is free software; you can redistribute it and/or
12 modify it under the terms of the GNU General Public License
13 as published by the Free Software Foundation; either version 2
14 of the License, or (at your option) any later version.
15
16 This program is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
19
20 See the GNU General Public License for more details.
21
22 You should have received a copy of the GNU General Public License
23 along with this program; if not, write to the Free Software
24 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25
26 */
27
28 #include "../../cl_shared.h"
29 #include "cp_campaign.h"
30 #include "cp_capacity.h"
31 #include "cp_time.h"
32 #include "save/save_transfer.h"
33 #include "cp_transfer_callbacks.h"
34 #include "aliencargo.h"
35 #include "aliencontainment.h"
36
37 /**
38 * @brief Unloads transfer cargo when finishing the transfer or destroys it when no buildings/base.
39 * @param[in,out] destination The destination base - might be nullptr in case the base
40 * is already destroyed
41 * @param[in] transfer Pointer to transfer in ccs.transfers.
42 * @param[in] success True if the transfer reaches dest base, false if the base got destroyed.
43 * @sa TR_TransferEnd
44 */
TR_EmptyTransferCargo(base_t * destination,transfer_t * transfer,bool success)45 static void TR_EmptyTransferCargo (base_t* destination, transfer_t* transfer, bool success)
46 {
47 assert(transfer);
48
49 if (transfer->hasItems && success) { /* Items. */
50 const objDef_t* od = INVSH_GetItemByID(ANTIMATTER_TECH_ID);
51 int i;
52
53 /* antimatter */
54 if (transfer->itemAmount[od->idx] > 0) {
55 if (B_GetBuildingStatus(destination, B_ANTIMATTER)) {
56 B_ManageAntimatter(destination, transfer->itemAmount[od->idx], true);
57 } else {
58 Com_sprintf(cp_messageBuffer, sizeof(cp_messageBuffer), _("%s does not have Antimatter Storage, antimatter are removed!"), destination->name);
59 MSO_CheckAddNewMessage(NT_TRANSFER_LOST, _("Transport mission"), cp_messageBuffer, MSG_TRANSFERFINISHED);
60 }
61 }
62 /* items */
63 for (i = 0; i < cgi->csi->numODs; i++) {
64 od = INVSH_GetItemByIDX(i);
65
66 if (transfer->itemAmount[od->idx] <= 0)
67 continue;
68 if (!B_ItemIsStoredInBaseStorage(od))
69 continue;
70 B_AddToStorage(destination, od, transfer->itemAmount[od->idx]);
71 }
72 }
73
74 if (transfer->hasEmployees && transfer->srcBase) { /* Employees. (cannot come from a mission) */
75 if (!success) { /* Employees will be unhired. */
76 for (int i = EMPL_SOLDIER; i < MAX_EMPL; i++) {
77 const employeeType_t type = (employeeType_t)i;
78 TR_ForeachEmployee(employee, transfer, type) {
79 employee->baseHired = transfer->srcBase; /* Restore back the original baseid. */
80 employee->transfer = false;
81 employee->unhire();
82 }
83 }
84 } else {
85 for (int i = EMPL_SOLDIER; i < MAX_EMPL; i++) {
86 const employeeType_t type = (employeeType_t)i;
87 TR_ForeachEmployee(employee, transfer, type) {
88 employee->baseHired = transfer->srcBase; /* Restore back the original baseid. */
89 employee->transfer = false;
90 employee->unhire();
91 E_HireEmployee(destination, employee);
92 }
93 }
94 }
95 }
96
97 /* Aliens */
98 if (transfer->alienCargo != nullptr) {
99 if (success) {
100 if (!destination->alienContainment) {
101 Com_sprintf(cp_messageBuffer, sizeof(cp_messageBuffer), _("%s does not have Alien Containment, Aliens are removed!"), destination->name);
102 MSO_CheckAddNewMessage(NT_TRANSFER_LOST, _("Transport mission"), cp_messageBuffer, MSG_TRANSFERFINISHED);
103 } else {
104 linkedList_t* cargo = transfer->alienCargo->list();
105 LIST_Foreach(cargo, alienCargo_t, item) {
106 destination->alienContainment->add(item->teamDef, item->alive, item->dead);
107 }
108 cgi->LIST_Delete(&cargo);
109 }
110 }
111 delete transfer->alienCargo;
112 transfer->alienCargo = nullptr;
113 }
114
115 TR_ForeachAircraft(aircraft, transfer) {
116 if (success) {
117 VectorCopy(destination->pos, aircraft->pos);
118 aircraft->status = AIR_HOME;
119 if (!destination->aircraftCurrent)
120 destination->aircraftCurrent = aircraft;
121 } else {
122 AIR_DeleteAircraft(aircraft);
123 }
124 }
125 cgi->LIST_Delete(&transfer->aircraft);
126 }
127
128 /**
129 * @brief Ends the transfer.
130 * @param[in] transfer Pointer to transfer in ccs.transfers
131 */
TR_TransferEnd(transfer_t * transfer)132 static void TR_TransferEnd (transfer_t* transfer)
133 {
134 base_t* destination = transfer->destBase;
135 assert(destination);
136
137 if (!destination->founded) {
138 TR_EmptyTransferCargo(nullptr, transfer, false);
139 MSO_CheckAddNewMessage(NT_TRANSFER_LOST, _("Transport mission"), _("The destination base no longer exists! Transfer cargo was lost, personnel has been discharged."), MSG_TRANSFERFINISHED);
140 /** @todo what if source base is lost? we won't be able to unhire transfered employees. */
141 } else {
142 char message[256];
143 TR_EmptyTransferCargo(destination, transfer, true);
144 Com_sprintf(message, sizeof(message), _("Transport mission ended, unloading cargo in %s"), destination->name);
145 MSO_CheckAddNewMessage(NT_TRANSFER_COMPLETED_SUCCESS, _("Transport mission"), message, MSG_TRANSFERFINISHED);
146 }
147 cgi->LIST_Remove(&ccs.transfers, transfer);
148 }
149
TR_AddData(transferData_t * transferData,transferCargoType_t type,const void * data)150 bool TR_AddData (transferData_t* transferData, transferCargoType_t type, const void* data)
151 {
152 if (transferData->trCargoCountTmp >= MAX_CARGO)
153 return false;
154 TR_SetData(&transferData->cargo[transferData->trCargoCountTmp], type, data);
155 transferData->trCargoCountTmp++;
156
157 return true;
158 }
159
160 /**
161 * @brief Starts a transfer
162 * @param[in] srcBase start transfer from this base
163 * @param[in] transData Container holds transfer details
164 */
TR_TransferStart(base_t * srcBase,transferData_t & transData)165 transfer_t* TR_TransferStart (base_t* srcBase, transferData_t &transData)
166 {
167 transfer_t transfer;
168 float time;
169 int i;
170
171 if (!transData.transferBase || !srcBase) {
172 Com_Printf("TR_TransferStart: No base selected!\n");
173 return nullptr;
174 }
175
176 /* Initialize transfer. */
177 OBJZERO(transfer);
178 /* calculate time to go from 1 base to another : 1 day for one quarter of the globe*/
179 time = GetDistanceOnGlobe(transData.transferBase->pos, srcBase->pos) / 90.0f;
180 transfer.event.day = ccs.date.day + floor(time); /* add day */
181 time = (time - floor(time)) * SECONDS_PER_DAY; /* convert remaining time in second */
182 transfer.event.sec = ccs.date.sec + round(time);
183 /* check if event is not the following day */
184 if (transfer.event.sec > SECONDS_PER_DAY) {
185 transfer.event.sec -= SECONDS_PER_DAY;
186 transfer.event.day++;
187 }
188 transfer.destBase = transData.transferBase; /* Destination base. */
189 transfer.srcBase = srcBase; /* Source base. */
190
191 int count = 0;
192 for (i = 0; i < cgi->csi->numODs; i++) { /* Items. */
193 if (transData.trItemsTmp[i] > 0) {
194 transfer.hasItems = true;
195 transfer.itemAmount[i] = transData.trItemsTmp[i];
196 count++;
197 }
198 }
199 /* Note that the employee remains hired in source base during the transfer, that is
200 * it takes Living Quarters capacity, etc, but it cannot be used anywhere. */
201 for (i = 0; i < MAX_EMPL; i++) { /* Employees. */
202 LIST_Foreach(transData.trEmployeesTmp[i], Employee, employee) {
203 assert(employee->isHiredInBase(srcBase));
204
205 transfer.hasEmployees = true;
206 employee->unassign();
207 cgi->LIST_AddPointer(&transfer.employees[i], (void*) employee);
208 employee->transfer = true;
209 count++;
210 }
211 }
212
213 /* Aliens. */
214 if (transData.alienCargo != nullptr) {
215 transfer.alienCargo = new AlienCargo(*transData.alienCargo);
216 count += transData.alienCargo->getAlive();
217 count += transData.alienCargo->getDead();
218 }
219
220 /* Aircraft */
221 LIST_Foreach(transData.aircraft, aircraft_t, aircraft) {
222 const baseCapacities_t capacity = AIR_GetCapacityByAircraftWeight(aircraft);
223 aircraft->status = AIR_TRANSFER;
224 AIR_RemoveEmployees(*aircraft);
225 aircraft->homebase = transData.transferBase;
226 transfer.hasAircraft = true;
227 cgi->LIST_AddPointer(&transfer.aircraft, (void*)aircraft);
228 if (srcBase->aircraftCurrent == aircraft)
229 srcBase->aircraftCurrent = AIR_GetFirstFromBase(srcBase);
230 CAP_AddCurrent(srcBase, capacity, -1);
231
232 /* This should happen in TR_EmptyTransferCargo but on loading capacities are
233 calculated based on aircraft->homebase. aircraft->homebase cannot be null yet
234 there are hidden tests on that. */
235 CAP_AddCurrent(transData.transferBase, capacity, 1);
236
237 count++;
238 }
239
240 /* don't start empty transfer */
241 if (count == 0)
242 return nullptr;
243
244 /* Recheck if production/research can be done on srcbase (if there are workers/scientists) */
245 PR_ProductionAllowed(srcBase);
246 RS_ResearchAllowed(srcBase);
247
248 return &LIST_Add(&ccs.transfers, transfer);
249 }
250
251 /**
252 * @brief Notify that an aircraft has been removed.
253 * @param[in] aircraft Aircraft that was removed from the game
254 * @sa AIR_DeleteAircraft
255 */
TR_NotifyAircraftRemoved(const aircraft_t * aircraft)256 void TR_NotifyAircraftRemoved (const aircraft_t* aircraft)
257 {
258 if (!aircraft)
259 return;
260
261 TR_Foreach(transfer) {
262 if (!transfer->hasAircraft)
263 continue;
264 if (cgi->LIST_Remove(&transfer->aircraft, aircraft))
265 return;
266 }
267 }
268
269 /**
270 * @brief Checks whether given transfer should be processed.
271 * @sa CP_CampaignRun
272 */
TR_TransferRun(void)273 void TR_TransferRun (void)
274 {
275 TR_Foreach(transfer) {
276 if (Date_IsDue(&transfer->event)) {
277 assert(transfer->destBase);
278 TR_TransferEnd(transfer);
279 return;
280 }
281 }
282 }
283
284 #ifdef DEBUG
285 /**
286 * @brief Lists an/all active transfer(s)
287 */
TR_ListTransfers_f(void)288 static void TR_ListTransfers_f (void)
289 {
290 int transIdx = -1;
291 int i = 0;
292
293 if (cgi->Cmd_Argc() == 2) {
294 transIdx = atoi(cgi->Cmd_Argv(1));
295 if (transIdx < 0 || transIdx > cgi->LIST_Count(ccs.transfers)) {
296 Com_Printf("Usage: %s [transferIDX]\nWithout parameter it lists all.\n", cgi->Cmd_Argv(0));
297 return;
298 }
299 }
300
301 TR_Foreach(transfer) {
302 dateLong_t date;
303 i++;
304
305 if (transIdx >= 0 && i != transIdx)
306 continue;
307
308 /* @todo: we need a strftime feature to make this easier */
309 CP_DateConvertLong(&transfer->event, &date);
310
311 Com_Printf("Transfer #%d\n", i);
312 Com_Printf("...From %d (%s) To %d (%s) Arrival: %04i-%02i-%02i %02i:%02i:%02i\n",
313 (transfer->srcBase) ? transfer->srcBase->idx : -1,
314 (transfer->srcBase) ? transfer->srcBase->name : "(null)",
315 (transfer->destBase) ? transfer->destBase->idx : -1,
316 (transfer->destBase) ? transfer->destBase->name : "(null)",
317 date.year, date.month, date.day, date.hour, date.min, date.sec);
318
319 /* ItemCargo */
320 if (transfer->hasItems) {
321 int j;
322 Com_Printf("...ItemCargo:\n");
323 for (j = 0; j < cgi->csi->numODs; j++) {
324 const objDef_t* od = INVSH_GetItemByIDX(j);
325 if (transfer->itemAmount[od->idx])
326 Com_Printf("......%s: %i\n", od->id, transfer->itemAmount[od->idx]);
327 }
328 }
329 /* Carried Employees */
330 if (transfer->hasEmployees) {
331 int i;
332
333 Com_Printf("...Carried Employee:\n");
334 for (i = EMPL_SOLDIER; i < MAX_EMPL; i++) {
335 const employeeType_t emplType = (employeeType_t)i;
336 TR_ForeachEmployee(employee, transfer, emplType) {
337 if (employee->getUGV()) {
338 /** @todo: improve ugv listing when they're implemented */
339 Com_Printf("......ugv: %s [ucn: %i]\n", employee->getUGV()->id, employee->chr.ucn);
340 } else {
341 Com_Printf("......%s (%s) / %s [ucn: %i]\n", employee->chr.name,
342 E_GetEmployeeString(employee->getType(), 1),
343 (employee->getNation()) ? employee->getNation()->id : "(nonation)",
344 employee->chr.ucn);
345 if (!employee->isHired())
346 Com_Printf("Warning: employee^ not hired!\n");
347 if (!employee->transfer)
348 Com_Printf("Warning: employee^ not marked as being transfered!\n");
349 }
350 }
351 }
352 }
353 /* AlienCargo */
354 if (transfer->alienCargo != nullptr) {
355 Com_Printf("...AlienCargo:\n");
356 linkedList_t* cargo = transfer->alienCargo->list();
357 LIST_Foreach(cargo, alienCargo_t, item) {
358 Com_Printf("......%s alive: %i dead: %i\n", item->teamDef->id, item->alive, item->dead);
359 }
360 cgi->LIST_Delete(&cargo);
361 }
362 /* Transfered Aircraft */
363 if (transfer->hasAircraft) {
364 Com_Printf("...Transfered Aircraft:\n");
365 TR_ForeachAircraft(aircraft, transfer) {
366 Com_Printf("......%s [idx: %i]\n", aircraft->id, aircraft->idx);
367 }
368 }
369 }
370 }
371 #endif
372
373 /**
374 * @brief Save callback for xml savegames
375 * @param[out] p XML Node structure, where we write the information to
376 * @sa TR_LoadXML
377 * @sa SAV_GameSaveXML
378 */
TR_SaveXML(xmlNode_t * p)379 bool TR_SaveXML (xmlNode_t* p)
380 {
381 xmlNode_t* n = cgi->XML_AddNode(p, SAVE_TRANSFER_TRANSFERS);
382
383 TR_Foreach(transfer) {
384 int j;
385 xmlNode_t* s;
386
387 s = cgi->XML_AddNode(n, SAVE_TRANSFER_TRANSFER);
388 cgi->XML_AddInt(s, SAVE_TRANSFER_DAY, transfer->event.day);
389 cgi->XML_AddInt(s, SAVE_TRANSFER_SEC, transfer->event.sec);
390 if (!transfer->destBase) {
391 Com_Printf("Could not save transfer, no destBase is set\n");
392 return false;
393 }
394 cgi->XML_AddInt(s, SAVE_TRANSFER_DESTBASE, transfer->destBase->idx);
395 /* scrBase can be nullptr if this is alien (mission->base) transport
396 * @sa TR_TransferAlienAfterMissionStart */
397 if (transfer->srcBase)
398 cgi->XML_AddInt(s, SAVE_TRANSFER_SRCBASE, transfer->srcBase->idx);
399 /* save items */
400 if (transfer->hasItems) {
401 for (j = 0; j < MAX_OBJDEFS; j++) {
402 if (transfer->itemAmount[j] > 0) {
403 const objDef_t* od = INVSH_GetItemByIDX(j);
404 xmlNode_t* ss = cgi->XML_AddNode(s, SAVE_TRANSFER_ITEM);
405
406 assert(od);
407 cgi->XML_AddString(ss, SAVE_TRANSFER_ITEMID, od->id);
408 cgi->XML_AddInt(ss, SAVE_TRANSFER_AMOUNT, transfer->itemAmount[j]);
409 }
410 }
411 }
412 /* save aliens */
413 if (transfer->alienCargo != nullptr) {
414 xmlNode_t* alienNode = cgi->XML_AddNode(s, SAVE_TRANSFER_ALIENCARGO);
415 if (!alienNode)
416 return false;
417 transfer->alienCargo->save(alienNode);
418 }
419 /* save employee */
420 if (transfer->hasEmployees) {
421 for (j = 0; j < MAX_EMPL; j++) {
422 TR_ForeachEmployee(employee, transfer, j) {
423 xmlNode_t* ss = cgi->XML_AddNode(s, SAVE_TRANSFER_EMPLOYEE);
424 cgi->XML_AddInt(ss, SAVE_TRANSFER_UCN, employee->chr.ucn);
425 }
426 }
427 }
428 /* save aircraft */
429 if (transfer->hasAircraft) {
430 TR_ForeachAircraft(aircraft, transfer) {
431 xmlNode_t* ss = cgi->XML_AddNode(s, SAVE_TRANSFER_AIRCRAFT);
432 cgi->XML_AddInt(ss, SAVE_TRANSFER_ID, aircraft->idx);
433 }
434 }
435 }
436 return true;
437 }
438
439 /**
440 * @brief Load callback for xml savegames
441 * @param[in] p XML Node structure, where we get the information from
442 * @sa TR_SaveXML
443 * @sa SAV_GameLoadXML
444 */
TR_LoadXML(xmlNode_t * p)445 bool TR_LoadXML (xmlNode_t* p)
446 {
447 xmlNode_t* n, *s;
448
449 n = cgi->XML_GetNode(p, SAVE_TRANSFER_TRANSFERS);
450 if (!n)
451 return false;
452
453 assert(B_AtLeastOneExists());
454
455 for (s = cgi->XML_GetNode(n, SAVE_TRANSFER_TRANSFER); s; s = cgi->XML_GetNextNode(s, n, SAVE_TRANSFER_TRANSFER)) {
456 xmlNode_t* ss;
457 transfer_t transfer;
458
459 OBJZERO(transfer);
460
461 transfer.destBase = B_GetBaseByIDX(cgi->XML_GetInt(s, SAVE_TRANSFER_DESTBASE, BYTES_NONE));
462 if (!transfer.destBase) {
463 Com_Printf("Error: Transfer has no destBase set\n");
464 return false;
465 }
466 transfer.srcBase = B_GetBaseByIDX(cgi->XML_GetInt(s, SAVE_TRANSFER_SRCBASE, BYTES_NONE));
467
468 transfer.event.day = cgi->XML_GetInt(s, SAVE_TRANSFER_DAY, 0);
469 transfer.event.sec = cgi->XML_GetInt(s, SAVE_TRANSFER_SEC, 0);
470
471 /* Initializing some variables */
472 transfer.hasItems = false;
473 transfer.hasEmployees = false;
474 transfer.hasAircraft = false;
475
476 /* load items */
477 /* If there is at last one element, hasItems is true */
478 ss = cgi->XML_GetNode(s, SAVE_TRANSFER_ITEM);
479 if (ss) {
480 transfer.hasItems = true;
481 for (; ss; ss = cgi->XML_GetNextNode(ss, s, SAVE_TRANSFER_ITEM)) {
482 const char* itemId = cgi->XML_GetString(ss, SAVE_TRANSFER_ITEMID);
483 const objDef_t* od = INVSH_GetItemByID(itemId);
484
485 if (od)
486 transfer.itemAmount[od->idx] = cgi->XML_GetInt(ss, SAVE_TRANSFER_AMOUNT, 1);
487 }
488 }
489 /* load aliens */
490 ss = cgi->XML_GetNode(s, SAVE_TRANSFER_ALIENCARGO);
491 if (ss) {
492 transfer.alienCargo = new AlienCargo();
493 if (transfer.alienCargo == nullptr)
494 cgi->Com_Error(ERR_DROP, "TR_LoadXML: Cannot create AlienCargo object\n");
495 transfer.alienCargo->load(ss);
496 } else {
497 /** @todo Remove: Fallback for compatibility */
498 ss = cgi->XML_GetNode(s, SAVE_TRANSFER_ALIEN);
499 if (ss) {
500 transfer.alienCargo = new AlienCargo();
501 if (transfer.alienCargo == nullptr)
502 cgi->Com_Error(ERR_DROP, "TR_LoadXML: Cannot create AlienCargo object\n");
503 for (; ss; ss = cgi->XML_GetNextNode(ss, s, SAVE_TRANSFER_ALIEN)) {
504 const int alive = cgi->XML_GetInt(ss, SAVE_TRANSFER_ALIVEAMOUNT, 0);
505 const int dead = cgi->XML_GetInt(ss, SAVE_TRANSFER_DEADAMOUNT, 0);
506 const char* id = cgi->XML_GetString(ss, SAVE_TRANSFER_ALIENID);
507 if (!transfer.alienCargo->add(id, alive, dead))
508 Com_Printf("TR_LoadXML: Cannot add aliens to cargo: %s, alive: %d, dead: %d\n", id, alive, dead);
509 }
510 }
511 }
512 /* load employee */
513 ss = cgi->XML_GetNode(s, SAVE_TRANSFER_EMPLOYEE);
514 if (ss) {
515 transfer.hasEmployees = true;
516 for (; ss; ss = cgi->XML_GetNextNode(ss, s, SAVE_TRANSFER_EMPLOYEE)) {
517 const int ucn = cgi->XML_GetInt(ss, SAVE_TRANSFER_UCN, -1);
518 Employee* empl = E_GetEmployeeFromChrUCN(ucn);
519
520 if (!empl) {
521 Com_Printf("Error: No employee found with UCN: %i\n", ucn);
522 return false;
523 }
524
525 cgi->LIST_AddPointer(&transfer.employees[empl->getType()], (void*) empl);
526 empl->transfer = true;
527 }
528 }
529 /* load aircraft */
530 ss = cgi->XML_GetNode(s, SAVE_TRANSFER_AIRCRAFT);
531 if (ss) {
532 transfer.hasAircraft = true;
533 for (; ss; ss = cgi->XML_GetNextNode(ss, s, SAVE_TRANSFER_AIRCRAFT)) {
534 const int j = cgi->XML_GetInt(ss, SAVE_TRANSFER_ID, -1);
535 aircraft_t* aircraft = AIR_AircraftGetFromIDX(j);
536
537 if (aircraft)
538 cgi->LIST_AddPointer(&transfer.aircraft, (void*)aircraft);
539 }
540 }
541 LIST_Add(&ccs.transfers, transfer);
542 }
543
544 return true;
545 }
546
547 /**
548 * @brief Defines commands and cvars for the Transfer menu(s).
549 * @sa UI_InitStartup
550 */
TR_InitStartup(void)551 void TR_InitStartup (void)
552 {
553 TR_InitCallbacks();
554 #ifdef DEBUG
555 cgi->Cmd_AddCommand("debug_listtransfers", TR_ListTransfers_f, "Lists an/all active transfer(s)");
556 #endif
557 }
558
559 /**
560 * @brief Closing actions for transfer-subsystem
561 */
TR_Shutdown(void)562 void TR_Shutdown (void)
563 {
564 TR_Foreach(transfer) {
565 int i;
566
567 if (transfer->alienCargo != nullptr) {
568 delete transfer->alienCargo;
569 transfer->alienCargo = nullptr;
570 }
571 cgi->LIST_Delete(&transfer->aircraft);
572 for (i = EMPL_SOLDIER; i < MAX_EMPL; i++) {
573 cgi->LIST_Delete(&transfer->employees[i]);
574 }
575 }
576
577 TR_ShutdownCallbacks();
578 #ifdef DEBUG
579 cgi->Cmd_RemoveCommand("debug_listtransfers");
580 #endif
581 }
582