1 #include "Items.h"
2 #include "Handle_Items.h"
3 #include "Overhead.h"
4 #include "Structure.h"
5 #include "Weapons.h"
6 #include "Points.h"
7 #include "TileDef.h"
8 #include "WorldDef.h"
9 #include "Font_Control.h"
10 #include "Render_Dirty.h"
11 #include "World_Items.h"
12 #include "Isometric_Utils.h"
13 #include "Sys_Globals.h"
14 #include "StrategicMap.h"
15 #include "Campaign_Types.h"
16 #include "Random.h"
17 #include "Action_Items.h"
18 #include "GameSettings.h"
19 #include "Quests.h"
20 #include "Soldier_Profile.h"
21 #include "MemMan.h"
22 #include "FileMan.h"
23 #include "ContentManager.h"
24 #include "GameInstance.h"
25 #include "MagazineModel.h"
26 #include "WeaponModels.h"
27
28 #include <algorithm>
29 #include <stdexcept>
30 #include <vector>
31
32 //Global dynamic array of all of the items in a loaded map.
33 std::vector<WORLDITEM> gWorldItems;
34
35 std::vector<WORLDBOMB> gWorldBombs;
36
37
GetFreeWorldBombIndex(void)38 static INT32 GetFreeWorldBombIndex(void)
39 {
40 INT32 idx;
41
42 Assert(gWorldBombs.size() <= INT32_MAX);
43 for (idx = 0; idx < static_cast<INT32>(gWorldBombs.size()); idx++)
44 {
45 if (!gWorldBombs[idx].fExists) return idx;
46 }
47
48 gWorldBombs.push_back(WORLDBOMB{});
49
50 // Return uiCount.....
51 return( idx );
52 }
53
54
AddBombToWorld(INT32 iItemIndex)55 static INT32 AddBombToWorld(INT32 iItemIndex)
56 {
57 UINT32 iBombIndex;
58
59 iBombIndex = GetFreeWorldBombIndex( );
60
61 //Add the new world item to the table.
62 gWorldBombs[ iBombIndex ].fExists = TRUE;
63 gWorldBombs[ iBombIndex ].iItemIndex = iItemIndex;
64
65 return ( iBombIndex );
66 }
67
68
RemoveBombFromWorldByItemIndex(INT32 iItemIndex)69 static void RemoveBombFromWorldByItemIndex(INT32 iItemIndex)
70 {
71 // Find the world bomb which corresponds with a particular world item, then
72 // remove the world bomb from the table.
73 FOR_EACH_WORLD_BOMB(wb)
74 {
75 if (wb.iItemIndex != iItemIndex) continue;
76
77 wb.fExists = FALSE;
78 return;
79 }
80 }
81
82
FindWorldItemForBombInGridNo(const INT16 sGridNo,const INT8 bLevel)83 INT32 FindWorldItemForBombInGridNo(const INT16 sGridNo, const INT8 bLevel)
84 {
85 CFOR_EACH_WORLD_BOMB(wb)
86 {
87 WORLDITEM const& wi = GetWorldItem(wb.iItemIndex);
88 if (wi.sGridNo != sGridNo || wi.ubLevel != bLevel) continue;
89
90 return wb.iItemIndex;
91 }
92 throw std::logic_error("Cannot find bomb item");
93 }
94
95
FindPanicBombsAndTriggers(void)96 void FindPanicBombsAndTriggers(void)
97 {
98 // This function searches the bomb table to find panic-trigger-tuned bombs and triggers
99 CFOR_EACH_WORLD_BOMB(wb)
100 {
101 WORLDITEM const& wi = GetWorldItem(wb.iItemIndex);
102 OBJECTTYPE const& o = wi.o;
103
104 INT8 bPanicIndex;
105 switch (o.bFrequency)
106 {
107 case PANIC_FREQUENCY: bPanicIndex = 0; break;
108 case PANIC_FREQUENCY_2: bPanicIndex = 1; break;
109 case PANIC_FREQUENCY_3: bPanicIndex = 2; break;
110 default: continue;
111 }
112
113 if (o.usItem == SWITCH)
114 {
115 INT16 sGridNo = wi.sGridNo;
116 const STRUCTURE* const switch_ = FindStructure(sGridNo, STRUCTURE_SWITCH);
117 if (switch_)
118 {
119 switch (switch_->ubWallOrientation)
120 {
121 case INSIDE_TOP_LEFT:
122 case OUTSIDE_TOP_LEFT: sGridNo += DirectionInc(SOUTH); break;
123 case INSIDE_TOP_RIGHT:
124 case OUTSIDE_TOP_RIGHT: sGridNo += DirectionInc(EAST); break;
125
126 default: break;
127 }
128 }
129
130 gTacticalStatus.fPanicFlags |= PANIC_TRIGGERS_HERE;
131 gTacticalStatus.sPanicTriggerGridNo[bPanicIndex] = sGridNo;
132 gTacticalStatus.ubPanicTolerance[bPanicIndex] = o.ubTolerance;
133 if (o.fFlags & OBJECT_ALARM_TRIGGER)
134 {
135 gTacticalStatus.bPanicTriggerIsAlarm[bPanicIndex] = TRUE;
136 }
137 if (bPanicIndex + 1 == NUM_PANIC_TRIGGERS) return;
138 }
139 else
140 {
141 gTacticalStatus.fPanicFlags |= PANIC_BOMBS_HERE;
142 }
143 }
144 }
145
146
GetFreeWorldItemIndex(void)147 static INT32 GetFreeWorldItemIndex(void)
148 {
149 INT32 iItemIndex;
150
151 Assert(gWorldItems.size() <= INT32_MAX);
152 for (iItemIndex = 0; iItemIndex < static_cast<INT32>(gWorldItems.size()); iItemIndex++)
153 {
154 if (!gWorldItems[iItemIndex].fExists) return iItemIndex;
155 }
156
157 gWorldItems.push_back(WORLDITEM{});
158 return iItemIndex;
159 }
160
161
GetNumUsedWorldItems(void)162 static UINT32 GetNumUsedWorldItems(void)
163 {
164 UINT32 count = 0;
165 CFOR_EACH_WORLD_ITEM(wi) ++count;
166 return count;
167 }
168
169
AddItemToWorld(INT16 sGridNo,const OBJECTTYPE * const pObject,const UINT8 ubLevel,const UINT16 usFlags,const INT8 bRenderZHeightAboveLevel,const INT8 bVisible)170 INT32 AddItemToWorld(INT16 sGridNo, const OBJECTTYPE* const pObject, const UINT8 ubLevel, const UINT16 usFlags, const INT8 bRenderZHeightAboveLevel, const INT8 bVisible)
171 {
172 // ATE: Check if the gridno is OK
173 if (sGridNo == NOWHERE)
174 {
175 // Display warning.....
176 SLOGW("Item %d was given invalid grid location %d. Please report", pObject->usItem, sGridNo);
177 return -1;
178 }
179
180 const UINT32 iItemIndex = GetFreeWorldItemIndex();
181 WORLDITEM& wi = GetWorldItem(iItemIndex);
182
183 //Add the new world item to the table.
184 wi.fExists = TRUE;
185 wi.sGridNo = sGridNo;
186 wi.ubLevel = ubLevel;
187 wi.usFlags = usFlags;
188 wi.bVisible = bVisible;
189 wi.bRenderZHeightAboveLevel = bRenderZHeightAboveLevel;
190 wi.o = *pObject;
191
192 // Add a bomb reference if needed
193 if (usFlags & WORLD_ITEM_ARMED_BOMB)
194 {
195 if (AddBombToWorld(iItemIndex) == -1) return -1;
196 }
197
198 return iItemIndex;
199 }
200
201
RemoveItemFromWorld(const INT32 iItemIndex)202 void RemoveItemFromWorld(const INT32 iItemIndex)
203 {
204 WORLDITEM& wi = GetWorldItem(iItemIndex);
205 if (!wi.fExists) return;
206
207 // If it's a bomb, remove the appropriate entry from the bomb table
208 if (wi.usFlags & WORLD_ITEM_ARMED_BOMB)
209 {
210 RemoveBombFromWorldByItemIndex(iItemIndex);
211 }
212 wi.fExists = FALSE;
213 }
214
215
TrashWorldItems()216 void TrashWorldItems()
217 {
218 FOR_EACH_WORLD_ITEM(wi)
219 {
220 RemoveItemFromPool(wi);
221 }
222 gWorldItems.clear();
223 gWorldBombs.clear();
224 }
225
226
SaveWorldItemsToMap(HWFILE const f)227 void SaveWorldItemsToMap(HWFILE const f)
228 {
229 UINT32 const n_actual_world_items = GetNumUsedWorldItems();
230 FileWrite(f, &n_actual_world_items, sizeof(n_actual_world_items));
231
232 CFOR_EACH_WORLD_ITEM(wi) FileWrite(f, &wi, sizeof(WORLDITEM));
233 }
234
235
236 static void DeleteWorldItemsBelongingToQueenIfThere(void);
237 static void DeleteWorldItemsBelongingToTerroristsWhoAreNotThere(void);
238
239
LoadWorldItemsFromMap(HWFILE const f)240 void LoadWorldItemsFromMap(HWFILE const f)
241 {
242 // If any world items exist, we must delete them now
243 TrashWorldItems();
244
245 // Read the number of items that were saved in the map
246 UINT32 n_world_items;
247 auto itemReplacements = GCM->getMapItemReplacements();
248 FileRead(f, &n_world_items, sizeof(n_world_items));
249
250 if (gTacticalStatus.uiFlags & LOADING_SAVED_GAME && !gfEditMode)
251 {
252 // The sector has already been visited. The items are saved in a different
253 // format that will be loaded later on. So, all we need to do is skip the
254 // data entirely.
255 FileSeek(f, sizeof(WORLDITEM) * n_world_items, FILE_SEEK_FROM_CURRENT);
256 return;
257 }
258
259 for (UINT32 n = n_world_items; n != 0; --n)
260 {
261 // Add all of the items to the world indirectly through AddItemToPool, but
262 // only if the chance associated with them succeed.
263 WORLDITEM wi;
264 FileRead(f, &wi, sizeof(wi));
265 OBJECTTYPE& o = wi.o;
266
267 if (o.usItem == OWNERSHIP) wi.ubNonExistChance = 0;
268
269 if (!gfEditMode && PreRandom(100) < wi.ubNonExistChance) continue;
270
271 if (!gfEditMode)
272 {
273 // Check for matching item existance modes and only add if there is a match
274 if (wi.usFlags & (gGameOptions.fSciFi ? WORLD_ITEM_SCIFI_ONLY : WORLD_ITEM_REALISTIC_ONLY)) continue;
275
276 // Check if we have a item replacement mapping for this item
277 if (itemReplacements.find(o.usItem) != itemReplacements.end())
278 {
279 auto item = itemReplacements.at(o.usItem);
280 if (item == 0)
281 {
282 STLOGW("Map item #{} removed", o.usItem);
283 continue;
284 }
285
286 STLOGD("Map item #{} replaced by #{}", o.usItem, item);
287 o.usItem = item;
288 }
289
290 const ItemModel* item = GCM->getItem(o.usItem);
291 if (item->getFlags() & ITEM_NOT_EDITOR) {
292 // This item is not placable by Editor. Maybe the map was created for a different item set.
293 STLOGW("Skipping non-Editor item #{}({}) at gridNo {}", item->getItemIndex(), item->getInternalName(), wi.sGridNo);
294 continue;
295 }
296
297 if (!gGameOptions.fGunNut)
298 {
299 // do replacements?
300 const WeaponModel *weapon = item->asWeapon();
301 const MagazineModel *mag = item->asAmmo();
302 if (weapon && weapon->isInBigGunList())
303 {
304 const WeaponModel *replacement = GCM->getWeaponByName(item->asWeapon()->getStandardReplacement());
305
306 // everything else can be the same? no.
307 INT8 const ammo = o.ubGunShotsLeft;
308 INT8 new_ammo = replacement->ubMagSize * ammo / weapon->ubMagSize;
309 if (new_ammo == 0 && ammo > 0) new_ammo = 1;
310 o.usItem = replacement->getItemIndex();
311 o.ubGunShotsLeft = new_ammo;
312 }
313 else if (mag && mag->isInBigGunList())
314 {
315 const MagazineModel *replacement = GCM->getMagazineByName(mag->getStandardReplacement());
316
317 // Go through status values and scale up/down
318 UINT8 const mag_size = mag->capacity;
319 UINT8 const new_mag_size = replacement->capacity;
320 for (UINT8 i = 0; i != o.ubNumberOfObjects; ++i)
321 {
322 o.bStatus[i] = o.bStatus[i] * new_mag_size / (mag_size? mag_size: 1);
323 }
324
325 // then replace item #
326 o.usItem = replacement->getItemIndex();
327 }
328 }
329 }
330
331 switch (o.usItem)
332 {
333 case ACTION_ITEM:
334 // If we are loading a pit, they are typically loaded without being armed.
335 if (o.bActionValue == ACTION_ITEM_SMALL_PIT ||
336 o.bActionValue == ACTION_ITEM_LARGE_PIT)
337 {
338 wi.usFlags &= ~WORLD_ITEM_ARMED_BOMB;
339 wi.bVisible = BURIED;
340 o.bDetonatorType = 0;
341 }
342 break;
343
344 case MINE:
345 case TRIP_FLARE:
346 case TRIP_KLAXON:
347 if (wi.bVisible == HIDDEN_ITEM && o.bTrap > 0)
348 {
349 ArmBomb(&o, BOMB_PRESSURE);
350 wi.usFlags |= WORLD_ITEM_ARMED_BOMB;
351 // this is coming from the map so the enemy must know about it.
352 gpWorldLevelData[wi.sGridNo].uiFlags |= MAPELEMENT_ENEMY_MINE_PRESENT;
353 }
354 break;
355 }
356
357 // All armed bombs are buried
358 if (wi.usFlags & WORLD_ITEM_ARMED_BOMB) wi.bVisible = BURIED;
359
360 INT32 const item_idx = AddItemToPool(wi.sGridNo, &o, static_cast<Visibility>(wi.bVisible), wi.ubLevel, wi.usFlags, wi.bRenderZHeightAboveLevel);
361 GetWorldItem(item_idx).ubNonExistChance = wi.ubNonExistChance;
362 }
363
364 if (!gfEditMode)
365 {
366 DeleteWorldItemsBelongingToTerroristsWhoAreNotThere();
367 if (gWorldSectorX == 3 && gWorldSectorY == MAP_ROW_P && gbWorldSectorZ == 1)
368 {
369 DeleteWorldItemsBelongingToQueenIfThere();
370 }
371 }
372 }
373
374
DeleteWorldItemsBelongingToTerroristsWhoAreNotThere(void)375 static void DeleteWorldItemsBelongingToTerroristsWhoAreNotThere(void)
376 {
377 // only do this after Carmen has talked to player and terrorists have been placed
378 //if ( CheckFact( FACT_CARMEN_EXPLAINED_DEAL, 0 ) == TRUE )
379 {
380 CFOR_EACH_WORLD_ITEM(wi)
381 {
382 // loop through all items, look for ownership
383 if (wi.o.usItem != OWNERSHIP) continue;
384
385 const ProfileID pid = wi.o.ubOwnerProfile;
386 // if owner is a terrorist
387 if (!IsProfileATerrorist(pid)) continue;
388
389 MERCPROFILESTRUCT const& p = GetProfile(pid);
390 // and they were not set in the current sector
391 if (p.sSectorX == gWorldSectorX && p.sSectorY == gWorldSectorY) continue;
392
393 // then all items in this location should be deleted
394 const INT16 sGridNo = wi.sGridNo;
395 const UINT8 ubLevel = wi.ubLevel;
396 FOR_EACH_WORLD_ITEM(owned_item)
397 {
398 if (owned_item.sGridNo == sGridNo && owned_item.ubLevel == ubLevel)
399 {
400 RemoveItemFromPool(owned_item);
401 }
402 }
403 }
404 }
405 // else the terrorists haven't been placed yet!
406 }
407
408
DeleteWorldItemsBelongingToQueenIfThere(void)409 static void DeleteWorldItemsBelongingToQueenIfThere(void)
410 {
411 MERCPROFILESTRUCT& q = GetProfile(QUEEN);
412
413 if (q.sSectorX != gWorldSectorX ||
414 q.sSectorY != gWorldSectorY ||
415 q.bSectorZ != gbWorldSectorZ)
416 {
417 return;
418 }
419
420 CFOR_EACH_WORLD_ITEM(wi)
421 {
422 // Look for items belonging to the queen
423 if (wi.o.usItem != OWNERSHIP) continue;
424 if (wi.o.ubOwnerProfile != QUEEN) continue;
425
426 // Delete all items on this tile
427 const INT16 sGridNo = wi.sGridNo;
428 const UINT8 ubLevel = wi.ubLevel;
429 FOR_EACH_WORLD_ITEM(item)
430 {
431 if (item.sGridNo != sGridNo) continue;
432 if (item.ubLevel != ubLevel) continue;
433
434 // Upgrade equipment
435 switch (item.o.usItem)
436 {
437 case AUTO_ROCKET_RIFLE:
438 {
439 // Give her auto rifle
440 INT8 const bSlot = FindObjectInSoldierProfile(q, ROCKET_RIFLE);
441 if (bSlot != NO_SLOT) q.inv[bSlot] = AUTO_ROCKET_RIFLE;
442 break;
443 }
444
445 case SPECTRA_HELMET_18: q.inv[HELMETPOS] = SPECTRA_HELMET_18; break;
446 case SPECTRA_VEST_18: q.inv[VESTPOS] = SPECTRA_VEST_18; break;
447 case SPECTRA_LEGGINGS_18: q.inv[LEGPOS] = SPECTRA_LEGGINGS_18; break;
448
449 default: break;
450 }
451 RemoveItemFromPool(item);
452 }
453 }
454 }
455
456
457 // Refresh item pools
RefreshWorldItemsIntoItemPools(const std::vector<WORLDITEM> & items)458 void RefreshWorldItemsIntoItemPools(const std::vector<WORLDITEM>& items)
459 {
460 for (const WORLDITEM& wi : items)
461 {
462 if (!wi.fExists) continue;
463 OBJECTTYPE o = wi.o; // XXX AddItemToPool() may alter the object
464 AddItemToPool(wi.sGridNo, &o, static_cast<Visibility>(wi.bVisible), wi.ubLevel, wi.usFlags, wi.bRenderZHeightAboveLevel);
465 }
466 }
467
468
469 #ifdef WITH_UNITTESTS
470 #undef FAIL
471 #include "gtest/gtest.h"
472
TEST(WorldItems,asserts)473 TEST(WorldItems, asserts)
474 {
475 EXPECT_EQ(sizeof(WORLDITEM), 52u);
476 }
477
478 #endif
479