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