/** * @file * @brief Most of the aircraft related stuff. * @sa cl_airfight.c * @note Aircraft management functions prefix: AIR_ * @note Aircraft menu(s) functions prefix: AIM_ * @note Aircraft equipment handling functions prefix: AII_ */ /* Copyright (C) 2002-2013 UFO: Alien Invasion. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "../../cl_shared.h" #include "../../ui/ui_dataids.h" #include "../../../shared/parse.h" #include "cp_campaign.h" #include "cp_mapfightequip.h" #include "cp_geoscape.h" #include "cp_ufo.h" #include "cp_alienbase.h" #include "cp_time.h" #include "cp_missions.h" #include "cp_aircraft_callbacks.h" #include "save/save_aircraft.h" #include "cp_popup.h" #include "aliencargo.h" /** * @brief Iterates through the aircraft of a base * @param[in] b The base to get the craft from */ aircraft_t* AIR_GetFirstFromBase (const base_t* b) { if (b) { AIR_ForeachFromBase(aircraft, b) return aircraft; } return nullptr; } /** * @brief Checks whether there is any aircraft assigned to the given base * @param[in] base The base to check * @return @c true if there is at least one aircraft, @c false otherwise (or when the @c base pointer is @c nullptr) */ bool AIR_BaseHasAircraft (const base_t* base) { return base != nullptr && AIR_GetFirstFromBase(base) != nullptr; } /** * @brief Returns the number of aircraft on the given base * @param[in] base The base to check */ int AIR_BaseCountAircraft (const base_t* base) { int count = 0; AIR_ForeachFromBase(aircraft, base) { count++; } return count; } #ifdef DEBUG /** * @brief Debug function which lists all aircraft in all bases. * @note Use with baseIdx as a parameter to display all aircraft in given base. * @note called with debug_listaircraft */ void AIR_ListAircraft_f (void) { base_t* base = nullptr; if (cgi->Cmd_Argc() == 2) { int baseIdx = atoi(cgi->Cmd_Argv(1)); base = B_GetFoundedBaseByIDX(baseIdx); } AIR_Foreach(aircraft) { int k; if (base && aircraft->homebase != base) continue; Com_Printf("Aircraft %s\n", aircraft->name); Com_Printf("...idx global %i\n", aircraft->idx); Com_Printf("...homebase: %s\n", aircraft->homebase ? aircraft->homebase->name : "NO HOMEBASE"); for (k = 0; k < aircraft->maxWeapons; k++) { aircraftSlot_t* slot = &aircraft->weapons[k]; if (slot->item) { Com_Printf("...weapon slot %i contains %s", k, slot->item->id); if (!slot->installationTime) { Com_Printf(" (functional)\n"); } else if (slot->installationTime > 0) { Com_Printf(" (%i hours before installation is finished)\n", slot->installationTime); } else { Com_Printf(" (%i hours before removing is finished)\n", slot->installationTime); } if (slot->ammo) { if (slot->ammoLeft > 1) { Com_Printf("......this weapon is loaded with ammo %s\n", slot->ammo->id); } else { Com_Printf("......no more ammo (%s)\n", slot->ammo->id); } } else { Com_Printf("......this weapon isn't loaded with ammo\n"); } } else { Com_Printf("...weapon slot %i is empty\n", k); } } if (aircraft->shield.item) { Com_Printf("...armour slot contains %s", aircraft->shield.item->id); if (!aircraft->shield.installationTime) { Com_Printf(" (functional)\n"); } else if (aircraft->shield.installationTime > 0) { Com_Printf(" (%i hours before installation is finished)\n", aircraft->shield.installationTime); } else { Com_Printf(" (%i hours before removing is finished)\n", aircraft->shield.installationTime); } } else { Com_Printf("...armour slot is empty\n"); } for (k = 0; k < aircraft->maxElectronics; k++) { aircraftSlot_t* slot = &aircraft->weapons[k]; if (slot->item) { Com_Printf("...electronics slot %i contains %s", k, slot->item->id); if (!slot->installationTime) { Com_Printf(" (functional)\n"); } else if (slot->installationTime > 0) { Com_Printf(" (%i hours before installation is finished)\n", slot->installationTime); } else { Com_Printf(" (%i hours before removing is finished)\n", slot->installationTime); } } else { Com_Printf("...electronics slot %i is empty\n", k); } } if (aircraft->pilot) { character_t* chr = &aircraft->pilot->chr; Com_Printf("...pilot: ucn: %i name: %s\n", chr->ucn, chr->name); } else { Com_Printf("...no pilot assigned\n"); } Com_Printf("...damage: %i\n", aircraft->damage); Com_Printf("...stats: "); for (k = 0; k < AIR_STATS_MAX; k++) { if (k == AIR_STATS_WRANGE) { Com_Printf("%.2f ", aircraft->stats[k] / 1000.0f); } else { Com_Printf("%i ", aircraft->stats[k]); } } Com_Printf("\n"); Com_Printf("...name %s\n", aircraft->id); Com_Printf("...type %i\n", aircraft->type); Com_Printf("...size %i\n", aircraft->maxTeamSize); Com_Printf("...fuel %i\n", aircraft->fuel); Com_Printf("...status %s\n", (aircraft->status == AIR_CRASHED) ? "crashed" : AIR_AircraftStatusToName(aircraft)); Com_Printf("...pos %.0f:%.0f\n", aircraft->pos[0], aircraft->pos[1]); Com_Printf("...team: (%i/%i)\n", cgi->LIST_Count(aircraft->acTeam), aircraft->maxTeamSize); LIST_Foreach(aircraft->acTeam, Employee, employee) { character_t* chr = &employee->chr; Com_Printf(".........name: %s (ucn: %i)\n", chr->name, chr->ucn); } if (aircraft->alienCargo) { Com_Printf("...alienCargo:\n"); linkedList_t* cargo = aircraft->alienCargo->list(); LIST_Foreach(cargo, alienCargo_t, item) { Com_Printf("......team: %s alive: %d dead: %d\n", item->teamDef->id, item->alive, item->dead); } cgi->LIST_Delete(&cargo); } } } #endif static equipDef_t eTempEq; /**< Used to count ammo in magazines. */ /** * @brief Count and collect ammo from gun magazine. * @param[in] data Pointer to aircraft used in this mission. * @param[in] magazine Pointer to Item being magazine. */ static void AII_CollectingAmmo (void* data, const Item* magazine) { aircraft_t* aircraft = (aircraft_t*)data; /* Let's add remaining ammo to market. */ eTempEq.numItemsLoose[magazine->ammoDef()->idx] += magazine->getAmmoLeft(); if (eTempEq.numItemsLoose[magazine->ammoDef()->idx] >= magazine->def()->ammo) { /* There are more or equal ammo on the market than magazine needs - collect magazine. */ eTempEq.numItemsLoose[magazine->ammoDef()->idx] -= magazine->def()->ammo; AII_CollectItem(aircraft, magazine->ammoDef(), 1); } } /** * @brief Add an item to aircraft inventory. * @param[in,out] aircraft Aircraft to load to * @param[in] item Item to add * @param amount Number of items to add * @sa AL_AddAliens * @sa AII_CollectingItems */ void AII_CollectItem (aircraft_t* aircraft, const objDef_t* item, int amount) { int i; itemsTmp_t* cargo = aircraft->itemcargo; for (i = 0; i < aircraft->itemTypes; i++) { if (cargo[i].item == item) { Com_DPrintf(DEBUG_CLIENT, "AII_CollectItem: collecting %s (%i) amount %i -> %i\n", item->name, item->idx, cargo[i].amount, cargo[i].amount + amount); cargo[i].amount += amount; return; } } if (aircraft->itemTypes >= MAX_CARGO) { Com_Printf("AII_CollectItem: Cannot add item to cargobay it's full!\n"); return; } Com_DPrintf(DEBUG_CLIENT, "AII_CollectItem: adding %s (%i) amount %i\n", item->name, item->idx, amount); cargo[aircraft->itemTypes].item = item; cargo[aircraft->itemTypes].amount = amount; aircraft->itemTypes++; } static inline void AII_CollectItem_ (void* data, const objDef_t* item, int amount) { AII_CollectItem((aircraft_t*)data, item, amount); } /** * @brief Process items carried by soldiers. * @param[in] soldierInventory Pointer to inventory from a soldier of our team. * @sa AII_CollectingItems */ static void AII_CarriedItems (const Inventory* soldierInventory) { Item* item; equipDef_t* ed = &ccs.eMission; const Container* cont = nullptr; while ((cont = soldierInventory->getNextCont(cont))) { /* Items on the ground are collected as ET_ITEM */ for (item = cont->_invList; item; item = item->getNext()) { const objDef_t* itemType = item->def(); technology_t* tech = RS_GetTechForItem(itemType); ed->numItems[itemType->idx]++; RS_MarkCollected(tech); if (!itemType->isReloadable() || item->getAmmoLeft() == 0) continue; ed->addClip(item); /* The guys keep their weapons (half-)loaded. Auto-reload * will happen at equip screen or at the start of next mission, * but fully loaded weapons will not be reloaded even then. */ } } } /** * @brief Collect items from the battlefield. * @note The way we are collecting items: * @note 1. Grab everything from the floor and add it to the aircraft cargo here. * @note 2. When collecting gun, collect it's remaining ammo as well * @note 3. Sell everything or add to base storage when dropship lands in base. * @note 4. Items carried by soldiers are processed nothing is being sold. * @sa CL_ParseResults * @sa AII_CollectingAmmo * @sa AII_CarriedItems */ void AII_CollectingItems (aircraft_t* aircraft, int won) { int i, j; itemsTmp_t* cargo; itemsTmp_t prevItemCargo[MAX_CARGO]; int prevItemTypes; /* Save previous cargo */ memcpy(prevItemCargo, aircraft->itemcargo, sizeof(aircraft->itemcargo)); prevItemTypes = aircraft->itemTypes; /* Make sure itemcargo is empty. */ OBJZERO(aircraft->itemcargo); /* Make sure eTempEq is empty as well. */ OBJZERO(eTempEq); cargo = aircraft->itemcargo; aircraft->itemTypes = 0; cgi->CollectItems(aircraft, won, AII_CollectItem_, AII_CollectingAmmo, AII_CarriedItems); /* Fill the missionResults array. */ ccs.missionResults.itemTypes = aircraft->itemTypes; for (i = 0; i < aircraft->itemTypes; i++) ccs.missionResults.itemAmount += cargo[i].amount; #ifdef DEBUG /* Print all of collected items. */ for (i = 0; i < aircraft->itemTypes; i++) { if (cargo[i].amount > 0) Com_DPrintf(DEBUG_CLIENT, "Collected items: idx: %i name: %s amount: %i\n", cargo[i].item->idx, cargo[i].item->name, cargo[i].amount); } #endif /* Put previous cargo back */ for (i = 0; i < prevItemTypes; i++) { for (j = 0; j < aircraft->itemTypes; j++) { if (cargo[j].item == prevItemCargo[i].item) { cargo[j].amount += prevItemCargo[i].amount; break; } } if (j == aircraft->itemTypes) { cargo[j] = prevItemCargo[i]; aircraft->itemTypes++; } } } /** * @brief Translates the aircraft status id to a translatable string * @param[in] aircraft Aircraft to translate the status of * @return Translation string of given status. */ const char* AIR_AircraftStatusToName (const aircraft_t* aircraft) { assert(aircraft); assert(aircraft->homebase); /* display special status if base-attack affects aircraft */ if (B_IsUnderAttack(aircraft->homebase) && AIR_IsAircraftInBase(aircraft)) return _("ON RED ALERT"); switch (aircraft->status) { case AIR_NONE: return _("Nothing - should not be displayed"); case AIR_HOME: return _("at home base"); case AIR_REFUEL: return _("refuelling"); case AIR_IDLE: return _("idle"); case AIR_TRANSIT: return _("in transit"); case AIR_MISSION: return _("enroute to mission"); case AIR_UFO: return _("pursuing a UFO"); case AIR_DROP: return _("ready to drop soldiers"); case AIR_INTERCEPT: return _("intercepting a UFO"); case AIR_TRANSFER: return _("enroute to new home base"); case AIR_RETURNING: return _("returning to base"); case AIR_CRASHED: cgi->Com_Error(ERR_DROP, "AIR_CRASHED should not be visible anywhere"); } return nullptr; } /** * @brief Checks whether given aircraft is in its homebase. * @param[in] aircraft Pointer to an aircraft. * @return true if given aircraft is in its homebase. * @return false if given aircraft is not in its homebase. */ bool AIR_IsAircraftInBase (const aircraft_t* aircraft) { if (aircraft->status == AIR_HOME || aircraft->status == AIR_REFUEL) return true; return false; } /** * @brief Checks whether given aircraft is on geoscape. * @param[in] aircraft Pointer to an aircraft. * @note aircraft may be neither on geoscape, nor in base (like when it's transferred) * @return @c false if given aircraft is not on geoscape, @c true if given aircraft is on geoscape. * @sa UFO_IsUFOSeenOnGeoscape */ bool AIR_IsAircraftOnGeoscape (const aircraft_t* aircraft) { switch (aircraft->status) { case AIR_IDLE: case AIR_TRANSIT: case AIR_MISSION: case AIR_UFO: case AIR_DROP: case AIR_INTERCEPT: case AIR_RETURNING: return true; case AIR_NONE: case AIR_REFUEL: case AIR_HOME: case AIR_TRANSFER: case AIR_CRASHED: return false; } cgi->Com_Error(ERR_FATAL, "Unknown aircraft status %i", aircraft->status); } /** * @brief Calculates the amount of aircraft (of the given type) in the selected base * @param[in] base The base to count the aircraft in * @param[in] aircraftType The type of the aircraft you want * @note that this type is just transporter/interceptor/ufo category */ int AIR_CountTypeInBase (const base_t* base, aircraftType_t aircraftType) { int count = 0; AIR_ForeachFromBase(aircraft, base) { if (aircraft->type == aircraftType) count++; } return count; } /** * @brief Calculates the amount of aircraft (of the given type) in the selected base * @param[in] base The base to count the aircraft in * @param[in] aircraftTemplate The type of the aircraft you want */ int AIR_CountInBaseByTemplate (const base_t* base, const aircraft_t* aircraftTemplate) { int count = 0; AIR_ForeachFromBase(aircraft, base) { if (aircraft->tpl == aircraftTemplate) count++; } return count; } /** * @brief Returns the string that matches the given aircraft type */ const char* AIR_GetAircraftString (aircraftType_t aircraftType) { switch (aircraftType) { case AIRCRAFT_INTERCEPTOR: return _("Interceptor"); case AIRCRAFT_TRANSPORTER: return _("Transporter"); case AIRCRAFT_UFO: return _("UFO"); } return ""; } /** * @brief Some of the aircraft values needs special calculations when they * are shown in the menus * @sa UP_AircraftStatToName */ int AIR_AircraftMenuStatsValues (const int value, const int stat) { switch (stat) { case AIR_STATS_SPEED: case AIR_STATS_MAXSPEED: /* Convert into km/h, and round this value */ return 10 * (int) (111.2 * value / 10.0f); case AIR_STATS_FUELSIZE: return value / 1000; default: return value; } } /** * @brief Calculates the range an aircraft can fly on the geoscape * @param aircraft The aircraft to calculate the range for * @return The range */ int AIR_GetOperationRange (const aircraft_t* aircraft) { const int range = aircraft->stats[AIR_STATS_SPEED] * aircraft->stats[AIR_STATS_FUELSIZE]; /* the 2.0f factor is for going to destination and then come back */ return 100 * (int) (KILOMETER_PER_DEGREE * range / (2.0f * (float)SECONDS_PER_HOUR * 100.0f)); } /** * @brief Calculates the remaining range the aircraft can fly * @param aircraft The aircraft to calculate the remaining range for * @return The remaining range */ int AIR_GetRemainingRange (const aircraft_t* aircraft) { return aircraft->stats[AIR_STATS_SPEED] * aircraft->fuel; } /** * @brief check if aircraft has enough fuel to go to destination, and then come back home * @param[in] aircraft Pointer to the aircraft * @param[in] destination Pointer to the position the aircraft should go to * @sa GEO_CalcLine * @return true if the aircraft can go to the position, false else */ bool AIR_AircraftHasEnoughFuel (const aircraft_t* aircraft, const vec2_t destination) { const base_t* base; float distance; assert(aircraft); base = aircraft->homebase; assert(base); /* Calculate the line that the aircraft should follow to go to destination */ distance = GetDistanceOnGlobe(aircraft->pos, destination); /* Calculate the line that the aircraft should then follow to go back home */ distance += GetDistanceOnGlobe(destination, base->pos); /* Check if the aircraft has enough fuel to go to destination and then go back home */ return (distance <= AIR_GetRemainingRange(aircraft) / (float)SECONDS_PER_HOUR); } /** * @brief check if aircraft has enough fuel to go to destination * @param[in] aircraft Pointer to the aircraft * @param[in] destination Pointer to the position the aircraft should go to * @sa GEO_CalcLine * @return true if the aircraft can go to the position, false else */ bool AIR_AircraftHasEnoughFuelOneWay (const aircraft_t* aircraft, const vec2_t destination) { float distance; assert(aircraft); /* Calculate the line that the aircraft should follow to go to destination */ distance = GetDistanceOnGlobe(aircraft->pos, destination); /* Check if the aircraft has enough fuel to go to destination */ return (distance <= AIR_GetRemainingRange(aircraft) / (float)SECONDS_PER_HOUR); } /** * @brief Calculates the way back to homebase for given aircraft and returns it. * @param[in] aircraft Pointer to aircraft, which should return to base. * @note Command to call this: "aircraft_return". */ void AIR_AircraftReturnToBase (aircraft_t* aircraft) { if (aircraft && AIR_IsAircraftOnGeoscape(aircraft)) { const base_t* base = aircraft->homebase; GEO_CalcLine(aircraft->pos, base->pos, &aircraft->route); aircraft->status = AIR_RETURNING; aircraft->time = 0; aircraft->point = 0; aircraft->mission = nullptr; aircraft->aircraftTarget = nullptr; } } /** * @param base The base to get the aircraft from * @param index The index of the aircraft in the given base * @return @c nullptr if there is no such aircraft in the given base, or the aircraft pointer that belongs to the given index. * @todo Remove this! Aircraft no longer have local index per base */ aircraft_t* AIR_GetAircraftFromBaseByIDXSafe (const base_t* base, int index) { int i; i = 0; AIR_ForeachFromBase(aircraft, base) { if (index == i) return aircraft; i++; } return nullptr; } /** * @brief Searches the global array of aircraft types for a given aircraft. * @param[in] name Aircraft id. * @return aircraft_t pointer or nullptr if not found. * @note This function gives no warning on null name or if no aircraft found */ const aircraft_t* AIR_GetAircraftSilent (const char* name) { int i; if (!name) return nullptr; for (i = 0; i < ccs.numAircraftTemplates; i++) { const aircraft_t* aircraftTemplate = &ccs.aircraftTemplates[i]; if (Q_streq(aircraftTemplate->id, name)) return aircraftTemplate; } return nullptr; } /** * @brief Searches the global array of aircraft types for a given aircraft. * @param[in] name Aircraft id. * @return aircraft_t pointer or errors out (ERR_DROP) */ const aircraft_t* AIR_GetAircraft (const char* name) { const aircraft_t* aircraft = AIR_GetAircraftSilent(name); if (Q_strnull(name)) cgi->Com_Error(ERR_DROP, "AIR_GetAircraft called with invaliad name!"); else if (aircraft == nullptr) cgi->Com_Error(ERR_DROP, "Aircraft '%s' not found", name); return aircraft; } /** * @brief Initialise aircraft pointer in each slot of an aircraft. * @param[in] aircraft Pointer to the aircraft where slots are. */ static void AII_SetAircraftInSlots (aircraft_t* aircraft) { int i; /* initialise weapon slots */ for (i = 0; i < MAX_AIRCRAFTSLOT; i++) { aircraft->weapons[i].aircraft = aircraft; aircraft->electronics[i].aircraft = aircraft; } aircraft->shield.aircraft = aircraft; } /** * @brief Adds a new aircraft from a given aircraft template to the base and sets the homebase for the new aircraft * to the given base * @param[out] base The base to add the aircraft to * @param[in] aircraftTemplate The template to create the new aircraft from * @return the newly added aircraft * @sa AIR_Delete */ aircraft_t* AIR_Add (base_t* base, const aircraft_t* aircraftTemplate) { const baseCapacities_t capType = AIR_GetCapacityByAircraftWeight(aircraftTemplate); aircraft_t& aircraft = LIST_Add(&ccs.aircraft, *aircraftTemplate); aircraft.homebase = base; if (base && capType != MAX_CAP && aircraft.status != AIR_CRASHED) CAP_AddCurrent(base, capType, 1); return &aircraft; } /** * @brief Will remove the given aircraft from the base * @param[out] base The base to remove the aircraft from * @param aircraft The aircraft to remove * @return @c true if the aircraft was removed, @c false otherwise * @sa AIR_Add */ bool AIR_Delete (base_t* base, const aircraft_t* aircraft) { const baseCapacities_t capType = AIR_GetCapacityByAircraftWeight(aircraft); const bool crashed = (aircraft->status == AIR_CRASHED); if (cgi->LIST_Remove(&ccs.aircraft, (const void*)aircraft)) { if (base && capType != MAX_CAP && !crashed) CAP_AddCurrent(base, capType, -1); return true; } return false; } /** * @brief Places a new aircraft in the given base. * @param[in] base Pointer to base where aircraft should be added. * @param[in] aircraftTemplate The aircraft template to create the new aircraft from. * @sa B_Load */ aircraft_t* AIR_NewAircraft (base_t* base, const aircraft_t* aircraftTemplate) { /* copy generic aircraft description to individual aircraft in base * we do this because every aircraft can have its own parameters * now lets use the aircraft array for the base to set some parameters */ aircraft_t* aircraft = AIR_Add(base, aircraftTemplate); aircraft->idx = ccs.campaignStats.aircraftHad++; /**< set a unique index to this aircraft. */ aircraft->homebase = base; /* Update the values of its stats */ AII_UpdateAircraftStats(aircraft); /* initialise aircraft pointer in slots */ AII_SetAircraftInSlots(aircraft); /* Set HP to maximum */ aircraft->damage = aircraft->stats[AIR_STATS_DAMAGE]; /* Set Default Name */ Q_strncpyz(aircraft->name, _(aircraft->defaultName), lengthof(aircraft->name)); /* set initial direction of the aircraft */ VectorSet(aircraft->direction, 1, 0, 0); AIR_ResetAircraftTeam(aircraft); Com_sprintf(cp_messageBuffer, sizeof(cp_messageBuffer), _("A new %s is ready in %s"), _(aircraft->tpl->name), base->name); MS_AddNewMessage(_("Notice"), cp_messageBuffer); Com_DPrintf(DEBUG_CLIENT, "Setting aircraft to pos: %.0f:%.0f\n", base->pos[0], base->pos[1]); Vector2Copy(base->pos, aircraft->pos); RADAR_Initialise(&aircraft->radar, aircraftTemplate->radar.range, aircraftTemplate->radar.trackingRange, 1.0f, false); aircraft->radar.ufoDetectionProbability = aircraftTemplate->radar.ufoDetectionProbability; /* Update base capacities. */ Com_DPrintf(DEBUG_CLIENT, "idx_sample: %i name: %s weight: %i\n", aircraft->tpl->idx, aircraft->id, aircraft->size); Com_DPrintf(DEBUG_CLIENT, "Adding new aircraft %s with IDX %i for %s\n", aircraft->id, aircraft->idx, base->name); if (!base->aircraftCurrent) base->aircraftCurrent = aircraft; /* also update the base menu buttons */ cgi->Cmd_ExecuteString("base_init"); return aircraft; } /** * @brief Returns capacity type needed for an aircraft * @param[in] aircraft Aircraft to check */ baseCapacities_t AIR_GetCapacityByAircraftWeight (const aircraft_t* aircraft) { if (!aircraft) return MAX_CAP; switch (aircraft->size) { case AIRCRAFT_SMALL: return CAP_AIRCRAFT_SMALL; case AIRCRAFT_LARGE: return CAP_AIRCRAFT_BIG; } return MAX_CAP; } /** * @brief Calculate used storage room corresponding to items in an aircraft. * @param[in] aircraft Pointer to the aircraft. */ static int AIR_GetStorageRoom (const aircraft_t* aircraft) { int size = 0; LIST_Foreach(aircraft->acTeam, Employee, employee) { const Container* cont = nullptr; while ((cont = employee->chr.inv.getNextCont(cont, true))) { Item* item = nullptr; while ((item = cont->getNextItem(item))) { const objDef_t* obj = item->def(); size += obj->size; obj = item->ammoDef(); if (obj) size += obj->size; } } } return size; } /** * @brief Checks if destination base can store an aircraft and its team * @param[in] aircraft Pointer to the aircraft to check * @param[in] base Pointer to the base to store aircraft */ const char* AIR_CheckMoveIntoNewHomebase (const aircraft_t* aircraft, const base_t* base) { const baseCapacities_t capacity = AIR_GetCapacityByAircraftWeight(aircraft); if (!B_GetBuildingStatus(base, B_GetBuildingTypeByCapacity(capacity))) return _("No operational hangars at that base."); /* not enough capacity */ if (CAP_GetFreeCapacity(base, capacity) <= 0) return _("No free hangars at that base."); if (CAP_GetFreeCapacity(base, CAP_EMPLOYEES) < AIR_GetTeamSize(aircraft) + (AIR_GetPilot(aircraft) ? 1 : 0)) return _("Insufficient free crew quarter space at that base."); if (aircraft->maxTeamSize && CAP_GetFreeCapacity(base, CAP_ITEMS) < AIR_GetStorageRoom(aircraft)) return _("Insufficient storage space at that base."); /* check aircraft fuel, because the aircraft has to travel to the new base */ if (!AIR_AircraftHasEnoughFuelOneWay(aircraft, base->pos)) return _("That base is beyond this aircraft's range."); return nullptr; } /** * @brief Transfer items carried by a soldier from one base to another. * @param[in] chr Pointer to the character. * @param[in] sourceBase Base where employee comes from. * @param[in] destBase Base where employee is going. */ static void AIR_TransferItemsCarriedByCharacterToBase (character_t* chr, base_t* sourceBase, base_t* destBase) { const Container* cont = nullptr; while ((cont = chr->inv.getNextCont(cont, true))) { Item* item = nullptr; while ((item = cont->getNextItem(item))) { const objDef_t* obj = item->def(); B_AddToStorage(sourceBase, obj, -1); B_AddToStorage(destBase, obj, 1); obj = item->ammoDef(); if (obj) { B_AddToStorage(sourceBase, obj, -1); B_AddToStorage(destBase, obj, 1); } } } } /** * @brief Moves a given aircraft to a new base (also the employees and inventory) * @param[in] aircraft The aircraft to move into a new base * @param[in] base The base to move the aircraft into */ void AIR_MoveAircraftIntoNewHomebase (aircraft_t* aircraft, base_t* base) { base_t* oldBase; assert(aircraft); assert(base); assert(base != aircraft->homebase); Com_DPrintf(DEBUG_CLIENT, "AIR_MoveAircraftIntoNewHomebase: Change homebase of '%s' to '%s'\n", aircraft->id, base->name); oldBase = aircraft->homebase; assert(oldBase); /* Transfer employees */ E_MoveIntoNewBase(AIR_GetPilot(aircraft), base); LIST_Foreach(aircraft->acTeam, Employee, employee) { E_MoveIntoNewBase(employee, base); /* Transfer items carried by soldiers from oldBase to base */ AIR_TransferItemsCarriedByCharacterToBase(&employee->chr, oldBase, base); } /* Move aircraft to new base */ const baseCapacities_t capacity = AIR_GetCapacityByAircraftWeight(aircraft); CAP_AddCurrent(oldBase, capacity, -1); aircraft->homebase = base; CAP_AddCurrent(base, capacity, 1); if (oldBase->aircraftCurrent == aircraft) oldBase->aircraftCurrent = AIR_GetFirstFromBase(oldBase); if (!base->aircraftCurrent) base->aircraftCurrent = aircraft; /* No need to update global IDX of every aircraft: the global IDX of this aircraft did not change */ /* Redirect selectedAircraft */ GEO_SelectAircraft(aircraft); if (aircraft->status == AIR_RETURNING) { /* redirect to the new base */ AIR_AircraftReturnToBase(aircraft); } } /** * @brief Removes an aircraft from its base and the game. * @param[in] aircraft Pointer to aircraft that should be removed. * @note The assigned soldiers (if any) are unassinged from the aircraft - not fired. * @sa AIR_DestroyAircraft * @note If you want to do something different (kill, fire, etc...) do it before calling this function. * @todo Return status of deletion for better error handling. */ void AIR_DeleteAircraft (aircraft_t* aircraft) { int i; base_t* base; /* Check if aircraft is on geoscape while it's not destroyed yet */ const bool aircraftIsOnGeoscape = AIR_IsAircraftOnGeoscape(aircraft); assert(aircraft); base = aircraft->homebase; assert(base); GEO_NotifyAircraftRemoved(aircraft); TR_NotifyAircraftRemoved(aircraft); /* Remove pilot and all soldiers from the aircraft (the employees are still hired after this). */ AIR_RemoveEmployees(*aircraft); /* base is nullptr here because we don't want to readd this to the inventory * If you want this in the inventory you really have to call these functions * before you are destroying a craft */ for (i = 0; i < MAX_AIRCRAFTSLOT; i++) { AII_RemoveItemFromSlot(nullptr, aircraft->weapons, false); AII_RemoveItemFromSlot(nullptr, aircraft->electronics, false); } AII_RemoveItemFromSlot(nullptr, &aircraft->shield, false); if (base->aircraftCurrent == aircraft) base->aircraftCurrent = nullptr; if (aircraft->alienCargo != nullptr) { delete aircraft->alienCargo; aircraft->alienCargo = nullptr; } AIR_Delete(base, aircraft); if (!AIR_BaseHasAircraft(base)) { cgi->Cvar_Set("mn_aircraftstatus", ""); cgi->Cvar_Set("mn_aircraftinbase", "0"); cgi->Cvar_Set("mn_aircraftname", ""); cgi->Cvar_Set("mn_aircraft_model", ""); } else if (base->aircraftCurrent == nullptr) { base->aircraftCurrent = AIR_GetFirstFromBase(base); } /* also update the base menu buttons */ cgi->Cmd_ExecuteString("base_init"); /* Update Radar overlay */ if (aircraftIsOnGeoscape) RADAR_UpdateWholeRadarOverlay(); } /** * @brief Removes an aircraft from its base and the game. * @param[in] aircraft Pointer to aircraft that should be removed. * @param[in] killPilot Should pilot be removed from game or not? * @note aircraft and assigned soldiers (if any) are removed from game. * @todo Return status of deletion for better error handling. */ void AIR_DestroyAircraft (aircraft_t* aircraft, bool killPilot) { Employee* pilot; assert(aircraft); LIST_Foreach(aircraft->acTeam, Employee, employee) { E_RemoveInventoryFromStorage(employee); E_DeleteEmployee(employee); } /* the craft may no longer have any employees assigned */ /* remove the pilot */ pilot = AIR_GetPilot(aircraft); if (pilot) { if (killPilot) { if (E_DeleteEmployee(pilot)) AIR_SetPilot(aircraft, nullptr); else cgi->Com_Error(ERR_DROP, "AIR_DestroyAircraft: Could not remove pilot from game: %s (ucn: %i)\n", pilot->chr.name, pilot->chr.ucn); } else { AIR_SetPilot(aircraft, nullptr); } } else { if (aircraft->status != AIR_CRASHED) cgi->Com_Error(ERR_DROP, "AIR_DestroyAircraft: aircraft id %s had no pilot\n", aircraft->id); } AIR_DeleteAircraft(aircraft); } /** * @brief Moves given aircraft. * @param[in] dt time delta * @param[in] aircraft Pointer to aircraft on its way. * @return true if the aircraft reached its destination. */ bool AIR_AircraftMakeMove (int dt, aircraft_t* aircraft) { float dist; /* calc distance */ aircraft->time += dt; aircraft->fuel -= dt; dist = (float) aircraft->stats[AIR_STATS_SPEED] * aircraft->time / (float)SECONDS_PER_HOUR; /* Check if destination reached */ if (dist >= aircraft->route.distance * (aircraft->route.numPoints - 1)) { return true; } else { /* calc new position */ float frac = dist / aircraft->route.distance; const int p = (int) frac; frac -= p; aircraft->point = p; aircraft->pos[0] = (1 - frac) * aircraft->route.point[p][0] + frac * aircraft->route.point[p + 1][0]; aircraft->pos[1] = (1 - frac) * aircraft->route.point[p][1] + frac * aircraft->route.point[p + 1][1]; GEO_CheckPositionBoundaries(aircraft->pos); } dist = (float) aircraft->stats[AIR_STATS_SPEED] * (aircraft->time + dt) / (float)SECONDS_PER_HOUR; /* Now calculate the projected position. This is the position that the aircraft should get on * next frame if its route didn't change in meantime. */ if (dist >= aircraft->route.distance * (aircraft->route.numPoints - 1)) { VectorSet(aircraft->projectedPos, 0.0f, 0.0f, 0.0f); } else { /* calc new position */ float frac = dist / aircraft->route.distance; const int p = (int) frac; frac -= p; aircraft->projectedPos[0] = (1 - frac) * aircraft->route.point[p][0] + frac * aircraft->route.point[p + 1][0]; aircraft->projectedPos[1] = (1 - frac) * aircraft->route.point[p][1] + frac * aircraft->route.point[p + 1][1]; GEO_CheckPositionBoundaries(aircraft->projectedPos); } return false; } static void AIR_Move (aircraft_t* aircraft, int deltaTime) { /* Aircraft is moving */ if (AIR_AircraftMakeMove(deltaTime, aircraft)) { /* aircraft reach its destination */ const float* end = aircraft->route.point[aircraft->route.numPoints - 1]; Vector2Copy(end, aircraft->pos); GEO_CheckPositionBoundaries(aircraft->pos); switch (aircraft->status) { case AIR_MISSION: /* Aircraft reached its mission */ assert(aircraft->mission); aircraft->mission->active = true; aircraft->status = AIR_DROP; GEO_SetMissionAircraft(aircraft); GEO_SelectMission(aircraft->mission); GEO_SetInterceptorAircraft(aircraft); CP_GameTimeStop(); cgi->UI_PushWindow("popup_intercept_ready"); cgi->UI_ExecuteConfunc("pop_intready_aircraft \"%s\" \"%s\"", aircraft->name, MIS_GetName(aircraft->mission)); break; case AIR_RETURNING: /* aircraft entered in homebase */ aircraft->status = AIR_REFUEL; B_AircraftReturnedToHomeBase(aircraft); Com_sprintf(cp_messageBuffer, lengthof(cp_messageBuffer), _("Craft %s has returned to %s."), aircraft->name, aircraft->homebase->name); MSO_CheckAddNewMessage(NT_AIRCRAFT_ARRIVEDHOME, _("Notice"), cp_messageBuffer); break; case AIR_TRANSFER: case AIR_UFO: break; default: aircraft->status = AIR_IDLE; break; } } } static void AIR_Refuel (aircraft_t* aircraft, int deltaTime) { /* Aircraft is refuelling at base */ int fillup; if (aircraft->fuel < 0) aircraft->fuel = 0; /* amount of fuel we would like to load */ fillup = std::min(deltaTime * AIRCRAFT_REFUEL_FACTOR, aircraft->stats[AIR_STATS_FUELSIZE] - aircraft->fuel); /* This craft uses antimatter as fuel */ assert(aircraft->homebase); if (aircraft->stats[AIR_STATS_ANTIMATTER] > 0 && fillup > 0) { /* the antimatter we have */ const int amAvailable = B_ItemInBase(INVSH_GetItemByID(ANTIMATTER_TECH_ID), aircraft->homebase); /* current antimatter level in craft */ const int amCurrentLevel = aircraft->stats[AIR_STATS_ANTIMATTER] * (aircraft->fuel / (float) aircraft->stats[AIR_STATS_FUELSIZE]); /* next antimatter level in craft */ const int amNextLevel = aircraft->stats[AIR_STATS_ANTIMATTER] * ((aircraft->fuel + fillup) / (float) aircraft->stats[AIR_STATS_FUELSIZE]); /* antimatter needed */ int amLoad = amNextLevel - amCurrentLevel; if (amLoad > amAvailable) { /* amount of fuel we can load */ fillup = aircraft->stats[AIR_STATS_FUELSIZE] * ((amCurrentLevel + amAvailable) / (float) aircraft->stats[AIR_STATS_ANTIMATTER]) - aircraft->fuel; amLoad = amAvailable; if (!aircraft->notifySent[AIR_CANNOT_REFUEL]) { Com_sprintf(cp_messageBuffer, lengthof(cp_messageBuffer), _("Craft %s couldn't be completely refueled at %s. Not enough antimatter."), aircraft->name, aircraft->homebase->name); MSO_CheckAddNewMessage(NT_AIRCRAFT_CANNOTREFUEL, _("Notice"), cp_messageBuffer); aircraft->notifySent[AIR_CANNOT_REFUEL] = true; } } if (amLoad > 0) B_ManageAntimatter(aircraft->homebase, amLoad, false); } aircraft->fuel += fillup; if (aircraft->fuel >= aircraft->stats[AIR_STATS_FUELSIZE]) { aircraft->fuel = aircraft->stats[AIR_STATS_FUELSIZE]; aircraft->status = AIR_HOME; Com_sprintf(cp_messageBuffer, lengthof(cp_messageBuffer), _("Craft %s has refueled at %s."), aircraft->name, aircraft->homebase->name); MSO_CheckAddNewMessage(NT_AIRCRAFT_REFUELED, _("Notice"), cp_messageBuffer); aircraft->notifySent[AIR_CANNOT_REFUEL] = false; } } /** * @brief Handles aircraft movement and actions in geoscape mode * @sa CP_CampaignRun * @param[in] campaign The campaign data structure * @param[in] dt time delta (may be 0 if radar overlay should be updated but no aircraft moves) * @param[in] updateRadarOverlay True if radar overlay should be updated (not needed if geoscape isn't updated) */ void AIR_CampaignRun (const campaign_t* campaign, int dt, bool updateRadarOverlay) { /* true if at least one aircraft moved: radar overlay must be updated * This is static because aircraft can move without radar being * updated (sa CP_CampaignRun) */ static bool radarOverlayReset = false; /* Run each aircraft */ AIR_Foreach(aircraft) { int k; if (aircraft->status == AIR_CRASHED) continue; assert(aircraft->homebase); if (aircraft->status == AIR_IDLE) { /* Aircraft idle out of base */ aircraft->fuel -= dt; } else if (AIR_IsAircraftOnGeoscape(aircraft)) { AIR_Move(aircraft, dt); /* radar overlay should be updated */ radarOverlayReset = true; } else if (aircraft->status == AIR_REFUEL) { AIR_Refuel(aircraft, dt); } /* Check aircraft low fuel (only if aircraft is not already returning to base or in base) */ if (aircraft->status != AIR_RETURNING && AIR_IsAircraftOnGeoscape(aircraft) && !AIR_AircraftHasEnoughFuel(aircraft, aircraft->pos)) { /** @todo check if aircraft can go to a closer base with free space */ MS_AddNewMessage(_("Notice"), va(_("Craft %s is low on fuel and must return to base."), aircraft->name)); AIR_AircraftReturnToBase(aircraft); } /* Aircraft purchasing ufo */ if (aircraft->status == AIR_UFO) { /* Solve the fight */ AIRFIGHT_ExecuteActions(campaign, aircraft, aircraft->aircraftTarget); } for (k = 0; k < aircraft->maxWeapons; k++) { aircraftSlot_t* slot = &aircraft->weapons[k]; /* Update delay to launch next projectile */ if (AIR_IsAircraftOnGeoscape(aircraft) && slot->delayNextShot > 0) slot->delayNextShot -= dt; /* Reload if needed */ if (slot->ammoLeft <= 0) AII_ReloadWeapon(slot); } } if (updateRadarOverlay && radarOverlayReset && GEO_IsRadarOverlayActivated()) { RADAR_UpdateWholeRadarOverlay(); radarOverlayReset = false; } } /** * @brief Returns aircraft for a given global index. * @param[in] aircraftIdx Global aircraft index. * @return An aircraft pointer (to a struct in a base) that has the given index or nullptr if no aircraft found. */ aircraft_t* AIR_AircraftGetFromIDX (int aircraftIdx) { AIR_Foreach(aircraft) { if (aircraft->idx == aircraftIdx) { Com_DPrintf(DEBUG_CLIENT, "AIR_AircraftGetFromIDX: aircraft idx: %i\n", aircraft->idx); return aircraft; } } return nullptr; } /** * @brief Sends the specified aircraft to specified mission. * @param[in] aircraft Pointer to aircraft to send to mission. * @param[in] mission Pointer to given mission. * @return true if sending an aircraft to specified mission is possible. */ bool AIR_SendAircraftToMission (aircraft_t* aircraft, mission_t* mission) { if (!aircraft || !mission) return false; if (AIR_GetTeamSize(aircraft) == 0) { CP_Popup(_("Notice"), _("Assign one or more soldiers to this aircraft first.")); return false; } /* if aircraft was in base */ if (AIR_IsAircraftInBase(aircraft)) { /* reload its ammunition */ AII_ReloadAircraftWeapons(aircraft); } /* ensure interceptAircraft is set correctly */ GEO_SetInterceptorAircraft(aircraft); /* if mission is a base-attack and aircraft already in base, launch * mission immediately */ if (B_IsUnderAttack(aircraft->homebase) && AIR_IsAircraftInBase(aircraft)) { aircraft->mission = mission; mission->active = true; cgi->UI_PushWindow("popup_baseattack"); return true; } if (!AIR_AircraftHasEnoughFuel(aircraft, mission->pos)) { MS_AddNewMessage(_("Notice"), _("Insufficient fuel.")); return false; } GEO_CalcLine(aircraft->pos, mission->pos, &aircraft->route); aircraft->status = AIR_MISSION; aircraft->time = 0; aircraft->point = 0; aircraft->mission = mission; return true; } /** * @brief Initialise all values of an aircraft slot. * @param[in] aircraftTemplate Pointer to the aircraft which needs initalisation of its slots. */ static void AII_InitialiseAircraftSlots (aircraft_t* aircraftTemplate) { int i; /* initialise weapon slots */ for (i = 0; i < MAX_AIRCRAFTSLOT; i++) { AII_InitialiseSlot(aircraftTemplate->weapons + i, aircraftTemplate, nullptr, nullptr, AC_ITEM_WEAPON); AII_InitialiseSlot(aircraftTemplate->electronics + i, aircraftTemplate, nullptr, nullptr, AC_ITEM_ELECTRONICS); } AII_InitialiseSlot(&aircraftTemplate->shield, aircraftTemplate, nullptr, nullptr, AC_ITEM_SHIELD); } /** * @brief List of valid strings for itemPos_t * @note must be in the same order than @c itemPos_t */ static char const* const air_position_strings[] = { "nose_left", "nose_center", "nose_right", "wing_left", "wing_right", "rear_left", "rear_center", "rear_right" }; /** @brief Valid aircraft parameter definitions from script files. * @note wrange can't be parsed in aircraft definition: this is a property * of weapon */ static const value_t aircraft_param_vals[] = { {"speed", V_INT, offsetof(aircraft_t, stats[AIR_STATS_SPEED]), MEMBER_SIZEOF(aircraft_t, stats[0])}, {"maxspeed", V_INT, offsetof(aircraft_t, stats[AIR_STATS_MAXSPEED]), MEMBER_SIZEOF(aircraft_t, stats[0])}, {"shield", V_INT, offsetof(aircraft_t, stats[AIR_STATS_SHIELD]), MEMBER_SIZEOF(aircraft_t, stats[0])}, {"ecm", V_INT, offsetof(aircraft_t, stats[AIR_STATS_ECM]), MEMBER_SIZEOF(aircraft_t, stats[0])}, {"damage", V_INT, offsetof(aircraft_t, stats[AIR_STATS_DAMAGE]), MEMBER_SIZEOF(aircraft_t, stats[0])}, {"accuracy", V_INT, offsetof(aircraft_t, stats[AIR_STATS_ACCURACY]), MEMBER_SIZEOF(aircraft_t, stats[0])}, {"antimatter", V_INT, offsetof(aircraft_t, stats[AIR_STATS_ANTIMATTER]), MEMBER_SIZEOF(aircraft_t, stats[0])}, {nullptr, V_NULL, 0, 0} }; /** @brief Valid aircraft definition values from script files. */ static const value_t aircraft_vals[] = { {"name", V_STRING, offsetof(aircraft_t, name), 0}, {"defaultname", V_TRANSLATION_STRING, offsetof(aircraft_t, defaultName), 0}, {"numteam", V_INT, offsetof(aircraft_t, maxTeamSize), MEMBER_SIZEOF(aircraft_t, maxTeamSize)}, {"size", V_INT, offsetof(aircraft_t, size), MEMBER_SIZEOF(aircraft_t, size)}, {"nogeoscape", V_BOOL, offsetof(aircraft_t, notOnGeoscape), MEMBER_SIZEOF(aircraft_t, notOnGeoscape)}, {"interestlevel", V_INT, offsetof(aircraft_t, ufoInterestOnGeoscape), MEMBER_SIZEOF(aircraft_t, ufoInterestOnGeoscape)}, {"leader", V_BOOL, offsetof(aircraft_t, leader), MEMBER_SIZEOF(aircraft_t, leader)}, {"image", V_HUNK_STRING, offsetof(aircraft_t, image), 0}, {"model", V_HUNK_STRING, offsetof(aircraft_t, model), 0}, /* price for selling/buying */ {"price", V_INT, offsetof(aircraft_t, price), MEMBER_SIZEOF(aircraft_t, price)}, /* this is needed to let the buy and sell screen look for the needed building */ /* to place the aircraft in */ {"productioncost", V_INT, offsetof(aircraft_t, productionCost), MEMBER_SIZEOF(aircraft_t, productionCost)}, {"building", V_HUNK_STRING, offsetof(aircraft_t, building), 0}, {nullptr, V_NULL, 0, 0} }; /** @brief Valid radar definition values for an airtcraft from script files. */ static const value_t aircraft_radar_vals[] = { {"range", V_INT, offsetof(aircraft_t, radar.range), MEMBER_SIZEOF(aircraft_t, radar.range)}, {"trackingrange", V_INT, offsetof(aircraft_t, radar.trackingRange), MEMBER_SIZEOF(aircraft_t, radar.trackingRange)}, {"detectionprobability", V_FLOAT, offsetof(aircraft_t, radar.ufoDetectionProbability), MEMBER_SIZEOF(aircraft_t, radar.ufoDetectionProbability)}, {nullptr, V_NULL, 0, 0} }; /** * @brief Parses all aircraft that are defined in our UFO-scripts. * @sa CL_ParseClientData * @sa CL_ParseScriptSecond * @note parses the aircraft into our aircraft_sample array to use as reference */ void AIR_ParseAircraft (const char* name, const char** text, bool assignAircraftItems) { const char* errhead = "AIR_ParseAircraft: unexpected end of file (aircraft "; aircraft_t* aircraftTemplate; const char* token; int i; technology_t* tech; aircraftItemType_t itemType = MAX_ACITEMS; if (ccs.numAircraftTemplates >= MAX_AIRCRAFT) { Com_Printf("AIR_ParseAircraft: too many aircraft definitions; def \"%s\" ignored\n", name); return; } if (!assignAircraftItems) { aircraftTemplate = nullptr; for (i = 0; i < ccs.numAircraftTemplates; i++) { aircraft_t* aircraft = &ccs.aircraftTemplates[i]; if (Q_streq(aircraft->id, name)) { aircraftTemplate = aircraft; break; } } if (aircraftTemplate) { Com_Printf("AIR_ParseAircraft: Second aircraft with same name found (%s) - second ignored\n", name); return; } /* initialize the menu */ aircraftTemplate = &ccs.aircraftTemplates[ccs.numAircraftTemplates]; OBJZERO(*aircraftTemplate); Com_DPrintf(DEBUG_CLIENT, "...found aircraft %s\n", name); aircraftTemplate->tpl = aircraftTemplate; aircraftTemplate->id = Mem_PoolStrDup(name, cp_campaignPool, 0); aircraftTemplate->status = AIR_HOME; /* default is no ufo */ aircraftTemplate->ufotype = UFO_MAX; aircraftTemplate->maxWeapons = 0; aircraftTemplate->maxElectronics = 0; AII_InitialiseAircraftSlots(aircraftTemplate); /* Initialise UFO sensored */ RADAR_InitialiseUFOs(&aircraftTemplate->radar); /* Default detection probability remains 100% for now */ aircraftTemplate->radar.ufoDetectionProbability = 1.0f; ccs.numAircraftTemplates++; } else { aircraftTemplate = nullptr; for (i = 0; i < ccs.numAircraftTemplates; i++) { aircraft_t* aircraft = &ccs.aircraftTemplates[i]; if (Q_streq(aircraft->id, name)) { aircraftTemplate = aircraft; break; } } if (!aircraftTemplate) Sys_Error("Could not find aircraft '%s'", name); } /* get it's body */ token = Com_Parse(text); if (!*text || *token != '{') { Com_Printf("AIR_ParseAircraft: aircraft def \"%s\" without body ignored\n", name); return; } do { token = cgi->Com_EParse(text, errhead, name); if (!*text) break; if (*token == '}') break; if (Q_streq(token, "name")) { token = cgi->Com_EParse(text, errhead, name); if (!*text) return; if (token[0] == '_') token++; Q_strncpyz(aircraftTemplate->name, token, sizeof(aircraftTemplate->name)); continue; } else if (Q_streq(token, "radar")) { token = cgi->Com_EParse(text, errhead, name); if (!*text || *token != '{') { Com_Printf("AIR_ParseAircraft: Invalid radar value for aircraft: %s\n", name); return; } do { token = cgi->Com_EParse(text, errhead, name); if (!*text) break; if (*token == '}') break; if (!Com_ParseBlockToken(name, text, aircraftTemplate, aircraft_radar_vals, cp_campaignPool, token)) Com_Printf("AIR_ParseAircraft: Ignoring unknown radar value '%s'\n", token); } while (*text); /* dummy condition */ continue; } if (assignAircraftItems) { /* write into cp_campaignPool - this data is reparsed on every new game */ /* blocks like param { [..] } - otherwise we would leave the loop too early */ if (*token == '{') { Com_SkipBlock(text); } else if (Q_streq(token, "shield")) { token = cgi->Com_EParse(text, errhead, name); if (!*text) return; Com_DPrintf(DEBUG_CLIENT, "use shield %s for aircraft %s\n", token, aircraftTemplate->id); tech = RS_GetTechByID(token); if (tech) aircraftTemplate->shield.item = INVSH_GetItemByID(tech->provides); } else if (Q_streq(token, "slot")) { token = cgi->Com_EParse(text, errhead, name); if (!*text || *token != '{') { Com_Printf("AIR_ParseAircraft: Invalid slot value for aircraft: %s\n", name); return; } do { token = cgi->Com_EParse(text, errhead, name); if (!*text) break; if (*token == '}') break; if (Q_streq(token, "type")) { token = cgi->Com_EParse(text, errhead, name); if (!*text) return; for (i = 0; i < MAX_ACITEMS; i++) { if (Q_streq(token, air_slot_type_strings[i])) { itemType = (aircraftItemType_t)i; switch (itemType) { case AC_ITEM_WEAPON: aircraftTemplate->maxWeapons++; break; case AC_ITEM_ELECTRONICS: aircraftTemplate->maxElectronics++; break; default: itemType = MAX_ACITEMS; break; } break; } } if (i == MAX_ACITEMS) cgi->Com_Error(ERR_DROP, "Unknown value '%s' for slot type\n", token); } else if (Q_streq(token, "position")) { token = cgi->Com_EParse(text, errhead, name); if (!*text) return; for (i = 0; i < AIR_POSITIONS_MAX; i++) { if (Q_streq(token, air_position_strings[i])) { switch (itemType) { case AC_ITEM_WEAPON: aircraftTemplate->weapons[aircraftTemplate->maxWeapons - 1].pos = (itemPos_t)i; break; case AC_ITEM_ELECTRONICS: aircraftTemplate->electronics[aircraftTemplate->maxElectronics - 1].pos = (itemPos_t)i; break; default: i = AIR_POSITIONS_MAX; break; } break; } } if (i == AIR_POSITIONS_MAX) cgi->Com_Error(ERR_DROP, "Unknown value '%s' for slot position\n", token); } else if (Q_streq(token, "contains")) { token = cgi->Com_EParse(text, errhead, name); if (!*text) return; tech = RS_GetTechByID(token); if (tech) { switch (itemType) { case AC_ITEM_WEAPON: aircraftTemplate->weapons[aircraftTemplate->maxWeapons - 1].item = INVSH_GetItemByID(tech->provides); Com_DPrintf(DEBUG_CLIENT, "use weapon %s for aircraft %s\n", token, aircraftTemplate->id); break; case AC_ITEM_ELECTRONICS: aircraftTemplate->electronics[aircraftTemplate->maxElectronics - 1].item = INVSH_GetItemByID(tech->provides); Com_DPrintf(DEBUG_CLIENT, "use electronics %s for aircraft %s\n", token, aircraftTemplate->id); break; default: Com_Printf("Ignoring item value '%s' due to unknown slot type\n", token); break; } } } else if (Q_streq(token, "ammo")) { token = cgi->Com_EParse(text, errhead, name); if (!*text) return; tech = RS_GetTechByID(token); if (tech) { if (itemType == AC_ITEM_WEAPON) { aircraftTemplate->weapons[aircraftTemplate->maxWeapons - 1].ammo = INVSH_GetItemByID(tech->provides); Com_DPrintf(DEBUG_CLIENT, "use ammo %s for aircraft %s\n", token, aircraftTemplate->id); } else Com_Printf("Ignoring ammo value '%s' due to unknown slot type\n", token); } } else if (Q_streq(token, "size")) { token = cgi->Com_EParse(text, errhead, name); if (!*text) return; if (itemType == AC_ITEM_WEAPON) { if (Q_streq(token, "light")) aircraftTemplate->weapons[aircraftTemplate->maxWeapons - 1].size = ITEM_LIGHT; else if (Q_streq(token, "medium")) aircraftTemplate->weapons[aircraftTemplate->maxWeapons - 1].size = ITEM_MEDIUM; else if (Q_streq(token, "heavy")) aircraftTemplate->weapons[aircraftTemplate->maxWeapons - 1].size = ITEM_HEAVY; else Com_Printf("Unknown size value for aircraft slot: '%s'\n", token); } else Com_Printf("Ignoring size parameter '%s' for non-weapon aircraft slots\n", token); } else Com_Printf("AIR_ParseAircraft: Ignoring unknown slot value '%s'\n", token); } while (*text); /* dummy condition */ } } else { if (Q_streq(token, "shield")) { cgi->Com_EParse(text, errhead, name); continue; } /* check for some standard values */ if (Com_ParseBlockToken(name, text, aircraftTemplate, aircraft_vals, cp_campaignPool, token)) continue; if (Q_streq(token, "type")) { token = cgi->Com_EParse(text, errhead, name); if (!*text) return; if (Q_streq(token, "transporter")) aircraftTemplate->type = AIRCRAFT_TRANSPORTER; else if (Q_streq(token, "interceptor")) aircraftTemplate->type = AIRCRAFT_INTERCEPTOR; else if (Q_streq(token, "ufo")) { aircraftTemplate->type = AIRCRAFT_UFO; aircraftTemplate->ufotype = cgi->Com_UFOShortNameToID(aircraftTemplate->id); } } else if (Q_streq(token, "slot")) { token = cgi->Com_EParse(text, errhead, name); if (!*text || *token != '{') { Com_Printf("AIR_ParseAircraft: Invalid slot value for aircraft: %s\n", name); return; } Com_SkipBlock(text); } else if (Q_streq(token, "param")) { token = cgi->Com_EParse(text, errhead, name); if (!*text || *token != '{') { Com_Printf("AIR_ParseAircraft: Invalid param value for aircraft: %s\n", name); return; } do { token = cgi->Com_EParse(text, errhead, name); if (!*text) break; if (*token == '}') break; if (Q_streq(token, "range")) { /* this is the range of aircraft, must be translated into fuel */ token = cgi->Com_EParse(text, errhead, name); if (!*text) return; cgi->Com_EParseValue(aircraftTemplate, token, V_INT, offsetof(aircraft_t, stats[AIR_STATS_FUELSIZE]), MEMBER_SIZEOF(aircraft_t, stats[0])); if (aircraftTemplate->stats[AIR_STATS_SPEED] == 0) cgi->Com_Error(ERR_DROP, "AIR_ParseAircraft: speed value must be entered before range value"); aircraftTemplate->stats[AIR_STATS_FUELSIZE] = (int) (2.0f * (float)SECONDS_PER_HOUR * aircraftTemplate->stats[AIR_STATS_FUELSIZE]) / ((float) aircraftTemplate->stats[AIR_STATS_SPEED]); } else { if (!Com_ParseBlockToken(name, text, aircraftTemplate, aircraft_param_vals, cp_campaignPool, token)) Com_Printf("AIR_ParseAircraft: Ignoring unknown param value '%s'\n", token); } } while (*text); /* dummy condition */ } else { Com_Printf("AIR_ParseAircraft: unknown token \"%s\" ignored (aircraft %s)\n", token, name); cgi->Com_EParse(text, errhead, name); } } /* assignAircraftItems */ } while (*text); if (aircraftTemplate->productionCost == 0) aircraftTemplate->productionCost = aircraftTemplate->price; if (aircraftTemplate->size < AIRCRAFT_SMALL || aircraftTemplate->size > AIRCRAFT_LARGE) Sys_Error("Invalid aircraft size given for '%s'", aircraftTemplate->id); } #ifdef DEBUG void AIR_ListCraftIndexes_f (void) { Com_Printf("globalIDX\t(Craftname)\n"); AIR_Foreach(aircraft) { Com_Printf("%i\t(%s)\n", aircraft->idx, aircraft->name); } } /** * @brief Debug function that prints aircraft to game console */ void AIR_ListAircraftSamples_f (void) { int i = 0, max = ccs.numAircraftTemplates; const value_t* vp; Com_Printf("%i aircraft\n", max); if (cgi->Cmd_Argc() == 2) { max = atoi(cgi->Cmd_Argv(1)); if (max >= ccs.numAircraftTemplates || max < 0) return; i = max - 1; } for (; i < max; i++) { aircraft_t* aircraftTemplate = &ccs.aircraftTemplates[i]; Com_Printf("aircraft: '%s'\n", aircraftTemplate->id); for (vp = aircraft_vals; vp->string; vp++) { Com_Printf("..%s: %s\n", vp->string, cgi->Com_ValueToStr(aircraftTemplate, vp->type, vp->ofs)); } for (vp = aircraft_param_vals; vp->string; vp++) { Com_Printf("..%s: %s\n", vp->string, cgi->Com_ValueToStr(aircraftTemplate, vp->type, vp->ofs)); } } } #endif /*=============================================== Aircraft functions related to UFOs or missions. ===============================================*/ /** * @brief Notify aircraft that a mission has been removed. * @param[in] mission Pointer to the mission that has been removed. * @note Aircraft currently moving to the mission will be redirect to base */ void AIR_AircraftsNotifyMissionRemoved (const mission_t* const mission) { AIR_Foreach(aircraft) { if (aircraft->mission == mission) AIR_AircraftReturnToBase(aircraft); } } /** * @brief Notify that a UFO has been removed. * @param[in] ufo Pointer to UFO that has been removed. * @param[in] destroyed True if the UFO has been destroyed, false if it only landed. */ void AIR_AircraftsNotifyUFORemoved (const aircraft_t* const ufo, bool destroyed) { base_t* base; assert(ufo); /* Aircraft currently purchasing the specified ufo will be redirect to base */ AIR_Foreach(aircraft) { if (ufo == aircraft->aircraftTarget) { AIR_AircraftReturnToBase(aircraft); } else if (destroyed && (ufo < aircraft->aircraftTarget)) { aircraft->aircraftTarget--; } } /** @todo this should be in a BDEF_NotifyUFORemoved callback */ base = nullptr; while ((base = B_GetNext(base)) != nullptr) { int i; /* Base currently targeting the specified ufo loose their target */ for (i = 0; i < base->numBatteries; i++) { baseWeapon_t* baseWeapon = &base->batteries[i]; if (baseWeapon->target == ufo) baseWeapon->target = nullptr; else if (destroyed && (baseWeapon->target > ufo)) baseWeapon->target--; } for (i = 0; i < base->numLasers; i++) { baseWeapon_t* baseWeapon = &base->lasers[i]; if (baseWeapon->target == ufo) baseWeapon->target = nullptr; else if (destroyed && (baseWeapon->target > ufo)) baseWeapon->target--; } } } /** * @brief Notify that a UFO disappear from radars. * @param[in] ufo Pointer to a UFO that has disappeared. * @note Aircraft currently pursuing the specified UFO will be redirected to base */ void AIR_AircraftsUFODisappear (const aircraft_t* const ufo) { AIR_Foreach(aircraft) { if (aircraft->status == AIR_UFO && ufo == aircraft->aircraftTarget) AIR_AircraftReturnToBase(aircraft); } } /** * @brief funtion we need to find roots. * @param[in] c angle SOT * @param[in] B angle STI * @param[in] speedRatio ratio of speed of shooter divided by speed of target. * @param[in] a angle TOI * @note S is the position of the shooter, T the position of the target, * D the destination of the target and I the interception point where shooter should reach target. * @return value of the function. */ static inline float AIR_GetDestinationFunction (const float c, const float B, const float speedRatio, float a) { return pow(cos(a) - cos(speedRatio * a) * cos(c), 2.) - sin(c) * sin(c) * (sin(speedRatio * a) * sin(speedRatio * a) - sin(a) * sin(a) * sin(B) * sin(B)); } /** * @brief derivative of the funtion we need to find roots. * @param[in] c angle SOT * @param[in] B angle STI * @param[in] speedRatio ratio of speed of shooter divided by speed of target. * @param[in] a angle TOI * @note S is the position of the shooter, T the position of the target, * D the destination of the target and I the interception point where shooter should reach target. * @return value of the derivative function. */ static inline float AIR_GetDestinationDerivativeFunction (const float c, const float B, const float speedRatio, float a) { return 2. * (cos(a) - cos(speedRatio * a) * cos(c)) * (- sin(a) + speedRatio * sin(speedRatio * a) * cos(c)) - sin(c) * sin(c) * (speedRatio * sin(2. * speedRatio * a) - sin(2. * a) * sin(B) * sin(B)); } /** * @brief Find the roots of a function. * @param[in] c angle SOT * @param[in] B angle STI * @param[in] speedRatio ratio of speed of shooter divided by speed of target. * @param[in] start minimum value of the root to find * @note S is the position of the shooter, T the position of the target, * D the destination of the target and I the interception point where shooter should reach target. * @return root of the function. */ static float AIR_GetDestinationFindRoot (const float c, const float B, const float speedRatio, float start) { const float BIG_STEP = .05; /**< step for rough calculation. this value must be short enough so * that we're sure there's only 1 root in this range. */ const float PRECISION_ROOT = 0.000001; /**< precision of the calculation */ const float MAXIMUM_VALUE_ROOT = 2. * M_PI; /**< maximum value of the root to search */ float epsilon; /**< precision of current point */ float begin, end, middle; /**< abscissa of the point */ float fBegin, fEnd, fMiddle; /**< ordinate of the point */ float fdBegin, fdEnd, fdMiddle; /**< derivative of the point */ /* there may be several solution, first try to find roughly the smallest one */ end = start + PRECISION_ROOT / 10.; /* don't start at 0: derivative is 0 */ fEnd = AIR_GetDestinationFunction(c, B, speedRatio, end); fdEnd = AIR_GetDestinationDerivativeFunction(c, B, speedRatio, end); do { begin = end; fBegin = fEnd; fdBegin = fdEnd; end = begin + BIG_STEP; if (end > MAXIMUM_VALUE_ROOT) { end = MAXIMUM_VALUE_ROOT; fEnd = AIR_GetDestinationFunction(c, B, speedRatio, end); break; } fEnd = AIR_GetDestinationFunction(c, B, speedRatio, end); fdEnd = AIR_GetDestinationDerivativeFunction(c, B, speedRatio, end); } while (fBegin * fEnd > 0 && fdBegin * fdEnd > 0); if (fBegin * fEnd > 0) { if (fdBegin * fdEnd < 0) { /* the sign of derivative changed: we could have a root somewhere * between begin and end: try to narrow down the root until fBegin * fEnd < 0 */ middle = (begin + end) / 2.; fMiddle = AIR_GetDestinationFunction(c, B, speedRatio, middle); fdMiddle = AIR_GetDestinationDerivativeFunction(c, B, speedRatio, middle); do { if (fdEnd * fdMiddle < 0) { /* root is bigger than middle */ begin = middle; fBegin = fMiddle; fdBegin = fdMiddle; } else if (fdBegin * fdMiddle < 0) { /* root is smaller than middle */ end = middle; fEnd = fMiddle; fdEnd = fdMiddle; } else { cgi->Com_Error(ERR_DROP, "AIR_GetDestinationFindRoot: Error in calculation, can't find root"); } middle = (begin + end) / 2.; fMiddle = AIR_GetDestinationFunction(c, B, speedRatio, middle); fdMiddle = AIR_GetDestinationDerivativeFunction(c, B, speedRatio, middle); epsilon = end - middle ; if (epsilon < PRECISION_ROOT) { /* this is only a root of the derivative: no root of the function itself * proceed with next value */ return AIR_GetDestinationFindRoot(c, B, speedRatio, end); } } while (fBegin * fEnd > 0); } else { /* there's no solution, return default value */ Com_DPrintf(DEBUG_CLIENT, "AIR_GetDestinationFindRoot: Did not find solution is range %.2f, %.2f\n", start, MAXIMUM_VALUE_ROOT); return -10.; } } /* now use dichotomy to get more precision on the solution */ middle = (begin + end) / 2.; fMiddle = AIR_GetDestinationFunction(c, B, speedRatio, middle); do { if (fEnd * fMiddle < 0) { /* root is bigger than middle */ begin = middle; fBegin = fMiddle; } else if (fBegin * fMiddle < 0) { /* root is smaller than middle */ end = middle; fEnd = fMiddle; } else { Com_DPrintf(DEBUG_CLIENT, "AIR_GetDestinationFindRoot: Error in calculation, one of the value is nan\n"); return -10.; } middle = (begin + end) / 2.; fMiddle = AIR_GetDestinationFunction(c, B, speedRatio, middle); epsilon = end - middle ; } while (epsilon > PRECISION_ROOT); return middle; } /** * @brief Calculates the point where aircraft should go to intecept a moving target. * @param[in] shooter Pointer to shooting aircraft. * @param[in] target Pointer to target aircraft. * @param[out] dest Destination that shooting aircraft should aim to intercept target aircraft. * @todo only compute this calculation every time target changes destination, or one of the aircraft speed changes. * @sa AIR_SendAircraftPursuingUFO * @sa UFO_SendPursuingAircraft */ void AIR_GetDestinationWhilePursuing (const aircraft_t* shooter, const aircraft_t* target, vec2_t dest) { vec3_t shooterPos, targetPos, targetDestPos, shooterDestPos, rotationAxis; vec3_t tangentVectTS, tangentVectTD; float a, b, c, B; const float speedRatio = (float)(shooter->stats[AIR_STATS_SPEED]) / target->stats[AIR_STATS_SPEED]; c = GetDistanceOnGlobe(shooter->pos, target->pos) * torad; /* Convert aircraft position into cartesian frame */ PolarToVec(shooter->pos, shooterPos); PolarToVec(target->pos, targetPos); PolarToVec(target->route.point[target->route.numPoints - 1], targetDestPos); /** In the following, we note S the position of the shooter, T the position of the target, * D the destination of the target and I the interception point where shooter should reach target * O is the center of earth. * A, B and C are the angles TSI, STI and SIT * a, b, and c are the angles TOI, SOI and SOT * * According to geometry on a sphere, the values defined above must be solutions of both equations: * sin(A) / sin(a) = sin(B) / sin(b) * cos(a) = cos(b) * cos(c) + sin(b) * sin(c) * cos(A) * And we have another equation, given by the fact that shooter and target must reach I at the same time: * shooterSpeed * a = targetSpeed * b * We the want to find and equation linking a, c and B (we know the last 2 values). We therefore * eliminate b, then A, to get the equation we need to solve: * pow(cos(a) - cos(speedRatio * a) * cos(c), 2.) * - sin(c) * sin(c) * (sin(speedRatio * a) * sin(speedRatio * a) - sin(a) * sin(a) * sin(B) * sin(B)) = 0 */ /* Get first vector (tangent to triangle in T, in the direction of D) */ CrossProduct(targetPos, shooterPos, rotationAxis); VectorNormalize(rotationAxis); RotatePointAroundVector(tangentVectTS, rotationAxis, targetPos, 90.0f); /* Get second vector (tangent to triangle in T, in the direction of S) */ CrossProduct(targetPos, targetDestPos, rotationAxis); VectorNormalize(rotationAxis); RotatePointAroundVector(tangentVectTD, rotationAxis, targetPos, 90.0f); /* Get angle B of the triangle (in radian) */ B = acos(DotProduct(tangentVectTS, tangentVectTD)); /* Look for a value, as long as we don't have a proper value */ for (a = 0;;) { a = AIR_GetDestinationFindRoot(c, B, speedRatio, a); if (a < 0.) { /* we couldn't find a root on the whole range */ break; } /* Get rotation vector */ CrossProduct(targetPos, targetDestPos, rotationAxis); VectorNormalize(rotationAxis); /* Rotate target position of dist to find destination point */ RotatePointAroundVector(shooterDestPos, rotationAxis, targetPos, a * todeg); VecToPolar(shooterDestPos, dest); b = GetDistanceOnGlobe(shooter->pos, dest) * torad; if (fabs(b - speedRatio * a) < .1) break; Com_DPrintf(DEBUG_CLIENT, "AIR_GetDestinationWhilePursuing: reject solution: doesn't fit %.2f == %.2f\n", b, speedRatio * a); } if (a < 0.) { /* did not find solution, go directly to target direction */ Vector2Copy(target->pos, dest); return; } /** @todo add EQUAL_EPSILON here? */ /* make sure we don't get a NAN value */ assert(dest[0] <= 180.0f && dest[0] >= -180.0f && dest[1] <= 90.0f && dest[1] >= -90.0f); } /** * @brief Make the specified aircraft purchasing a UFO. * @param[in] aircraft Pointer to an aircraft which will hunt for a UFO. * @param[in] ufo Pointer to a UFO. */ bool AIR_SendAircraftPursuingUFO (aircraft_t* aircraft, aircraft_t* ufo) { vec2_t dest; if (!aircraft) return false; /* if aircraft was in base */ if (AIR_IsAircraftInBase(aircraft)) { /* reload its ammunition */ AII_ReloadAircraftWeapons(aircraft); } AIR_GetDestinationWhilePursuing(aircraft, ufo, dest); /* check if aircraft has enough fuel */ if (!AIR_AircraftHasEnoughFuel(aircraft, dest)) { /* did not find solution, go directly to target direction if enough fuel */ if (AIR_AircraftHasEnoughFuel(aircraft, ufo->pos)) { Com_DPrintf(DEBUG_CLIENT, "AIR_SendAircraftPursuingUFO: not enough fuel to anticipate target movement: go directly to target position\n"); Vector2Copy(ufo->pos, dest); } else { MS_AddNewMessage(_("Notice"), va(_("Craft %s has not enough fuel to intercept UFO: fly back to %s."), aircraft->name, aircraft->homebase->name)); AIR_AircraftReturnToBase(aircraft); return false; } } GEO_CalcLine(aircraft->pos, dest, &aircraft->route); aircraft->status = AIR_UFO; aircraft->time = 0; aircraft->point = 0; aircraft->aircraftTarget = ufo; return true; } /*============================================ Aircraft functions related to team handling. ============================================*/ /** * @brief Resets team in given aircraft. * @param[in] aircraft Pointer to an aircraft, where the team will be reset. */ void AIR_ResetAircraftTeam (aircraft_t* aircraft) { cgi->LIST_Delete(&aircraft->acTeam); } /** * @brief Adds given employee to given aircraft. * @param[in] aircraft Pointer to an aircraft, to which we will add employee. * @param[in] employee The employee to add to the aircraft. * @note this is responsible for adding soldiers to a team in dropship */ bool AIR_AddToAircraftTeam (aircraft_t* aircraft, Employee* employee) { if (!employee) return false; if (!aircraft) return false; if (AIR_GetTeamSize(aircraft) < aircraft->maxTeamSize) { cgi->LIST_AddPointer(&aircraft->acTeam, employee); return true; } return false; } /** * @brief Checks whether given employee is in given aircraft * @param[in] aircraft The aircraft to check * @param[in] employee Employee to check. * @return @c true if the given employee is assigned to the given aircraft. */ bool AIR_IsInAircraftTeam (const aircraft_t* aircraft, const Employee* employee) { assert(aircraft); assert(employee); return cgi->LIST_GetPointer(aircraft->acTeam, employee) != nullptr; } /** * @brief Counts the number of soldiers in given aircraft. * @param[in] aircraft Pointer to the aircraft, for which we return the amount of soldiers. * @return Amount of soldiers. */ int AIR_GetTeamSize (const aircraft_t* aircraft) { assert(aircraft); return cgi->LIST_Count(aircraft->acTeam); } /** * @brief Assign a pilot to an aircraft * @param[out] aircraft Pointer to the aircraft to add pilot to * @param[in] pilot Pointer to the pilot to add * @return @c true if the assignment was successful (there wasn't a pilot * assigned), @c false if there was already a pilot assigned and we tried * to assign a new one (@c pilot isn't @c nullptr). */ bool AIR_SetPilot (aircraft_t* aircraft, Employee* pilot) { if (aircraft->pilot == nullptr || pilot == nullptr) { aircraft->pilot = pilot; return true; } return false; } /** * @brief Get pilot of an aircraft * @param[in] aircraft Pointer to the aircraft * @return @c nullptr if there is no pilot assigned to this craft, the employee pointer otherwise */ Employee* AIR_GetPilot (const aircraft_t* aircraft) { const Employee* e = aircraft->pilot; if (!e) return nullptr; return E_GetEmployeeByTypeFromChrUCN(e->getType(), e->chr.ucn); } /** * @brief Determine if an aircraft's pilot survived a crash, based on his piloting skill (and a bit of randomness) * @param[in] aircraft Pointer to crashed aircraft * @return true if survived, false if not */ bool AIR_PilotSurvivedCrash (const aircraft_t* aircraft) { if (aircraft == nullptr) return false; if (aircraft->pilot == nullptr) return false; const int pilotSkill = aircraft->pilot->chr.score.skills[SKILL_PILOTING]; float baseProbability = (float) pilotSkill; const byte* color = GEO_GetColor(aircraft->pos, MAPTYPE_TERRAIN, nullptr); /* Crash over water: chances for survival are very bad */ if (MapIsWater(color)) { baseProbability /= 10.0f; } /* Crash over arctic or wasted terrain; fare a little better */ if (MapIsArctic(color) || MapIsWasted(color)) { baseProbability /= 8.0f; } /* Crash over mountain, desert or cold terrain: survive a little longer */ if (MapIsCold(color) || MapIsDesert(color) || MapIsMountain(color)) { baseProbability /= 3.0f; } /* Crash over tropical or fertile are: This is the good life :) */ if (MapIsGrass(color) || MapIsTropical(color)) { baseProbability *= 2.5f; } /* Add a random factor to our probability */ float randomProbability = crand() * (float) pilotSkill; if (randomProbability > 0.25f * baseProbability) { while (randomProbability > 0.25f * baseProbability) randomProbability /= 2.0f; } const float survivalProbability = baseProbability + randomProbability; return survivalProbability >= (float) pilotSkill; } /** * @brief Adds the pilot to the first available aircraft at the specified base. * @param[in] base Which base has aircraft to add the pilot to. * @param[in] pilot Which pilot to add. */ void AIR_AutoAddPilotToAircraft (const base_t* base, Employee* pilot) { AIR_ForeachFromBase(aircraft, base) { if (AIR_SetPilot(aircraft, pilot)) break; } } /** * @brief Checks to see if the pilot is in any aircraft at this base. * If he is then he is removed from that aircraft. * @param[in] base Which base has the aircraft to search for the employee in. * @param[in] pilot Which pilot to search for. */ void AIR_RemovePilotFromAssignedAircraft (const base_t* base, const Employee* pilot) { AIR_ForeachFromBase(aircraft, base) { if (AIR_GetPilot(aircraft) == pilot) { AIR_SetPilot(aircraft, nullptr); break; } } } /** * @brief Get the all the unique weapon ranges of this aircraft. * @param[in] slot Pointer to the aircrafts weapon slot list. * @param[in] maxSlot maximum number of weapon slots in aircraft. * @param[out] weaponRanges An array containing a unique list of weapons ranges. * @return Number of unique weapons ranges. */ int AIR_GetAircraftWeaponRanges (const aircraftSlot_t* slot, int maxSlot, float* weaponRanges) { int idxSlot; float allWeaponRanges[MAX_AIRCRAFTSLOT]; int numAllWeaponRanges = 0; int numUniqueWeaponRanges = 0; assert(slot); /* We choose the usable weapon to add to the weapons array */ for (idxSlot = 0; idxSlot < maxSlot; idxSlot++) { const aircraftSlot_t* weapon = slot + idxSlot; const objDef_t* ammo = weapon->ammo; if (!ammo) continue; allWeaponRanges[numAllWeaponRanges] = ammo->craftitem.stats[AIR_STATS_WRANGE]; numAllWeaponRanges++; } if (numAllWeaponRanges > 0) { /* sort the list of all weapon ranges and create an array with only the unique ranges */ qsort(allWeaponRanges, numAllWeaponRanges, sizeof(allWeaponRanges[0]), Q_FloatSort); int idxAllWeap; for (idxAllWeap = 0; idxAllWeap < numAllWeaponRanges; idxAllWeap++) { if (allWeaponRanges[idxAllWeap] != weaponRanges[numUniqueWeaponRanges - 1] || idxAllWeap == 0) { weaponRanges[numUniqueWeaponRanges] = allWeaponRanges[idxAllWeap]; numUniqueWeaponRanges++; } } } return numUniqueWeaponRanges; } /** * @brief Saves an route plan of an aircraft * @param[out] node XML Node structure, where we write the information to * @param[in] route Aircraft route plan */ static void AIR_SaveRouteXML (xmlNode_t* node, const mapline_t* route) { int j; xmlNode_t* subnode; subnode = cgi->XML_AddNode(node, SAVE_AIRCRAFT_ROUTE); cgi->XML_AddFloatValue(subnode, SAVE_AIRCRAFT_ROUTE_DISTANCE, route->distance); for (j = 0; j < route->numPoints; j++) { cgi->XML_AddPos2(subnode, SAVE_AIRCRAFT_ROUTE_POINT, route->point[j]); } } /** * @brief Saves an item slot * @param[in] slot Pointer to the slot where item is. * @param[in] num Number of slots for this aircraft. * @param[out] p XML Node structure, where we write the information to * @param[in] p pointer where information are written. * @param[in] weapon True if the slot is a weapon slot. * @sa B_Save * @sa AII_InitialiseSlot */ static void AIR_SaveAircraftSlotsXML (const aircraftSlot_t* slot, const int num, xmlNode_t* p, bool weapon) { int i; for (i = 0; i < num; i++) { xmlNode_t* sub = cgi->XML_AddNode(p, SAVE_AIRCRAFT_SLOT); AII_SaveOneSlotXML(sub, &slot[i], weapon); } } /** * @brief Saves an aircraft * @param[out] p XML Node structure, where we write the information to * @param[in] aircraft Aircraft we save * @param[in] isUfo If this aircraft is a UFO */ static bool AIR_SaveAircraftXML (xmlNode_t* p, const aircraft_t* const aircraft, bool const isUfo) { xmlNode_t* node; xmlNode_t* subnode; int l; const Employee* pilot; cgi->Com_RegisterConstList(saveAircraftConstants); node = cgi->XML_AddNode(p, SAVE_AIRCRAFT_AIRCRAFT); cgi->XML_AddInt(node, SAVE_AIRCRAFT_IDX, aircraft->idx); cgi->XML_AddString(node, SAVE_AIRCRAFT_ID, aircraft->id); cgi->XML_AddString(node, SAVE_AIRCRAFT_NAME, aircraft->name); cgi->XML_AddString(node, SAVE_AIRCRAFT_STATUS, cgi->Com_GetConstVariable(SAVE_AIRCRAFTSTATUS_NAMESPACE, aircraft->status)); cgi->XML_AddInt(node, SAVE_AIRCRAFT_FUEL, aircraft->fuel); cgi->XML_AddInt(node, SAVE_AIRCRAFT_DAMAGE, aircraft->damage); cgi->XML_AddPos3(node, SAVE_AIRCRAFT_POS, aircraft->pos); cgi->XML_AddPos3(node, SAVE_AIRCRAFT_DIRECTION, aircraft->direction); cgi->XML_AddInt(node, SAVE_AIRCRAFT_POINT, aircraft->point); cgi->XML_AddInt(node, SAVE_AIRCRAFT_TIME, aircraft->time); subnode = cgi->XML_AddNode(node, SAVE_AIRCRAFT_WEAPONS); AIR_SaveAircraftSlotsXML(aircraft->weapons, aircraft->maxWeapons, subnode, true); subnode = cgi->XML_AddNode(node, SAVE_AIRCRAFT_SHIELDS); AIR_SaveAircraftSlotsXML(&aircraft->shield, 1, subnode, false); subnode = cgi->XML_AddNode(node, SAVE_AIRCRAFT_ELECTRONICS); AIR_SaveAircraftSlotsXML(aircraft->electronics, aircraft->maxElectronics, subnode, false); AIR_SaveRouteXML(node, &aircraft->route); if (isUfo) { #ifdef DEBUG if (!aircraft->mission) Com_Printf("Error: UFO '%s'is not linked to any mission\n", aircraft->id); #endif cgi->XML_AddString(node, SAVE_AIRCRAFT_MISSIONID, aircraft->mission->id); /** detection id and time */ cgi->XML_AddInt(node, SAVE_AIRCRAFT_DETECTIONIDX, aircraft->detectionIdx); cgi->XML_AddDate(node, SAVE_AIRCRAFT_LASTSPOTTED_DATE, aircraft->lastSpotted.day, aircraft->lastSpotted.sec); } else { if (aircraft->status == AIR_MISSION) { assert(aircraft->mission); cgi->XML_AddString(node, SAVE_AIRCRAFT_MISSIONID, aircraft->mission->id); } if (aircraft->homebase) { cgi->XML_AddInt(node, SAVE_AIRCRAFT_HOMEBASE, aircraft->homebase->idx); } } if (aircraft->aircraftTarget) { if (isUfo) cgi->XML_AddInt(node, SAVE_AIRCRAFT_AIRCRAFTTARGET, aircraft->aircraftTarget->idx); else cgi->XML_AddInt(node, SAVE_AIRCRAFT_AIRCRAFTTARGET, UFO_GetGeoscapeIDX(aircraft->aircraftTarget)); } subnode = cgi->XML_AddNode(node, SAVE_AIRCRAFT_AIRSTATS); for (l = 0; l < AIR_STATS_MAX; l++) { #ifdef DEBUG /* UFO HP can be < 0 if the UFO has been destroyed */ if (!(isUfo && l == AIR_STATS_DAMAGE) && aircraft->stats[l] < 0) Com_Printf("Warning: ufo '%s' stats %i: %i is smaller than 0\n", aircraft->id, l, aircraft->stats[l]); #endif if (aircraft->stats[l] != 0) { xmlNode_t* statNode = cgi->XML_AddNode(subnode, SAVE_AIRCRAFT_AIRSTAT); cgi->XML_AddString(statNode, SAVE_AIRCRAFT_AIRSTATID, cgi->Com_GetConstVariable(SAVE_AIRCRAFTSTAT_NAMESPACE, l)); cgi->XML_AddLong(statNode, SAVE_AIRCRAFT_VAL, aircraft->stats[l]); } } cgi->XML_AddBoolValue(node, SAVE_AIRCRAFT_DETECTED, aircraft->detected); cgi->XML_AddBoolValue(node, SAVE_AIRCRAFT_LANDED, aircraft->landed); cgi->Com_UnregisterConstList(saveAircraftConstants); /* All other informations are not needed for ufos */ if (isUfo) return true; subnode = cgi->XML_AddNode(node, SAVE_AIRCRAFT_AIRCRAFTTEAM); LIST_Foreach(aircraft->acTeam, Employee, employee) { xmlNode_t* ssnode = cgi->XML_AddNode(subnode, SAVE_AIRCRAFT_MEMBER); cgi->XML_AddInt(ssnode, SAVE_AIRCRAFT_TEAM_UCN, employee->chr.ucn); } pilot = AIR_GetPilot(aircraft); if (pilot) cgi->XML_AddInt(node, SAVE_AIRCRAFT_PILOTUCN, pilot->chr.ucn); /* itemcargo */ subnode = cgi->XML_AddNode(node, SAVE_AIRCRAFT_CARGO); for (l = 0; l < aircraft->itemTypes; l++) { xmlNode_t* ssnode = cgi->XML_AddNode(subnode, SAVE_AIRCRAFT_ITEM); assert(aircraft->itemcargo[l].item); cgi->XML_AddString(ssnode, SAVE_AIRCRAFT_ITEMID, aircraft->itemcargo[l].item->id); cgi->XML_AddInt(ssnode, SAVE_AIRCRAFT_AMOUNT, aircraft->itemcargo[l].amount); } /* aliencargo */ if (aircraft->alienCargo != nullptr) { subnode = cgi->XML_AddNode(node, SAVE_AIRCRAFT_ALIENCARGO); if (!subnode) return false; aircraft->alienCargo->save(subnode); } return true; } /** * @brief Save callback for savegames in xml format * @sa AIR_LoadXML * @sa B_SaveXML * @sa SAV_GameSaveXML */ bool AIR_SaveXML (xmlNode_t* parent) { int i; xmlNode_t* node, *snode; /* save phalanx aircraft */ snode = cgi->XML_AddNode(parent, SAVE_AIRCRAFT_PHALANX); AIR_Foreach(aircraft) { AIR_SaveAircraftXML(snode, aircraft, false); } /* save the ufos on geoscape */ snode = cgi->XML_AddNode(parent, SAVE_AIRCRAFT_UFOS); for (i = 0; i < MAX_UFOONGEOSCAPE; i++) { const aircraft_t* ufo = UFO_GetByIDX(i); if (!ufo || (ufo->id == nullptr)) continue; AIR_SaveAircraftXML(snode, ufo, true); } /* Save projectiles. */ node = cgi->XML_AddNode(parent, SAVE_AIRCRAFT_PROJECTILES); if (!AIRFIGHT_SaveXML(node)) return false; return true; } /** * @brief Loads the weapon slots of an aircraft. * @param[in] aircraft Pointer to the aircraft. * @param[out] slot Pointer to the slot where item should be added. * @param[in] p XML Node structure, where we get the information from * @param[in] weapon True if the slot is a weapon slot. * @param[in] max Maximum number of slots for this aircraft that should be loaded. * @sa B_Load * @sa B_SaveAircraftSlots */ static void AIR_LoadAircraftSlotsXML (aircraft_t* aircraft, aircraftSlot_t* slot, xmlNode_t* p, bool weapon, const int max) { xmlNode_t* act; int i; for (i = 0, act = cgi->XML_GetNode(p, SAVE_AIRCRAFT_SLOT); act && i <= max; act = cgi->XML_GetNextNode(act, p, SAVE_AIRCRAFT_SLOT), i++) { slot[i].aircraft = aircraft; AII_LoadOneSlotXML(act, &slot[i], weapon); } if (i > max) Com_Printf("Error: Trying to assign more than max (%d) Aircraft Slots (cur is %d)\n", max, i); } /** * @brief Loads the route of an aircraft * @param[in] p XML Node structure, where we get the information from * @param[out] route Route points of the aircraft */ static bool AIR_LoadRouteXML (xmlNode_t* p, mapline_t* route) { xmlNode_t* actual; xmlNode_t* snode; int count = 0; snode = cgi->XML_GetNode(p, SAVE_AIRCRAFT_ROUTE); if (!snode) return false; for (actual = cgi->XML_GetPos2(snode, SAVE_AIRCRAFT_ROUTE_POINT, route->point[count]); actual && count <= LINE_MAXPTS; actual = cgi->XML_GetNextPos2(actual, snode, SAVE_AIRCRAFT_ROUTE_POINT, route->point[++count])) ; if (count > LINE_MAXPTS) { Com_Printf("AIR_Load: number of points (%i) for UFO route exceed maximum value (%i)\n", count, LINE_MAXPTS); return false; } route->numPoints = count; route->distance = cgi->XML_GetFloat(snode, SAVE_AIRCRAFT_ROUTE_DISTANCE, 0.0); return true; } /** * @brief Loads an Aircraft from the savegame * @param[in] p XML Node structure, where we get the information from * @param[out] craft Pointer to the aircraft */ static bool AIR_LoadAircraftXML (xmlNode_t* p, aircraft_t* craft) { xmlNode_t* snode; xmlNode_t* ssnode; const char* statusId; /* vars, if aircraft wasn't found */ int tmpInt; int l, status; const char* s = cgi->XML_GetString(p, SAVE_AIRCRAFT_ID); const aircraft_t* crafttype = AIR_GetAircraft(s); /* Copy all datas that don't need to be saved (tpl, hangar,...) */ *craft = *crafttype; tmpInt = cgi->XML_GetInt(p, SAVE_AIRCRAFT_HOMEBASE, MAX_BASES); craft->homebase = (tmpInt != MAX_BASES) ? B_GetBaseByIDX(tmpInt) : nullptr; craft->idx = cgi->XML_GetInt(p, SAVE_AIRCRAFT_IDX, -1); if (craft->idx < 0) { Com_Printf("Invalid (or no) aircraft index %i\n", craft->idx); return false; } cgi->Com_RegisterConstList(saveAircraftConstants); statusId = cgi->XML_GetString(p, SAVE_AIRCRAFT_STATUS); if (!cgi->Com_GetConstIntFromNamespace(SAVE_AIRCRAFTSTATUS_NAMESPACE, statusId, &status)) { Com_Printf("Invalid aircraft status '%s'\n", statusId); cgi->Com_UnregisterConstList(saveAircraftConstants); return false; } craft->status = (aircraftStatus_t)status; craft->fuel = cgi->XML_GetInt(p, SAVE_AIRCRAFT_FUEL, 0); craft->damage = cgi->XML_GetInt(p, SAVE_AIRCRAFT_DAMAGE, 0); cgi->XML_GetPos3(p, SAVE_AIRCRAFT_POS, craft->pos); cgi->XML_GetPos3(p, SAVE_AIRCRAFT_DIRECTION, craft->direction); craft->point = cgi->XML_GetInt(p, SAVE_AIRCRAFT_POINT, 0); craft->time = cgi->XML_GetInt(p, SAVE_AIRCRAFT_TIME, 0); if (!AIR_LoadRouteXML(p, &craft->route)) { cgi->Com_UnregisterConstList(saveAircraftConstants); return false; } s = cgi->XML_GetString(p, SAVE_AIRCRAFT_NAME); if (s[0] == '\0') s = _(craft->defaultName); Q_strncpyz(craft->name, s, sizeof(craft->name)); s = cgi->XML_GetString(p, SAVE_AIRCRAFT_MISSIONID); craft->missionID = Mem_PoolStrDup(s, cp_campaignPool, 0); if (!craft->homebase) { /* detection id and time */ craft->detectionIdx = cgi->XML_GetInt(p, SAVE_AIRCRAFT_DETECTIONIDX, 0); cgi->XML_GetDate(p, SAVE_AIRCRAFT_LASTSPOTTED_DATE, &craft->lastSpotted.day, &craft->lastSpotted.sec); } snode = cgi->XML_GetNode(p, SAVE_AIRCRAFT_AIRSTATS); for (ssnode = cgi->XML_GetNode(snode, SAVE_AIRCRAFT_AIRSTAT); ssnode; ssnode = cgi->XML_GetNextNode(ssnode, snode, SAVE_AIRCRAFT_AIRSTAT)) { const char* statId = cgi->XML_GetString(ssnode, SAVE_AIRCRAFT_AIRSTATID); int idx; if (!cgi->Com_GetConstIntFromNamespace(SAVE_AIRCRAFTSTAT_NAMESPACE, statId, &idx)) { Com_Printf("Invalid aircraft stat '%s'\n", statId); cgi->Com_UnregisterConstList(saveAircraftConstants); return false; } craft->stats[idx] = cgi->XML_GetLong(ssnode, SAVE_AIRCRAFT_VAL, 0); #ifdef DEBUG /* UFO HP can be < 0 if the UFO has been destroyed */ if (!(!craft->homebase && idx == AIR_STATS_DAMAGE) && craft->stats[idx] < 0) Com_Printf("Warning: ufo '%s' stats %i: %i is smaller than 0\n", craft->id, idx, craft->stats[idx]); #endif } craft->detected = cgi->XML_GetBool(p, SAVE_AIRCRAFT_DETECTED, false); craft->landed = cgi->XML_GetBool(p, SAVE_AIRCRAFT_LANDED, false); tmpInt = cgi->XML_GetInt(p, SAVE_AIRCRAFT_AIRCRAFTTARGET, -1); if (tmpInt == -1) craft->aircraftTarget = nullptr; else if (!craft->homebase) craft->aircraftTarget = AIR_AircraftGetFromIDX(tmpInt); else craft->aircraftTarget = ccs.ufos + tmpInt; /* read equipment slots */ snode = cgi->XML_GetNode(p, SAVE_AIRCRAFT_WEAPONS); AIR_LoadAircraftSlotsXML(craft, craft->weapons, snode, true, craft->maxWeapons); snode = cgi->XML_GetNode(p, SAVE_AIRCRAFT_SHIELDS); AIR_LoadAircraftSlotsXML(craft, &craft->shield, snode, false, 1); snode = cgi->XML_GetNode(p, SAVE_AIRCRAFT_ELECTRONICS); AIR_LoadAircraftSlotsXML(craft, craft->electronics, snode, false, craft->maxElectronics); cgi->Com_UnregisterConstList(saveAircraftConstants); /* All other informations are not needed for ufos */ if (!craft->homebase) return true; snode = cgi->XML_GetNode(p, SAVE_AIRCRAFT_AIRCRAFTTEAM); for (ssnode = cgi->XML_GetNode(snode, SAVE_AIRCRAFT_MEMBER); AIR_GetTeamSize(craft) < craft->maxTeamSize && ssnode; ssnode = cgi->XML_GetNextNode(ssnode, snode, SAVE_AIRCRAFT_MEMBER)) { const int ucn = cgi->XML_GetInt(ssnode, SAVE_AIRCRAFT_TEAM_UCN, -1); if (ucn != -1) cgi->LIST_AddPointer(&craft->acTeam, E_GetEmployeeFromChrUCN(ucn)); } tmpInt = cgi->XML_GetInt(p, SAVE_AIRCRAFT_PILOTUCN, -1); /* the employee subsystem is loaded after the base subsystem * this means, that the pilot pointer is not (really) valid until * E_Load was called, too */ if (tmpInt != -1) AIR_SetPilot(craft, E_GetEmployeeFromChrUCN(tmpInt)); else AIR_SetPilot(craft, nullptr); RADAR_Initialise(&craft->radar, crafttype->radar.range, crafttype->radar.trackingRange, 1.0f, false); RADAR_InitialiseUFOs(&craft->radar); craft->radar.ufoDetectionProbability = crafttype->radar.ufoDetectionProbability; /* itemcargo */ snode = cgi->XML_GetNode(p, SAVE_AIRCRAFT_CARGO); for (l = 0, ssnode = cgi->XML_GetNode(snode, SAVE_AIRCRAFT_ITEM); l < MAX_CARGO && ssnode; l++, ssnode = cgi->XML_GetNextNode(ssnode, snode, SAVE_AIRCRAFT_ITEM)) { const char* const str = cgi->XML_GetString(ssnode, SAVE_AIRCRAFT_ITEMID); const objDef_t* od = INVSH_GetItemByID(str); if (!od) { Com_Printf("AIR_LoadAircraftXML: Could not find aircraftitem '%s'\n", str); l--; continue; } craft->itemcargo[l].item = od; craft->itemcargo[l].amount = cgi->XML_GetInt(ssnode, SAVE_AIRCRAFT_AMOUNT, 0); } craft->itemTypes = l; /* aliencargo */ snode = cgi->XML_GetNode(p, SAVE_AIRCRAFT_ALIENCARGO); if (snode) { craft->alienCargo = new AlienCargo(); if (craft->alienCargo == nullptr) cgi->Com_Error(ERR_DROP, "AIR_LoadAircraftXML: Cannot create AlienCargo object\n"); craft->alienCargo->load(snode); } return true; } /** * @brief resets aircraftSlots' backreference pointers for aircraft * @param[in] aircraft Pointer to the aircraft */ static void AIR_CorrectAircraftSlotPointers (aircraft_t* aircraft) { int i; assert(aircraft); for (i = 0; i < aircraft->maxWeapons; i++) { aircraft->weapons[i].aircraft = aircraft; aircraft->weapons[i].base = nullptr; aircraft->weapons[i].installation = nullptr; } for (i = 0; i < aircraft->maxElectronics; i++) { aircraft->electronics[i].aircraft = aircraft; aircraft->electronics[i].base = nullptr; aircraft->electronics[i].installation = nullptr; } aircraft->shield.aircraft = aircraft; aircraft->shield.base = nullptr; aircraft->shield.installation = nullptr; } bool AIR_LoadXML (xmlNode_t* parent) { xmlNode_t* snode, *ssnode; xmlNode_t* projectiles; int i; /* load phalanx aircraft */ snode = cgi->XML_GetNode(parent, SAVE_AIRCRAFT_PHALANX); for (ssnode = cgi->XML_GetNode(snode, SAVE_AIRCRAFT_AIRCRAFT); ssnode; ssnode = cgi->XML_GetNextNode(ssnode, snode, SAVE_AIRCRAFT_AIRCRAFT)) { aircraft_t craft; if (!AIR_LoadAircraftXML(ssnode, &craft)) return false; assert(craft.homebase); AIR_CorrectAircraftSlotPointers(AIR_Add(craft.homebase, &craft)); } /* load the ufos on geoscape */ snode = cgi->XML_GetNode(parent, SAVE_AIRCRAFT_UFOS); for (i = 0, ssnode = cgi->XML_GetNode(snode, SAVE_AIRCRAFT_AIRCRAFT); i < MAX_UFOONGEOSCAPE && ssnode; ssnode = cgi->XML_GetNextNode(ssnode, snode, SAVE_AIRCRAFT_AIRCRAFT), i++) { if (!AIR_LoadAircraftXML(ssnode, UFO_GetByIDX(i))) return false; ccs.numUFOs++; } /* Load projectiles. */ projectiles = cgi->XML_GetNode(parent, SAVE_AIRCRAFT_PROJECTILES); if (!AIRFIGHT_LoadXML(projectiles)) return false; /* check UFOs - backwards */ for (i = ccs.numUFOs - 1; i >= 0; i--) { aircraft_t* ufo = UFO_GetByIDX(i); if (ufo->time < 0 || ufo->stats[AIR_STATS_SPEED] <= 0) { Com_Printf("AIR_Load: Found invalid ufo entry - remove it - time: %i - speed: %i\n", ufo->time, ufo->stats[AIR_STATS_SPEED]); UFO_RemoveFromGeoscape(ufo); } } return true; } /** * @brief Set the mission pointers for all the aircraft after loading a savegame */ static bool AIR_PostLoadInitMissions (void) { bool success = true; aircraft_t* prevUfo; aircraft_t* ufo; /* PHALANX aircraft */ AIR_Foreach(aircraft) { if (Q_strnull(aircraft->missionID)) continue; aircraft->mission = CP_GetMissionByID(aircraft->missionID); if (!aircraft->mission) { Com_Printf("Aircraft %s (idx: %i) is linked to an invalid mission: %s\n", aircraft->name, aircraft->idx, aircraft->missionID); if (aircraft->status == AIR_MISSION) AIR_AircraftReturnToBase(aircraft); } Mem_Free(aircraft->missionID); aircraft->missionID = nullptr; } /* UFOs */ /** * @todo UFO_RemoveFromGeoscape call doesn't notify other systems (aircraft, base defences, sam sites, radar) * about the removal of the UFO. Destroying UFOs should get a dedicated function with all necessary notify-callbacks called */ prevUfo = nullptr; while ((ufo = UFO_GetNext(prevUfo)) != nullptr) { if (Q_strnull(ufo->missionID)) { Com_Printf("Warning: %s (idx: %i) has no mission assigned, removing it\n", ufo->name, ufo->idx); UFO_RemoveFromGeoscape(ufo); continue; } ufo->mission = CP_GetMissionByID(ufo->missionID); if (!ufo->mission) { Com_Printf("Warning: %s (idx: %i) is linked to an invalid mission %s, removing it\n", ufo->name, ufo->idx, ufo->missionID); UFO_RemoveFromGeoscape(ufo); continue; } ufo->mission->ufo = ufo; Mem_Free(ufo->missionID); ufo->missionID = nullptr; prevUfo = ufo; } return success; } /** * @brief Actions needs to be done after loading the savegame * @sa SAV_GameActionsAfterLoad */ bool AIR_PostLoadInit (void) { return AIR_PostLoadInitMissions(); } /** * @brief Returns true if the current base is able to handle aircraft * @sa B_BaseInit_f * @note Hangar must be accessible during base attack to make aircraft lift off and to equip soldiers. */ bool AIR_AircraftAllowed (const base_t* base) { return B_GetBuildingStatus(base, B_HANGAR) || B_GetBuildingStatus(base, B_SMALL_HANGAR); } /** * @param aircraft The aircraft to check * @return @c true if the given aircraft can go on interceptions */ bool AIR_CanIntercept (const aircraft_t* aircraft) { if (aircraft->status == AIR_NONE || aircraft->status == AIR_CRASHED) return false; /* if dependencies of hangar are missing, you can't send aircraft */ if (aircraft->size == AIRCRAFT_SMALL && !B_GetBuildingStatus(aircraft->homebase, B_SMALL_HANGAR)) return false; if (aircraft->size == AIRCRAFT_LARGE && !B_GetBuildingStatus(aircraft->homebase, B_HANGAR)) return false; /* we need a pilot to intercept */ if (AIR_GetPilot(aircraft) == nullptr) return false; return true; } /** * @brief Checks the parsed aircraft for errors * @return false if there are errors - true otherwise */ bool AIR_ScriptSanityCheck (void) { int i, j, k, error = 0; aircraft_t* a; for (i = 0, a = ccs.aircraftTemplates; i < ccs.numAircraftTemplates; i++, a++) { if (a->name[0] == '\0') { error++; Com_Printf("...... aircraft '%s' has no name\n", a->id); } if (!a->defaultName) { error++; Com_Printf("...... aircraft '%s' has no defaultName\n", a->id); } /* check that every weapons fits slot */ for (j = 0; j < a->maxWeapons - 1; j++) if (a->weapons[j].item && AII_GetItemWeightBySize(a->weapons[j].item) > a->weapons[j].size) { error++; Com_Printf("...... aircraft '%s' has an item (%s) too heavy for its slot\n", a->id, a->weapons[j].item->id); } /* check that every slots has a different location for PHALANX aircraft (not needed for UFOs) */ if (a->type != AIRCRAFT_UFO) { for (j = 0; j < a->maxWeapons - 1; j++) { const itemPos_t var = a->weapons[j].pos; for (k = j + 1; k < a->maxWeapons; k++) if (var == a->weapons[k].pos) { error++; Com_Printf("...... aircraft '%s' has 2 weapons slots at the same location\n", a->id); } } for (j = 0; j < a->maxElectronics - 1; j++) { const itemPos_t var = a->electronics[j].pos; for (k = j + 1; k < a->maxElectronics; k++) if (var == a->electronics[k].pos) { error++; Com_Printf("...... aircraft '%s' has 2 electronics slots at the same location\n", a->id); } } } } return !error; } /** * @brief Removes a soldier from an aircraft. * @param[in,out] employee The soldier to be removed from the aircraft. * @param[in,out] aircraft The aircraft to remove the soldier from. * Use @c nullptr to remove the soldier from any aircraft. * @sa AIR_AddEmployee */ bool AIR_RemoveEmployee (Employee* employee, aircraft_t* aircraft) { if (!employee) return false; /* If no aircraft is given we search if he is in _any_ aircraft and set * the aircraft pointer to it. */ if (!aircraft) { AIR_Foreach(acTemp) { if (AIR_IsEmployeeInAircraft(employee, acTemp)) { aircraft = acTemp; break; } } if (!aircraft) return false; } Com_DPrintf(DEBUG_CLIENT, "AIR_RemoveEmployee: base: %i - aircraft->idx: %i\n", aircraft->homebase ? aircraft->homebase->idx : -1, aircraft->idx); if (AIR_GetPilot(aircraft) == employee) { #ifdef DEBUG if (employee->getType() != EMPL_PILOT) Com_Printf("Warning: pilot of aircraf %i is not a qualified pilot (ucn: %i)\n", aircraft->idx, employee->chr.ucn); #endif return AIR_SetPilot(aircraft, nullptr); } return cgi->LIST_Remove(&aircraft->acTeam, employee); } /** * @brief Tells you if an employee is assigned to an aircraft. * @param[in] employee The employee to search for. * @param[in] aircraft The aircraft to search the employee in. Use @c nullptr to * check if the soldier is in @b any aircraft. * @return true if the soldier was found in the aircraft otherwise false. */ const aircraft_t* AIR_IsEmployeeInAircraft (const Employee* employee, const aircraft_t* aircraft) { if (!employee) return nullptr; if (employee->transfer) return nullptr; /* If no aircraft is given we search if he is in _any_ aircraft and return true if that's the case. */ if (!aircraft) { AIR_Foreach(anyAircraft) { if (AIR_IsEmployeeInAircraft(employee, anyAircraft)) return anyAircraft; } return nullptr; } if (employee->isPilot()) { if (AIR_GetPilot(aircraft) == employee) return aircraft; return nullptr; } if (AIR_IsInAircraftTeam(aircraft, employee)) return aircraft; return nullptr; } /** * @brief Removes all soldiers from an aircraft. * @param[in,out] aircraft The aircraft to remove the soldiers from. * @sa AIR_RemoveEmployee */ void AIR_RemoveEmployees (aircraft_t &aircraft) { LIST_Foreach(aircraft.acTeam, Employee, employee) { /* use global aircraft index here */ AIR_RemoveEmployee(employee, &aircraft); } /* Remove pilot */ AIR_SetPilot(&aircraft, nullptr); if (AIR_GetTeamSize(&aircraft) > 0) cgi->Com_Error(ERR_DROP, "AIR_RemoveEmployees: Error, there went something wrong with soldier-removing from aircraft."); } /** * @brief Move all the equipment carried by the team on the aircraft into the given equipment * @param[in] aircraft The craft with the team (and thus equipment) onboard. * @param[out] ed The equipment definition which will receive all the stuff from the aircraft-team. */ void AIR_MoveEmployeeInventoryIntoStorage (const aircraft_t &aircraft, equipDef_t &ed) { if (AIR_GetTeamSize(&aircraft) == 0) { Com_DPrintf(DEBUG_CLIENT, "AIR_MoveEmployeeInventoryIntoStorage: No team to remove equipment from.\n"); return; } LIST_Foreach(aircraft.acTeam, Employee, employee) { const Container* cont = nullptr; while ((cont = employee->chr.inv.getNextCont(cont, true))) { Item* ic = cont->getNextItem(nullptr); while (ic) { const Item item = *ic; const objDef_t* type = item.def(); Item* next = ic->getNext(); ed.numItems[type->idx]++; if (item.getAmmoLeft() && type->isReloadable()) { assert(item.ammoDef()); /* Accumulate loose ammo into clips */ ed.addClip(&item); /* does not delete the item */ } ic = next; } } } } /** * @brief Assigns a soldier to an aircraft. * @param[in] employee The employee to be assigned to the aircraft. * @param[in] aircraft What aircraft to assign employee to. * @return returns true if a soldier could be assigned to the aircraft. * @sa AIR_RemoveEmployee * @sa AIR_AddToAircraftTeam */ bool AIR_AddEmployee (Employee* employee, aircraft_t* aircraft) { if (!employee || !aircraft) return false; if (AIR_GetTeamSize(aircraft) < aircraft->maxTeamSize) { /* Check whether the soldier is already on another aircraft */ if (AIR_IsEmployeeInAircraft(employee, nullptr)) return false; /* Assign the soldier to the aircraft. */ return AIR_AddToAircraftTeam(aircraft, employee); } return false; } /** * @brief Assigns initial team of soldiers to aircraft * @param[in,out] aircraft soldiers to add to */ void AIR_AssignInitial (aircraft_t* aircraft) { int count; base_t* base; if (!aircraft) { Com_Printf("AIR_AssignInitial: No aircraft given\n"); return; } base = aircraft->homebase; assert(base); count = 0; E_Foreach(EMPL_SOLDIER, employee) { if (count >= aircraft->maxTeamSize) break; if (employee->baseHired != base) continue; if (AIR_AddEmployee(employee, aircraft)) count++; } } /** * @brief Init actions for aircraft-subsystem */ void AIR_InitStartup (void) { AIR_InitCallbacks(); #ifdef DEBUG cgi->Cmd_AddCommand("debug_listaircraftsample", AIR_ListAircraftSamples_f, "Show aircraft parameter on game console"); cgi->Cmd_AddCommand("debug_listaircraft", AIR_ListAircraft_f, "Debug function to list all aircraft in all bases"); cgi->Cmd_AddCommand("debug_listaircraftidx", AIR_ListCraftIndexes_f, "Debug function to list local/global aircraft indexes"); #endif } /** * @brief Closing actions for aircraft-subsystem */ void AIR_Shutdown (void) { AIR_Foreach(craft) { AIR_ResetAircraftTeam(craft); if (craft->alienCargo != nullptr) { delete craft->alienCargo; craft->alienCargo = nullptr; } } cgi->LIST_Delete(&ccs.aircraft); AIR_ShutdownCallbacks(); #ifdef DEBUG cgi->Cmd_RemoveCommand("debug_listaircraftsample"); cgi->Cmd_RemoveCommand("debug_listaircraft"); cgi->Cmd_RemoveCommand("debug_listaircraftidx"); #endif }