0
0
mirror of https://github.com/sp-tarkov/server.git synced 2025-02-12 17:30:42 -05:00

Apply Biome Formatting

This is the result of running `npm run format` which applies the Biome formatting rules. Rejoice!
This commit is contained in:
Refringe 2024-07-23 11:12:53 -04:00
parent 7c76342ee2
commit 5740774a46
Signed by: Refringe
SSH Key Fingerprint: SHA256:t865XsQpfTeqPRBMN2G6+N8wlDjkgUCZF3WGW6O9N/k
755 changed files with 22787 additions and 31002 deletions

View File

@ -15,14 +15,10 @@
"keepClassNames": true,
"baseUrl": "./",
"paths": {
"@spt/*": [
"src/*"
]
"@spt/*": ["src/*"]
}
},
"exclude": [
"node_modules/"
],
"exclude": ["node_modules/"],
"module": {
"type": "commonjs",
"strict": false,

View File

@ -104,12 +104,7 @@
"max": 50
}
},
"armorLevelWhitelist": [
0,
4,
5,
6
],
"armorLevelWhitelist": [0, 4, 5, 6],
"allowBossItems": false
},
"weaponArmor": {
@ -181,13 +176,7 @@
"max": 50
}
},
"armorLevelWhitelist": [
0,
3,
4,
5,
6
],
"armorLevelWhitelist": [0, 3, 4, 5, 6],
"allowBossItems": false
},
"foodMedical": {
@ -267,9 +256,7 @@
"max": 50
}
},
"armorLevelWhitelist": [
0
],
"armorLevelWhitelist": [0],
"allowBossItems": false
},
"barter": {
@ -350,9 +337,7 @@
"max": 50
}
},
"armorLevelWhitelist": [
0
],
"armorLevelWhitelist": [0],
"allowBossItems": false
}
}

View File

@ -277,12 +277,9 @@
},
"chanceAssaultScavHasPlayerScavName": 10,
"secureContainerAmmoStackCount": 20,
"botRolesWithDogTags": [
"pmcbear",
"pmcusec"
],
"botRolesWithDogTags": ["pmcbear", "pmcusec"],
"revenge": {
"pmcBot": ["pmcBot","gifter"],
"pmcBot": ["pmcBot", "gifter"],
"arenaFighter": ["pmcBot", "gifter"],
"arenaFighterEvent": ["pmcBot", "gifter"],
"bossKnight": ["exUsec", "gifter", "bossKnight", "followerBigPipe", "followerBirdEye"],
@ -495,9 +492,7 @@
"lightIsActiveNightChancePercent": 90,
"laserIsActiveChancePercent": 85,
"forceStock": true,
"weaponSlotIdsToMakeRequired": [
"mod_reciever"
]
"weaponSlotIdsToMakeRequired": ["mod_reciever"]
},
"bossbully": {
"nvgIsActiveChanceDayPercent": 10,
@ -776,9 +771,7 @@
"scopeLimit": 1,
"lightLaserLimit": 1
},
"weaponSlotIdsToMakeRequired": [
"mod_reciever"
]
"weaponSlotIdsToMakeRequired": ["mod_reciever"]
},
"peacemaker": {
"nvgIsActiveChanceDayPercent": 10,
@ -926,11 +919,7 @@
"laserIsActiveChancePercent": 85,
"forceOnlyArmoredRigWhenNoArmor": true,
"filterPlatesByLevel": true,
"weaponSlotIdsToMakeRequired": [
"mod_reciever",
"mod_stock",
"mod_muzzle"
],
"weaponSlotIdsToMakeRequired": ["mod_reciever", "mod_stock", "mod_muzzle"],
"randomisation": [
{
"levelRange": {
@ -1148,11 +1137,7 @@
"Scabbard": 100,
"TacticalVest": 90
},
"randomisedArmorSlots": [
"Headwear",
"ArmorVest",
"TacticalVest"
],
"randomisedArmorSlots": ["Headwear", "ArmorVest", "TacticalVest"],
"randomisedWeaponModSlots": [
"mod_scope",
"mod_scope_000",
@ -1250,11 +1235,7 @@
"SecondPrimaryWeapon": 10,
"FaceCover": 50
},
"randomisedArmorSlots": [
"Headwear",
"TacticalVest",
"ArmorVest"
],
"randomisedArmorSlots": ["Headwear", "TacticalVest", "ArmorVest"],
"randomisedWeaponModSlots": [
"mod_scope",
"mod_scope_000",
@ -1321,11 +1302,7 @@
"mod_muzzle_000": 95,
"mod_muzzle_001": 95
},
"randomisedArmorSlots": [
"Headwear",
"TacticalVest",
"ArmorVest"
],
"randomisedArmorSlots": ["Headwear", "TacticalVest", "ArmorVest"],
"randomisedWeaponModSlots": [
"mod_scope",
"mod_scope_000",
@ -1397,30 +1374,15 @@
"65392f611406374f82152ba5",
"653931da5db71d30ab1d6296"
],
"mod_nvg": [
"5c11046cd174af02a012e42b"
],
"mod_reciever": [
"5d4405aaa4b9361e6a4e6bd3"
],
"mod_stock": [
"5cde739cd7f00c0010373bd3"
],
"mod_rear_sight": [
"5a0ed824fcdbcb0176308b0d"
],
"mod_front_sight": [
"5a0f096dfcdbcb0176308b15"
]
"mod_nvg": ["5c11046cd174af02a012e42b"],
"mod_reciever": ["5d4405aaa4b9361e6a4e6bd3"],
"mod_stock": ["5cde739cd7f00c0010373bd3"],
"mod_rear_sight": ["5a0ed824fcdbcb0176308b0d"],
"mod_front_sight": ["5a0f096dfcdbcb0176308b15"]
},
"cartridge": {
"Caliber23x75": [
"5e85a9f4add9fe03027d9bf1"
],
"Caliber127x55": [
"5cadf6e5ae921500113bb973",
"5cadf6ddae9215051e1c23b2"
]
"Caliber23x75": ["5e85a9f4add9fe03027d9bf1"],
"Caliber127x55": ["5cadf6e5ae921500113bb973", "5cadf6ddae9215051e1c23b2"]
}
}
],
@ -2677,10 +2639,7 @@
"569668774bdc2da2298b4568": 0,
"5696686a4bdc2da3298b456a": 0
},
"walletTplPool": [
"5783c43d2459774bbe137486",
"60b0f6c058e0b0481a09ad11"
]
"walletTplPool": ["5783c43d2459774bbe137486", "60b0f6c058e0b0481a09ad11"]
},
"currencyStackSize": {
"default": {
@ -2735,7 +2694,14 @@
}
}
},
"lowProfileGasBlockTpls": ["61702f1b67085e45ef140b26", "5dfa3d45dfc58d14537c20b0", "5bb20dcad4351e3bac1212da", "56eabcd4d2720b66698b4574", "6065dc8a132d4d12c81fd8e3", "55d4af3a4bdc2d972f8b456f"],
"lowProfileGasBlockTpls": [
"61702f1b67085e45ef140b26",
"5dfa3d45dfc58d14537c20b0",
"5bb20dcad4351e3bac1212da",
"56eabcd4d2720b66698b4574",
"6065dc8a132d4d12c81fd8e3",
"55d4af3a4bdc2d972f8b456f"
],
"disableLootOnBotTypes": [],
"assaultToBossConversion": {
"bossConvertEnabled": false,

File diff suppressed because it is too large Load Diff

View File

@ -13,15 +13,7 @@
"save": {
"loot": true
},
"carExtracts": [
"Dorms V-Ex",
"PP Exfil",
"V-Ex_light",
"South V-Ex",
"E7_car",
"Sandbox_VExit",
"Shorl_V-Ex"
],
"carExtracts": ["Dorms V-Ex", "PP Exfil", "V-Ex_light", "South V-Ex", "E7_car", "Sandbox_VExit", "Shorl_V-Ex"],
"coopExtracts": [
"Interchange Cooperation",
"tunnel_shared",

View File

@ -3,15 +3,8 @@
"54cb50c76803fa8b248b4571": 75,
"54cb57776803fa99248b456e": 85
},
"blacklistedEquipment": [
"SpecialSlot1",
"SpecialSlot2",
"SpecialSlot3"
],
"slotIdsToAlwaysRemove": [
"cartridges",
"patron_in_weapon"
],
"blacklistedEquipment": ["SpecialSlot1", "SpecialSlot2", "SpecialSlot3"],
"slotIdsToAlwaysRemove": ["cartridges", "patron_in_weapon"],
"returnTimeOverrideSeconds": 0,
"runIntervalSeconds": 600,
"minAttachmentRoublePriceToBeTaken": 2000,

View File

@ -49,10 +49,7 @@
"6241c2c2117ad530666a5108",
"5580239d4bdc2de7118b4583"
],
"lootableItemBlacklist": [
"660bbc47c38b837877075e47",
"660bc341c38b837877075e4c"
],
"lootableItemBlacklist": ["660bbc47c38b837877075e47", "660bc341c38b837877075e4c"],
"rewardItemBlacklist": [
"58ac60eb86f77401897560ff",
"5e997f0b86f7741ac73993e2",

View File

@ -56,9 +56,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -79,9 +77,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -104,9 +100,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "2"
}
],
@ -127,9 +121,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "2"
}
],
@ -152,9 +144,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "2"
}
],
@ -175,9 +165,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "2"
}
],
@ -198,9 +186,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "2"
}
],
@ -221,9 +207,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "2"
}
],
@ -244,9 +228,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "2"
}
],
@ -267,9 +249,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "2"
}
],
@ -292,9 +272,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -315,9 +293,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -338,9 +314,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -361,9 +335,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -384,9 +356,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -407,9 +377,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -432,9 +400,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -455,9 +421,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -478,9 +442,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "0"
}
],
@ -501,9 +463,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -524,9 +484,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -547,9 +505,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "0"
}
],
@ -572,9 +528,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -595,9 +549,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -620,9 +572,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -643,9 +593,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -668,9 +616,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -691,9 +637,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -714,9 +658,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -737,9 +679,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "1"
}
],
@ -762,9 +702,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "2"
}
],
@ -785,9 +723,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "2"
}
],
@ -810,9 +746,7 @@
"Supports": [
{
"BossEscortType": "pmcBEAR",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "2"
}
],
@ -833,9 +767,7 @@
"Supports": [
{
"BossEscortType": "pmcUSEC",
"BossEscortDifficult": [
"normal"
],
"BossEscortDifficult": ["normal"],
"BossEscortAmount": "2"
}
],
@ -1047,9 +979,7 @@
}
},
"openZones": {
"bigmap": [
"ZoneScavBase"
]
"bigmap": ["ZoneScavBase"]
},
"forcedLootSingleSpawnById": {
"bigmap": [
@ -1058,9 +988,7 @@
"64e74a3d4d49d23b2c39d319",
"6614230055afee107f05e998"
],
"interchange": [
"64e74a5ac2b4f829615ec336"
],
"interchange": ["64e74a5ac2b4f829615ec336"],
"lighthouse": [
"6331bb0d1aa9f42b804997a6",
"6398a0861c712b1e1d4dadf1",
@ -1068,10 +996,7 @@
"64e74a64aac4cd0a7264ecdf",
"661666458c2aa9cb1602503b"
],
"rezervbase": [
"64e74a4baac4cd0a7264ecdd",
"6398a072e301557ae24cec92"
],
"rezervbase": ["64e74a4baac4cd0a7264ecdd", "6398a072e301557ae24cec92"],
"shoreline": [
"64e74a534d49d23b2c39d31b",
"661421c7c1f2f548c50ee649",
@ -1101,14 +1026,8 @@
"657acb2ac900be5902191ac9",
"6582dbf0b8d7830efc45016f"
],
"laboratory": [
"6398a4cfb5992f573c6562b3",
"64e74a44c2b4f829615ec334"
],
"sandbox": [
"6575a6ca8778e96ded05a802",
"6582bd252b50c61c565828e2"
]
"laboratory": ["6398a4cfb5992f573c6562b3", "64e74a44c2b4f829615ec334"],
"sandbox": ["6575a6ca8778e96ded05a802", "6582bd252b50c61c565828e2"]
},
"splitWaveIntoSingleSpawnsSettings": {
"enabled": true,
@ -1131,15 +1050,7 @@
},
"fixEmptyBotWavesSettings": {
"enabled": true,
"ignoreMaps": [
"base",
"develop",
"hideout",
"privatearea",
"suburbs",
"terminal",
"town"
]
"ignoreMaps": ["base", "develop", "hideout", "privatearea", "suburbs", "terminal", "town"]
},
"fitLootIntoContainerAttempts": 3,
"addOpenZonesToAllMaps": true,

View File

@ -856,19 +856,10 @@
"botTypeForLoot": "assault",
"equipmentBlacklist": {
"Headwear": [],
"ArmorVest": [
"5df8a2ca86f7740bfe6df777"
],
"TacticalVest": [
"572b7adb24597762ae139821"
],
"Backpack": [
"56e33634d2720bd8058b456b",
"5ab8ee7786f7742d8f33f0b9"
],
"FirstPrimaryWeapon": [
"5a38e6bac4a2826c6e06d79b"
],
"ArmorVest": ["5df8a2ca86f7740bfe6df777"],
"TacticalVest": ["572b7adb24597762ae139821"],
"Backpack": ["56e33634d2720bd8058b456b", "5ab8ee7786f7742d8f33f0b9"],
"FirstPrimaryWeapon": ["5a38e6bac4a2826c6e06d79b"],
"Holster": [],
"Scabbard": []
},
@ -955,22 +946,10 @@
"botTypeForLoot": "assault",
"equipmentBlacklist": {
"Headwear": [],
"ArmorVest": [
"59e7635f86f7742cbf2c1095",
"5df8a2ca86f7740bfe6df777"
],
"TacticalVest": [
"5fd4c5477a8d854fa0105061",
"572b7adb24597762ae139821"
],
"Backpack": [
"56e33680d2720be2748b4576",
"56e33634d2720bd8058b456b",
"5ab8ee7786f7742d8f33f0b9"
],
"FirstPrimaryWeapon": [
"5a38e6bac4a2826c6e06d79b"
],
"ArmorVest": ["59e7635f86f7742cbf2c1095", "5df8a2ca86f7740bfe6df777"],
"TacticalVest": ["5fd4c5477a8d854fa0105061", "572b7adb24597762ae139821"],
"Backpack": ["56e33680d2720be2748b4576", "56e33634d2720bd8058b456b", "5ab8ee7786f7742d8f33f0b9"],
"FirstPrimaryWeapon": ["5a38e6bac4a2826c6e06d79b"],
"Holster": [],
"Scabbard": []
},
@ -1056,22 +1035,10 @@
"botTypeForLoot": "assault",
"equipmentBlacklist": {
"Headwear": [],
"ArmorVest": [
"59e7635f86f7742cbf2c1095",
"5df8a2ca86f7740bfe6df777"
],
"TacticalVest": [
"5fd4c5477a8d854fa0105061",
"572b7adb24597762ae139821"
],
"Backpack": [
"56e33680d2720be2748b4576",
"56e33634d2720bd8058b456b",
"5ab8ee7786f7742d8f33f0b9"
],
"FirstPrimaryWeapon": [
"5a38e6bac4a2826c6e06d79b"
],
"ArmorVest": ["59e7635f86f7742cbf2c1095", "5df8a2ca86f7740bfe6df777"],
"TacticalVest": ["5fd4c5477a8d854fa0105061", "572b7adb24597762ae139821"],
"Backpack": ["56e33680d2720be2748b4576", "56e33634d2720bd8058b456b", "5ab8ee7786f7742d8f33f0b9"],
"FirstPrimaryWeapon": ["5a38e6bac4a2826c6e06d79b"],
"Holster": [],
"Scabbard": []
},
@ -1168,14 +1135,8 @@
"5f60e7788adaa7100c3adb49",
"5aa2a7e8e5b5b00016327c16"
],
"ArmorVest": [
"59e7635f86f7742cbf2c1095",
"5df8a2ca86f7740bfe6df777"
],
"TacticalVest": [
"5fd4c5477a8d854fa0105061",
"572b7adb24597762ae139821"
],
"ArmorVest": ["59e7635f86f7742cbf2c1095", "5df8a2ca86f7740bfe6df777"],
"TacticalVest": ["5fd4c5477a8d854fa0105061", "572b7adb24597762ae139821"],
"Backpack": [
"56e33680d2720be2748b4576",
"56e33634d2720bd8058b456b",
@ -1183,14 +1144,8 @@
"5ab8f04f86f774585f4237d8",
"5f5e45cc5021ce62144be7aa"
],
"FirstPrimaryWeapon": [
"5a38e6bac4a2826c6e06d79b"
],
"Holster": [
"5448bd6b4bdc2dfc2f8b4569",
"579204f224597773d619e051",
"571a12c42459771f627b58a0"
],
"FirstPrimaryWeapon": ["5a38e6bac4a2826c6e06d79b"],
"Holster": ["5448bd6b4bdc2dfc2f8b4569", "579204f224597773d619e051", "571a12c42459771f627b58a0"],
"Scabbard": []
},
"modifiers": {

File diff suppressed because it is too large Load Diff

View File

@ -276,10 +276,7 @@
},
"armor": {
"removeRemovablePlateChance": 40,
"plateSlotIdToRemovePool": [
"front_plate",
"back_plate"
]
"plateSlotIdToRemovePool": ["front_plate", "back_plate"]
},
"itemPriceMultiplier": {
"5737292724597765e5728562": 6,
@ -314,10 +311,7 @@
"traderItems": false,
"armorPlate": {
"maxProtectionLevel": 4,
"ignoreSlots": [
"right_side_plate",
"left_side_plate"
]
"ignoreSlots": ["right_side_plate", "left_side_plate"]
},
"enableCustomItemCategoryList": false,
"customItemCategoryList": []

View File

@ -12,9 +12,9 @@
"trader": 0.6
},
"weaponTreatment": {
"critSuccessChance": 0.10,
"critSuccessChance": 0.1,
"critSuccessAmount": 4,
"critFailureChance": 0.10,
"critFailureChance": 0.1,
"critFailureAmount": 4,
"pointGainMultiplier": 0.6
},
@ -163,7 +163,7 @@
"MalfunctionProtections": {
"valuesMinMax": {
"min": 0.75,
"max": 0.90
"max": 0.9
},
"activeDurabilityPercentMinMax": {
"min": 75,

View File

@ -86,9 +86,7 @@
"ammoRewardChancePercent": 10,
"minStackSize": 10,
"ammoRewardBlacklist": {
"common": [
"59e690b686f7746c9f75e848"
],
"common": ["59e690b686f7746c9f75e848"],
"rare": [],
"superrare": []
},

View File

@ -2,94 +2,32 @@
"acceleration": 7,
"weather": {
"clouds": {
"values": [
-1.5,
-1,
0,
0.5,
1,
1.5
],
"weights": [
60,
50,
15,
5,
4,
3
]
"values": [-1.5, -1, 0, 0.5, 1, 1.5],
"weights": [60, 50, 15, 5, 4, 3]
},
"windSpeed": {
"values": [
0,
1,
2,
3
],
"weights": [
4,
3,
2,
1
]
"values": [0, 1, 2, 3],
"weights": [4, 3, 2, 1]
},
"windDirection": {
"values": [
1,
2,
3,
4,
5,
6,
7,
8
],
"weights": [
1,
1,
1,
1,
1,
1,
1,
1
]
"values": [1, 2, 3, 4, 5, 6, 7, 8],
"weights": [1, 1, 1, 1, 1, 1, 1, 1]
},
"windGustiness": {
"min": 0,
"max": 1
},
"rain": {
"values": [
1,
2,
3
],
"weights": [
25,
1,
1
]
"values": [1, 2, 3],
"weights": [25, 1, 1]
},
"rainIntensity": {
"min": 0,
"max": 1
},
"fog": {
"values": [
0.002,
0.004,
0.008,
0.012,
0.087
],
"weights": [
20,
8,
5,
5,
1
]
"values": [0.002, 0.004, 0.008, 0.012, 0.087],
"weights": [20, 8, 5, 5, 1]
},
"temp": {
"min": 0,

View File

@ -37,8 +37,7 @@ const licenseFile = "../LICENSE.md";
/**
* Transpile src files into Javascript with SWC
*/
const compile = async () =>
{
const compile = async () => {
// Compile TypeScript files using SWC
await exec("npx swc src -d obj", { stdio: "inherit" });
@ -46,29 +45,23 @@ const compile = async () =>
const srcDir = path.join("obj", "src");
const destDir = path.join("obj");
try
{
try {
const entities = await fs.readdir(srcDir);
for (const entity of entities)
{
for (const entity of entities) {
const srcPath = path.join(srcDir, entity);
const destPath = path.join(destDir, entity);
await fs.move(srcPath, destPath, { overwrite: true });
}
// After moving all contents, remove the now-empty /obj/src directory
await fs.remove(srcDir);
}
catch (error)
{
} catch (error) {
console.error("An error occurred during the merge operation:", error);
}
};
// Packaging
const fetchPackageImage = async () =>
{
try
{
const fetchPackageImage = async () => {
try {
const output = "./.pkg-cache/v3.5";
const fetchedPkg = await pkgfetch.need({
arch: targetArch,
@ -79,18 +72,14 @@ const fetchPackageImage = async () =>
console.log(`fetched node binary at ${fetchedPkg}`);
const builtPkg = fetchedPkg.replace("node", "built");
await fs.copyFile(fetchedPkg, builtPkg);
}
catch (e)
{
} catch (e) {
console.error(`Error while fetching and patching package image: ${e.message}`);
console.error(e.stack);
}
};
const updateBuildProperties = async () =>
{
if (targetPlatform !== "win32")
{
const updateBuildProperties = async () => {
if (targetPlatform !== "win32") {
// can't modify executable's resource on non-windows build
return;
}
@ -110,12 +99,15 @@ const updateBuildProperties = async () =>
const vi = ResEdit.Resource.VersionInfo.fromEntries(res.entries)[0];
vi.setStringValues({ lang: 1033, codepage: 1200 }, {
vi.setStringValues(
{ lang: 1033, codepage: 1200 },
{
ProductName: manifest.author,
FileDescription: manifest.description,
CompanyName: manifest.name,
LegalCopyright: manifest.license,
});
},
);
vi.removeStringValue({ lang: 1033, codepage: 1200 }, "OriginalFilename");
vi.removeStringValue({ lang: 1033, codepage: 1200 }, "InternalName");
vi.setFileVersion(...manifest.version.split(".").map(Number));
@ -129,15 +121,14 @@ const updateBuildProperties = async () =>
* Copy various asset files to the destination directory
*/
const copyAssets = () =>
gulp.src(["assets/**/*.json", "assets/**/*.json5", "assets/**/*.png", "assets/**/*.jpg", "assets/**/*.ico"]).pipe(
gulp.dest(dataDir),
);
gulp
.src(["assets/**/*.json", "assets/**/*.json5", "assets/**/*.png", "assets/**/*.jpg", "assets/**/*.ico"])
.pipe(gulp.dest(dataDir));
/**
* Download pnpm executable
*/
const downloadPnpm = async () =>
{
const downloadPnpm = async () => {
// Please ensure that the @pnpm/exe version in devDependencies is pinned to a specific version. If it's not, the
// following task will download *all* versions that are compatible with the semver range specified.
const pnpmVersion = manifest.devDependencies["@pnpm/exe"];
@ -145,7 +136,9 @@ const downloadPnpm = async () =>
const npmResult = await exec(`npm view ${pnpmPackageName}@${pnpmVersion} dist.tarball`, { stdout: "pipe" });
const pnpmLink = npmResult.stdout.trim();
console.log(`Downloading pnpm binary from ${pnpmLink}`);
download(pnpmLink).pipe(decompress({ strip: 1 })).pipe(gulp.dest(path.join(dataDir, "@pnpm", "exe")));
download(pnpmLink)
.pipe(decompress({ strip: 1 }))
.pipe(gulp.dest(path.join(dataDir, "@pnpm", "exe")));
};
/**
@ -156,10 +149,8 @@ const copyLicense = () => gulp.src([licenseFile]).pipe(rename("LICENSE-Server.tx
/**
* Writes the latest build data to the core.json and build.json configuration files.
*/
const writeBuildDataToJSON = async () =>
{
try
{
const writeBuildDataToJSON = async () => {
try {
// Fetch the latest Git commit hash
const gitResult = await exec("git rev-parse HEAD", { stdout: "pipe" });
@ -180,9 +171,7 @@ const writeBuildDataToJSON = async () =>
buildInfo.buildTime = coreParsed.buildTime;
buildInfo.sptVersion = coreParsed.sptVersion;
await fs.writeFile(buildJsonPath, JSON.stringify(buildInfo, null, 4));
}
catch (error)
{
} catch (error) {
throw new Error(`Failed to write commit hash to core.json: ${error.message}`);
}
};
@ -190,8 +179,7 @@ const writeBuildDataToJSON = async () =>
/**
* Create a hash file for asset checks
*/
const createHashFile = async () =>
{
const createHashFile = async () => {
const hashFileDir = path.resolve(dataDir, "checks.dat");
const assetData = await loadRecursiveAsync("assets/");
const assetDataString = Buffer.from(JSON.stringify(assetData), "utf-8").toString("base64");
@ -218,18 +206,13 @@ const cleanCompiled = async () => await fs.rm("./obj", { recursive: true, force:
* @param {string[]} files
* @returns {Promise<string[]>}
*/
const getJSONFiles = async (dir, files = []) =>
{
const getJSONFiles = async (dir, files = []) => {
const fileList = await fs.readdir(dir);
for (const file of fileList)
{
for (const file of fileList) {
const name = path.resolve(dir, file);
if ((await fs.stat(name)).isDirectory())
{
if ((await fs.stat(name)).isDirectory()) {
getJSONFiles(name, files);
}
else if (name.slice(-5) === ".json")
{
} else if (name.slice(-5) === ".json") {
files.push(name);
}
}
@ -239,21 +222,16 @@ const getJSONFiles = async (dir, files = []) =>
/**
* Goes through every json file in assets and makes sure they're valid json.
*/
const validateJSONs = async () =>
{
const validateJSONs = async () => {
const assetsPath = path.resolve("assets");
const jsonFileList = await getJSONFiles(assetsPath);
let jsonFileInProcess = "";
try
{
for (const jsonFile of jsonFileList)
{
try {
for (const jsonFile of jsonFileList) {
jsonFileInProcess = jsonFile;
JSON.parse(await fs.readFile(jsonFile));
}
}
catch (error)
{
} catch (error) {
throw new Error(`${error.message} | ${jsonFileInProcess}`);
}
};
@ -264,8 +242,7 @@ const validateJSONs = async () =>
* @param {crypto.BinaryLike} data
* @returns {string}
*/
const generateHashForData = (data) =>
{
const generateHashForData = (data) => {
const hashSum = crypto.createHash("sha1");
hashSum.update(data);
return hashSum.digest("hex");
@ -277,21 +254,16 @@ const generateHashForData = (data) =>
* @param {fs.PathLike} filepath
* @returns {}
*/
const loadRecursiveAsync = async (filepath) =>
{
const loadRecursiveAsync = async (filepath) => {
const result = {};
const filesList = await fs.readdir(filepath);
for (const file of filesList)
{
for (const file of filesList) {
const curPath = path.parse(path.join(filepath, file));
if ((await fs.stat(path.join(curPath.dir, curPath.base))).isDirectory())
{
if ((await fs.stat(path.join(curPath.dir, curPath.base))).isDirectory()) {
result[curPath.name] = loadRecursiveAsync(`${filepath}${file}/`);
}
else if (curPath.ext === ".json")
{
} else if (curPath.ext === ".json") {
result[curPath.name] = generateHashForData(await fs.readFile(`${filepath}${file}`));
}
}
@ -299,8 +271,7 @@ const loadRecursiveAsync = async (filepath) =>
// set all loadRecursive to be executed asynchronously
const resEntries = Object.entries(result);
const resResolved = await Promise.all(resEntries.map((ent) => ent[1]));
for (let resIdx = 0; resIdx < resResolved.length; resIdx++)
{
for (let resIdx = 0; resIdx < resResolved.length; resIdx++) {
resEntries[resIdx][1] = resResolved[resIdx];
}
@ -309,8 +280,7 @@ const loadRecursiveAsync = async (filepath) =>
};
// Main Tasks Generation
const build = (packagingType) =>
{
const build = (packagingType) => {
const anonPackaging = () => packaging(entries[packagingType]);
anonPackaging.displayName = `packaging-${packagingType}`;
const tasks = [
@ -327,11 +297,9 @@ const build = (packagingType) =>
};
// Packaging Arguments
const packaging = async (entry) =>
{
const packaging = async (entry) => {
const target = `${nodeVersion}-${targetPlatform}-${targetArch}`;
try
{
try {
await pkg.exec([
entry,
"--compress",
@ -344,9 +312,7 @@ const packaging = async (entry) =>
pkgConfig,
"--public",
]);
}
catch (error)
{
} catch (error) {
console.error(`Error occurred during packaging: ${error}`);
}
};
@ -361,8 +327,7 @@ gulp.task(
"run:debug",
async () => await exec("ts-node-dev -r tsconfig-paths/register src/ide/TestEntry.ts", { stdio }),
);
gulp.task("run:profiler", async () =>
{
gulp.task("run:profiler", async () => {
await cleanCompiled();
await compile();
await exec("node --prof --inspect --trace-warnings obj/ide/TestEntry.js", { stdio });

View File

@ -1,11 +1,6 @@
{
"pkg": {
"scripts": [
"obj/**/*.js"
],
"assets": [
"obj/**/*.js.map",
"package.json"
]
"scripts": ["obj/**/*.js"],
"assets": ["obj/**/*.js.map", "package.json"]
}
}

View File

@ -3,23 +3,19 @@ import { ILogger } from "@spt/models/spt/utils/ILogger";
import { AsyncQueue } from "@spt/utils/AsyncQueue";
import { WinstonMainLogger } from "@spt/utils/logging/WinstonMainLogger";
export class ErrorHandler
{
export class ErrorHandler {
private logger: ILogger;
private readLine: readline.Interface;
constructor()
{
constructor() {
this.logger = new WinstonMainLogger(new AsyncQueue());
this.readLine = readline.createInterface({ input: process.stdin, output: process.stdout });
}
public handleCriticalError(err: Error): void
{
public handleCriticalError(err: Error): void {
this.logger.error("The application had a critical error and failed to run");
this.logger.error(`Exception produced: ${err.name}`);
if (err.stack)
{
if (err.stack) {
this.logger.error(`\nStacktrace:\n${err.stack}`);
}

View File

@ -1,25 +1,21 @@
import { container } from "tsyringe";
import { Container } from "@spt/di/Container";
import { ErrorHandler } from "@spt/ErrorHandler";
import { Container } from "@spt/di/Container";
import type { PreSptModLoader } from "@spt/loaders/PreSptModLoader";
import { App } from "@spt/utils/App";
import { Watermark } from "@spt/utils/Watermark";
import { container } from "tsyringe";
export class Program
{
export class Program {
private errorHandler: ErrorHandler;
constructor()
{
constructor() {
// set window properties
process.stdout.setEncoding("utf8");
process.title = "SPT Server";
this.errorHandler = new ErrorHandler();
}
public async start(): Promise<void>
{
try
{
public async start(): Promise<void> {
try {
Container.registerTypes(container);
const childContainer = container.createChildContainer();
const watermark = childContainer.resolve<Watermark>("Watermark");
@ -31,9 +27,7 @@ export class Program
Container.registerPostLoadTypes(container, childContainer);
childContainer.resolve<App>("App").load();
}
catch (err: any)
{
} catch (err: any) {
this.errorHandler.handleCriticalError(err instanceof Error ? err : new Error(err));
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { AchievementController } from "@spt/controllers/AchievementController";
import { ProfileController } from "@spt/controllers/ProfileController";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
@ -6,16 +5,15 @@ import { IGetBodyResponseData } from "@spt/models/eft/httpResponse/IGetBodyRespo
import { ICompletedAchievementsResponse } from "@spt/models/eft/profile/ICompletedAchievementsResponse";
import { IGetAchievementsResponse } from "@spt/models/eft/profile/IGetAchievementsResponse";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class AchievementCallbacks
{
export class AchievementCallbacks {
constructor(
@inject("AchievementController") protected achievementController: AchievementController,
@inject("ProfileController") protected profileController: ProfileController,
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
)
{}
) {}
/**
* Handle client/achievement/list
@ -24,8 +22,7 @@ export class AchievementCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<IGetAchievementsResponse>
{
): IGetBodyResponseData<IGetAchievementsResponse> {
return this.httpResponse.getBody(this.achievementController.getAchievements(sessionID));
}
@ -36,8 +33,7 @@ export class AchievementCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<ICompletedAchievementsResponse>
{
): IGetBodyResponseData<ICompletedAchievementsResponse> {
return this.httpResponse.getBody(this.achievementController.getAchievementStatistics(sessionID));
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { BotController } from "@spt/controllers/BotController";
import { IGenerateBotsRequestData } from "@spt/models/eft/bot/IGenerateBotsRequestData";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
@ -6,23 +5,21 @@ import { IBotBase } from "@spt/models/eft/common/tables/IBotBase";
import { Difficulties } from "@spt/models/eft/common/tables/IBotType";
import { IGetBodyResponseData } from "@spt/models/eft/httpResponse/IGetBodyResponseData";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class BotCallbacks
{
export class BotCallbacks {
constructor(
@inject("BotController") protected botController: BotController,
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
)
{}
) {}
/**
* Handle singleplayer/settings/bot/limit
* Is called by client to define each bot roles wave limit
* @returns string
*/
public getBotLimit(url: string, info: IEmptyRequestData, sessionID: string): string
{
public getBotLimit(url: string, info: IEmptyRequestData, sessionID: string): string {
const splittedUrl = url.split("/");
const type = splittedUrl[splittedUrl.length - 1];
return this.httpResponse.noBody(this.botController.getBotPresetGenerationLimit(type));
@ -32,13 +29,11 @@ export class BotCallbacks
* Handle singleplayer/settings/bot/difficulty
* @returns string
*/
public getBotDifficulty(url: string, info: IEmptyRequestData, sessionID: string): string
{
public getBotDifficulty(url: string, info: IEmptyRequestData, sessionID: string): string {
const splittedUrl = url.split("/");
const type = splittedUrl[splittedUrl.length - 2].toLowerCase();
const difficulty = splittedUrl[splittedUrl.length - 1];
if (difficulty === "core")
{
if (difficulty === "core") {
return this.httpResponse.noBody(this.botController.getBotCoreDifficulty());
}
return this.httpResponse.noBody(this.botController.getBotDifficulty(type, difficulty));
@ -52,8 +47,7 @@ export class BotCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): Record<string, Difficulties>
{
): Record<string, Difficulties> {
return this.httpResponse.noBody(this.botController.getAllBotDifficulties());
}
@ -65,8 +59,7 @@ export class BotCallbacks
url: string,
info: IGenerateBotsRequestData,
sessionID: string,
): Promise<IGetBodyResponseData<IBotBase[]>>
{
): Promise<IGetBodyResponseData<IBotBase[]>> {
return this.httpResponse.getBody(await this.botController.generate(sessionID, info));
}
@ -74,8 +67,7 @@ export class BotCallbacks
* Handle singleplayer/settings/bot/maxCap
* @returns string
*/
public getBotCap(url: string, info: any, sessionID: string): string
{
public getBotCap(url: string, info: any, sessionID: string): string {
const splitUrl = url.split("/");
const location = splitUrl[splitUrl.length - 1];
return this.httpResponse.noBody(this.botController.getBotCap(location));
@ -85,8 +77,7 @@ export class BotCallbacks
* Handle singleplayer/settings/bot/getBotBehaviours
* @returns string
*/
public getBotBehaviours(): string
{
public getBotBehaviours(): string {
return this.httpResponse.noBody(this.botController.getAiBotBrainTypes());
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { BuildController } from "@spt/controllers/BuildController";
import { ISetMagazineRequest } from "@spt/models/eft/builds/ISetMagazineRequest";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
@ -8,29 +7,26 @@ import { IPresetBuildActionRequestData } from "@spt/models/eft/presetBuild/IPres
import { IRemoveBuildRequestData } from "@spt/models/eft/presetBuild/IRemoveBuildRequestData";
import { IUserBuilds } from "@spt/models/eft/profile/ISptProfile";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class BuildsCallbacks
{
export class BuildsCallbacks {
constructor(
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("BuildController") protected buildController: BuildController,
)
{}
) {}
/**
* Handle client/builds/list
*/
public getBuilds(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<IUserBuilds>
{
public getBuilds(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<IUserBuilds> {
return this.httpResponse.getBody(this.buildController.getUserBuilds(sessionID));
}
/**
* Handle client/builds/magazine/save
*/
public createMagazineTemplate(url: string, request: ISetMagazineRequest, sessionID: string): INullResponseData
{
public createMagazineTemplate(url: string, request: ISetMagazineRequest, sessionID: string): INullResponseData {
this.buildController.createMagazineTemplate(sessionID, request);
return this.httpResponse.nullResponse();
@ -39,8 +35,7 @@ export class BuildsCallbacks
/**
* Handle client/builds/weapon/save
*/
public setWeapon(url: string, info: IPresetBuildActionRequestData, sessionID: string): INullResponseData
{
public setWeapon(url: string, info: IPresetBuildActionRequestData, sessionID: string): INullResponseData {
this.buildController.saveWeaponBuild(sessionID, info);
return this.httpResponse.nullResponse();
@ -49,8 +44,7 @@ export class BuildsCallbacks
/**
* Handle client/builds/equipment/save
*/
public setEquipment(url: string, info: IPresetBuildActionRequestData, sessionID: string): INullResponseData
{
public setEquipment(url: string, info: IPresetBuildActionRequestData, sessionID: string): INullResponseData {
this.buildController.saveEquipmentBuild(sessionID, info);
return this.httpResponse.nullResponse();
@ -59,8 +53,7 @@ export class BuildsCallbacks
/**
* Handle client/builds/delete
*/
public deleteBuild(url: string, info: IRemoveBuildRequestData, sessionID: string): INullResponseData
{
public deleteBuild(url: string, info: IRemoveBuildRequestData, sessionID: string): INullResponseData {
this.buildController.removeBuild(sessionID, info);
return this.httpResponse.nullResponse();

View File

@ -1,34 +1,30 @@
import { inject, injectable } from "tsyringe";
import { BundleLoader } from "@spt/loaders/BundleLoader";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { IHttpConfig } from "@spt/models/spt/config/IHttpConfig";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class BundleCallbacks
{
export class BundleCallbacks {
protected httpConfig: IHttpConfig;
constructor(
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("BundleLoader") protected bundleLoader: BundleLoader,
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
) {
this.httpConfig = this.configServer.getConfig(ConfigTypes.HTTP);
}
/**
* Handle singleplayer/bundles
*/
public getBundles(url: string, info: any, sessionID: string): string
{
public getBundles(url: string, info: any, sessionID: string): string {
return this.httpResponse.noBody(this.bundleLoader.getBundles());
}
public getBundle(url: string, info: any, sessionID: string): string
{
public getBundle(url: string, info: any, sessionID: string): string {
return "BUNDLE";
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { ClientLogController } from "@spt/controllers/ClientLogController";
import { ModLoadOrder } from "@spt/loaders/ModLoadOrder";
import { INullResponseData } from "@spt/models/eft/httpResponse/INullResponseData";
@ -8,25 +7,23 @@ import { IClientLogRequest } from "@spt/models/spt/logging/IClientLogRequest";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { LocalisationService } from "@spt/services/LocalisationService";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
/** Handle client logging related events */
@injectable()
export class ClientLogCallbacks
{
export class ClientLogCallbacks {
constructor(
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("ClientLogController") protected clientLogController: ClientLogController,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ModLoadOrder") protected modLoadOrder: ModLoadOrder,
)
{}
) {}
/**
* Handle /singleplayer/log
*/
public clientLog(url: string, info: IClientLogRequest, sessionID: string): INullResponseData
{
public clientLog(url: string, info: IClientLogRequest, sessionID: string): INullResponseData {
this.clientLogController.clientLog(info);
return this.httpResponse.nullResponse();
}
@ -34,8 +31,7 @@ export class ClientLogCallbacks
/**
* Handle /singleplayer/release
*/
public releaseNotes(): string
{
public releaseNotes(): string {
const data: IRelease = this.configServer.getConfig<ICoreConfig>(ConfigTypes.CORE).release;
data.betaDisclaimerText = globalThis.G_MODS_ENABLED
@ -62,8 +58,7 @@ export class ClientLogCallbacks
* Handle /singleplayer/enableBSGlogging
*/
public bsgLogging(): string
{
public bsgLogging(): string {
const data: IBsgLogging = this.configServer.getConfig<ICoreConfig>(ConfigTypes.CORE).bsgLogging;
return this.httpResponse.noBody(data);
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { CustomizationController } from "@spt/controllers/CustomizationController";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
@ -10,23 +9,21 @@ import { IGetBodyResponseData } from "@spt/models/eft/httpResponse/IGetBodyRespo
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { SaveServer } from "@spt/servers/SaveServer";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class CustomizationCallbacks
{
export class CustomizationCallbacks {
constructor(
@inject("CustomizationController") protected customizationController: CustomizationController,
@inject("SaveServer") protected saveServer: SaveServer,
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
)
{}
) {}
/**
* Handle client/trading/customization/storage
* @returns IGetSuitsResponse
*/
public getSuits(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<IGetSuitsResponse>
{
public getSuits(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<IGetSuitsResponse> {
const result: IGetSuitsResponse = { _id: sessionID, suites: this.saveServer.getProfile(sessionID).suits };
return this.httpResponse.getBody(result);
}
@ -35,8 +32,7 @@ export class CustomizationCallbacks
* Handle client/trading/customization
* @returns ISuit[]
*/
public getTraderSuits(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<ISuit[]>
{
public getTraderSuits(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<ISuit[]> {
const splittedUrl = url.split("/");
const traderID = splittedUrl[splittedUrl.length - 2];
@ -50,16 +46,14 @@ export class CustomizationCallbacks
pmcData: IPmcData,
body: IWearClothingRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.customizationController.wearClothing(pmcData, body, sessionID);
}
/**
* Handle CustomizationBuy event
*/
public buyClothing(pmcData: IPmcData, body: IBuyClothingRequestData, sessionID: string): IItemEventRouterResponse
{
public buyClothing(pmcData: IPmcData, body: IBuyClothingRequestData, sessionID: string): IItemEventRouterResponse {
return this.customizationController.buyClothing(pmcData, body, sessionID);
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { HideoutController } from "@spt/controllers/HideoutController";
import { RagfairController } from "@spt/controllers/RagfairController";
import { TraderHelper } from "@spt/helpers/TraderHelper";
@ -17,13 +16,13 @@ import { ISettingsBase } from "@spt/models/spt/server/ISettingsBase";
import { DatabaseService } from "@spt/services/DatabaseService";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { inject, injectable } from "tsyringe";
/**
* Handle client requests
*/
@injectable()
export class DataCallbacks
{
export class DataCallbacks {
constructor(
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@ -31,15 +30,13 @@ export class DataCallbacks
@inject("DatabaseService") protected databaseService: DatabaseService,
@inject("RagfairController") protected ragfairController: RagfairController,
@inject("HideoutController") protected hideoutController: HideoutController,
)
{}
) {}
/**
* Handle client/settings
* @returns ISettingsBase
*/
public getSettings(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<ISettingsBase>
{
public getSettings(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<ISettingsBase> {
return this.httpResponse.getBody(this.databaseService.getSettings());
}
@ -47,8 +44,7 @@ export class DataCallbacks
* Handle client/globals
* @returns IGlobals
*/
public getGlobals(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<IGlobals>
{
public getGlobals(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<IGlobals> {
const globals = this.databaseService.getGlobals();
globals.time = Date.now() / 1000;
@ -59,8 +55,7 @@ export class DataCallbacks
* Handle client/items
* @returns string
*/
public getTemplateItems(url: string, info: IEmptyRequestData, sessionID: string): string
{
public getTemplateItems(url: string, info: IEmptyRequestData, sessionID: string): string {
return this.httpResponse.getUnclearedBody(this.databaseService.getItems());
}
@ -72,8 +67,7 @@ export class DataCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<IHandbookBase>
{
): IGetBodyResponseData<IHandbookBase> {
return this.httpResponse.getBody(this.databaseService.getHandbook());
}
@ -85,8 +79,7 @@ export class DataCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<Record<string, ICustomizationItem>>
{
): IGetBodyResponseData<Record<string, ICustomizationItem>> {
return this.httpResponse.getBody(this.databaseService.getTemplates().customization);
}
@ -98,8 +91,7 @@ export class DataCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<string[]>
{
): IGetBodyResponseData<string[]> {
return this.httpResponse.getBody(this.databaseService.getTemplates().character);
}
@ -111,8 +103,7 @@ export class DataCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<IHideoutSettingsBase>
{
): IGetBodyResponseData<IHideoutSettingsBase> {
return this.httpResponse.getBody(this.databaseService.getHideout().settings);
}
@ -120,8 +111,7 @@ export class DataCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<IHideoutArea[]>
{
): IGetBodyResponseData<IHideoutArea[]> {
return this.httpResponse.getBody(this.databaseService.getHideout().areas);
}
@ -129,8 +119,7 @@ export class DataCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<IHideoutProduction[]>
{
): IGetBodyResponseData<IHideoutProduction[]> {
return this.httpResponse.getBody(this.databaseService.getHideout().production);
}
@ -138,8 +127,7 @@ export class DataCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<IHideoutScavCase[]>
{
): IGetBodyResponseData<IHideoutScavCase[]> {
return this.httpResponse.getBody(this.databaseService.getHideout().scavcase);
}
@ -150,27 +138,23 @@ export class DataCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<Record<string, string>>
{
): IGetBodyResponseData<Record<string, string>> {
return this.httpResponse.getBody(this.databaseService.getLocales().languages);
}
/**
* Handle client/menu/locale
*/
public getLocalesMenu(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<string>
{
public getLocalesMenu(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<string> {
const localeId = url.replace("/client/menu/locale/", "");
const locales = this.databaseService.getLocales();
let result = locales.menu[localeId];
if (result === undefined)
{
if (result === undefined) {
result = locales.menu.en;
}
if (result === undefined)
throw new Error(`Unable to determine locale for request with '${localeId}'`);
if (result === undefined) throw new Error(`Unable to determine locale for request with '${localeId}'`);
return this.httpResponse.getBody(result);
}
@ -178,14 +162,12 @@ export class DataCallbacks
/**
* Handle client/locale
*/
public getLocalesGlobal(url: string, info: IEmptyRequestData, sessionID: string): string
{
public getLocalesGlobal(url: string, info: IEmptyRequestData, sessionID: string): string {
const localeId = url.replace("/client/locale/", "");
const locales = this.databaseService.getLocales();
let result = locales.global[localeId];
if (result === undefined)
{
if (result === undefined) {
result = locales.global["en"];
}
@ -195,8 +177,7 @@ export class DataCallbacks
/**
* Handle client/hideout/qte/list
*/
public getQteList(url: string, info: IEmptyRequestData, sessionID: string): string
{
public getQteList(url: string, info: IEmptyRequestData, sessionID: string): string {
return this.httpResponse.getUnclearedBody(this.hideoutController.getQteList(sessionID));
}
@ -209,16 +190,14 @@ export class DataCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<IGetItemPricesResponse>
{
): IGetBodyResponseData<IGetItemPricesResponse> {
const traderId = url.replace("/client/items/prices/", "");
// All traders share same item prices, unknown how to tell what items are shown for each trader
// Shown items listed are likely linked to traders items_buy/category array
const handbookPrices = this.ragfairController.getStaticPrices();
const response: IGetItemPricesResponse
= {
const response: IGetItemPricesResponse = {
supplyNextTime: this.traderHelper.getNextUpdateTimestamp(traderId),
prices: handbookPrices,
currencyCourses: {

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { DialogueController } from "@spt/controllers/DialogueController";
import { OnUpdate } from "@spt/di/OnUpdate";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
@ -36,17 +35,16 @@ import { DialogueInfo } from "@spt/models/eft/profile/ISptProfile";
import { HashUtil } from "@spt/utils/HashUtil";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class DialogueCallbacks implements OnUpdate
{
export class DialogueCallbacks implements OnUpdate {
constructor(
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("DialogueController") protected dialogueController: DialogueController,
)
{}
) {}
/**
* Handle client/friend/list
@ -56,8 +54,7 @@ export class DialogueCallbacks implements OnUpdate
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<IGetFriendListDataResponse>
{
): IGetBodyResponseData<IGetFriendListDataResponse> {
return this.httpResponse.getBody(this.dialogueController.getFriendList(sessionID));
}
@ -69,8 +66,7 @@ export class DialogueCallbacks implements OnUpdate
url: string,
info: IGetChatServerListRequestData,
sessionID: string,
): IGetBodyResponseData<IChatServer[]>
{
): IGetBodyResponseData<IChatServer[]> {
const chatServer: IChatServer = {
_id: this.hashUtil.generate(),
RegistrationId: 20,
@ -91,8 +87,7 @@ export class DialogueCallbacks implements OnUpdate
url: string,
info: IGetMailDialogListRequestData,
sessionID: string,
): IGetBodyResponseData<DialogueInfo[]>
{
): IGetBodyResponseData<DialogueInfo[]> {
return this.httpResponse.getBody(this.dialogueController.generateDialogueList(sessionID), 0, undefined, false);
}
@ -101,8 +96,7 @@ export class DialogueCallbacks implements OnUpdate
url: string,
info: IGetMailDialogViewRequestData,
sessionID: string,
): IGetBodyResponseData<IGetMailDialogViewResponseData>
{
): IGetBodyResponseData<IGetMailDialogViewResponseData> {
return this.httpResponse.getBody(
this.dialogueController.generateDialogueView(info, sessionID),
0,
@ -116,35 +110,30 @@ export class DialogueCallbacks implements OnUpdate
url: string,
info: IGetMailDialogInfoRequestData,
sessionID: string,
): IGetBodyResponseData<DialogueInfo>
{
): IGetBodyResponseData<DialogueInfo> {
return this.httpResponse.getBody(this.dialogueController.getDialogueInfo(info.dialogId, sessionID));
}
/** Handle client/mail/dialog/remove */
public removeDialog(url: string, info: IRemoveDialogRequestData, sessionID: string): IGetBodyResponseData<any[]>
{
public removeDialog(url: string, info: IRemoveDialogRequestData, sessionID: string): IGetBodyResponseData<any[]> {
this.dialogueController.removeDialogue(info.dialogId, sessionID);
return this.httpResponse.emptyArrayResponse();
}
/** Handle client/mail/dialog/pin */
public pinDialog(url: string, info: IPinDialogRequestData, sessionID: string): IGetBodyResponseData<any[]>
{
public pinDialog(url: string, info: IPinDialogRequestData, sessionID: string): IGetBodyResponseData<any[]> {
this.dialogueController.setDialoguePin(info.dialogId, true, sessionID);
return this.httpResponse.emptyArrayResponse();
}
/** Handle client/mail/dialog/unpin */
public unpinDialog(url: string, info: IPinDialogRequestData, sessionID: string): IGetBodyResponseData<any[]>
{
public unpinDialog(url: string, info: IPinDialogRequestData, sessionID: string): IGetBodyResponseData<any[]> {
this.dialogueController.setDialoguePin(info.dialogId, false, sessionID);
return this.httpResponse.emptyArrayResponse();
}
/** Handle client/mail/dialog/read */
public setRead(url: string, info: ISetDialogReadRequestData, sessionID: string): IGetBodyResponseData<any[]>
{
public setRead(url: string, info: ISetDialogReadRequestData, sessionID: string): IGetBodyResponseData<any[]> {
this.dialogueController.setRead(info.dialogs, sessionID);
return this.httpResponse.emptyArrayResponse();
}
@ -157,28 +146,24 @@ export class DialogueCallbacks implements OnUpdate
url: string,
info: IGetAllAttachmentsRequestData,
sessionID: string,
): IGetBodyResponseData<IGetAllAttachmentsResponse | undefined>
{
): IGetBodyResponseData<IGetAllAttachmentsResponse | undefined> {
return this.httpResponse.getBody(this.dialogueController.getAllAttachments(info.dialogId, sessionID));
}
/** Handle client/mail/msg/send */
public sendMessage(url: string, request: ISendMessageRequest, sessionID: string): IGetBodyResponseData<string>
{
public sendMessage(url: string, request: ISendMessageRequest, sessionID: string): IGetBodyResponseData<string> {
return this.httpResponse.getBody(this.dialogueController.sendMessage(sessionID, request));
}
/** Handle client/friend/request/list/outbox */
public listOutbox(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<any[]>
{
public listOutbox(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<any[]> {
return this.httpResponse.getBody([]);
}
/**
* Handle client/friend/request/list/inbox
*/
public listInbox(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<any[]>
{
public listInbox(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<any[]> {
return this.httpResponse.getBody([]);
}
@ -189,16 +174,14 @@ export class DialogueCallbacks implements OnUpdate
url: string,
request: IFriendRequestData,
sessionID: string,
): IGetBodyResponseData<IFriendRequestSendResponse>
{
): IGetBodyResponseData<IFriendRequestSendResponse> {
return this.httpResponse.getBody(this.dialogueController.sendFriendRequest(sessionID, request));
}
/**
* Handle client/friend/request/accept-all
*/
public acceptAllFriendRequests(url: string, request: IEmptyRequestData, sessionID: string): INullResponseData
{
public acceptAllFriendRequests(url: string, request: IEmptyRequestData, sessionID: string): INullResponseData {
return this.httpResponse.nullResponse();
}
@ -209,8 +192,7 @@ export class DialogueCallbacks implements OnUpdate
url: string,
request: IAcceptFriendRequestData,
sessionID: string,
): IGetBodyResponseData<boolean>
{
): IGetBodyResponseData<boolean> {
return this.httpResponse.getBody(true);
}
@ -221,8 +203,7 @@ export class DialogueCallbacks implements OnUpdate
url: string,
request: IDeclineFriendRequestData,
sessionID: string,
): IGetBodyResponseData<boolean>
{
): IGetBodyResponseData<boolean> {
return this.httpResponse.getBody(true);
}
@ -233,41 +214,34 @@ export class DialogueCallbacks implements OnUpdate
url: string,
request: ICancelFriendRequestData,
sessionID: string,
): IGetBodyResponseData<boolean>
{
): IGetBodyResponseData<boolean> {
return this.httpResponse.getBody(true);
}
/** Handle client/friend/delete */
public deleteFriend(url: string, request: IDeleteFriendRequest, sessionID: string): INullResponseData
{
public deleteFriend(url: string, request: IDeleteFriendRequest, sessionID: string): INullResponseData {
return this.httpResponse.nullResponse();
}
/** Handle client/friend/ignore/set */
public ignoreFriend(url: string, request: IUIDRequestData, sessionID: string): INullResponseData
{
public ignoreFriend(url: string, request: IUIDRequestData, sessionID: string): INullResponseData {
return this.httpResponse.nullResponse();
}
/** Handle client/friend/ignore/remove */
public unIgnoreFriend(url: string, request: IUIDRequestData, sessionID: string): INullResponseData
{
public unIgnoreFriend(url: string, request: IUIDRequestData, sessionID: string): INullResponseData {
return this.httpResponse.nullResponse();
}
public clearMail(url: string, request: IClearMailMessageRequest, sessionID: string): IGetBodyResponseData<any[]>
{
public clearMail(url: string, request: IClearMailMessageRequest, sessionID: string): IGetBodyResponseData<any[]> {
return this.httpResponse.emptyArrayResponse();
}
public removeMail(url: string, request: IRemoveMailMessageRequest, sessionID: string): IGetBodyResponseData<any[]>
{
public removeMail(url: string, request: IRemoveMailMessageRequest, sessionID: string): IGetBodyResponseData<any[]> {
return this.httpResponse.emptyArrayResponse();
}
public createGroupMail(url: string, info: ICreateGroupMailRequest, sessionID: string): IGetBodyResponseData<any[]>
{
public createGroupMail(url: string, info: ICreateGroupMailRequest, sessionID: string): IGetBodyResponseData<any[]> {
return this.httpResponse.emptyArrayResponse();
}
@ -275,13 +249,11 @@ export class DialogueCallbacks implements OnUpdate
url: string,
info: IChangeGroupMailOwnerRequest,
sessionID: string,
): IGetBodyResponseData<any[]>
{
): IGetBodyResponseData<any[]> {
throw new Error("Method not implemented.");
}
public addUserToMail(url: string, info: IAddUserGroupMailRequest, sessionID: string): IGetBodyResponseData<any[]>
{
public addUserToMail(url: string, info: IAddUserGroupMailRequest, sessionID: string): IGetBodyResponseData<any[]> {
throw new Error("Method not implemented.");
}
@ -289,19 +261,16 @@ export class DialogueCallbacks implements OnUpdate
url: string,
info: IRemoveUserGroupMailRequest,
sessionID: string,
): IGetBodyResponseData<any[]>
{
): IGetBodyResponseData<any[]> {
throw new Error("Method not implemented.");
}
public async onUpdate(timeSinceLastRun: number): Promise<boolean>
{
public async onUpdate(timeSinceLastRun: number): Promise<boolean> {
this.dialogueController.update();
return true;
}
public getRoute(): string
{
public getRoute(): string {
return "spt-dialogue";
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { GameController } from "@spt/controllers/GameController";
import { OnLoad } from "@spt/di/OnLoad";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
@ -21,25 +20,22 @@ import { INullResponseData } from "@spt/models/eft/httpResponse/INullResponseDat
import { SaveServer } from "@spt/servers/SaveServer";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { Watermark } from "@spt/utils/Watermark";
import { inject, injectable } from "tsyringe";
@injectable()
export class GameCallbacks implements OnLoad
{
export class GameCallbacks implements OnLoad {
constructor(
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("Watermark") protected watermark: Watermark,
@inject("SaveServer") protected saveServer: SaveServer,
@inject("GameController") protected gameController: GameController,
)
{}
) {}
public async onLoad(): Promise<void>
{
public async onLoad(): Promise<void> {
this.gameController.load();
}
public getRoute(): string
{
public getRoute(): string {
return "spt-game";
}
@ -47,8 +43,7 @@ export class GameCallbacks implements OnLoad
* Handle client/game/version/validate
* @returns INullResponseData
*/
public versionValidate(url: string, info: IVersionValidateRequestData, sessionID: string): INullResponseData
{
public versionValidate(url: string, info: IVersionValidateRequestData, sessionID: string): INullResponseData {
return this.httpResponse.nullResponse();
}
@ -60,8 +55,7 @@ export class GameCallbacks implements OnLoad
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<IGameStartResponse>
{
): IGetBodyResponseData<IGameStartResponse> {
const today = new Date().toUTCString();
const startTimeStampMS = Date.parse(today);
this.gameController.gameStart(url, info, sessionID, startTimeStampMS);
@ -77,8 +71,7 @@ export class GameCallbacks implements OnLoad
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<IGameLogoutResponseData>
{
): IGetBodyResponseData<IGameLogoutResponseData> {
this.saveServer.save();
return this.httpResponse.getBody({ status: "ok" });
}
@ -91,8 +84,7 @@ export class GameCallbacks implements OnLoad
url: string,
info: IGameEmptyCrcRequestData,
sessionID: string,
): IGetBodyResponseData<IGameConfigResponse>
{
): IGetBodyResponseData<IGameConfigResponse> {
return this.httpResponse.getBody(this.gameController.getGameConfig(sessionID));
}
@ -104,16 +96,14 @@ export class GameCallbacks implements OnLoad
url: string,
info: IGameModeRequestData,
sessionID: string,
): IGetBodyResponseData<IGameModeResponse>
{
): IGetBodyResponseData<IGameModeResponse> {
return this.httpResponse.getBody(this.gameController.getGameMode(sessionID, info));
}
/**
* Handle client/server/list
*/
public getServer(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<IServerDetails[]>
{
public getServer(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<IServerDetails[]> {
return this.httpResponse.getBody(this.gameController.getServer(sessionID));
}
@ -124,8 +114,7 @@ export class GameCallbacks implements OnLoad
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<ICurrentGroupResponse>
{
): IGetBodyResponseData<ICurrentGroupResponse> {
return this.httpResponse.getBody(this.gameController.getCurrentGroup(sessionID));
}
@ -136,8 +125,7 @@ export class GameCallbacks implements OnLoad
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<ICheckVersionResponse>
{
): IGetBodyResponseData<ICheckVersionResponse> {
return this.httpResponse.getBody(this.gameController.getValidGameVersion(sessionID));
}
@ -149,8 +137,7 @@ export class GameCallbacks implements OnLoad
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<IGameKeepAliveResponse>
{
): IGetBodyResponseData<IGameKeepAliveResponse> {
return this.httpResponse.getBody(this.gameController.getKeepAlive(sessionID));
}
@ -158,8 +145,7 @@ export class GameCallbacks implements OnLoad
* Handle singleplayer/settings/version
* @returns string
*/
public getVersion(url: string, info: IEmptyRequestData, sessionID: string): string
{
public getVersion(url: string, info: IEmptyRequestData, sessionID: string): string {
return this.httpResponse.noBody({ Version: this.watermark.getInGameVersionLabel() });
}
@ -167,8 +153,7 @@ export class GameCallbacks implements OnLoad
* Handle /client/report/send & /client/reports/lobby/send
* @returns INullResponseData
*/
public reportNickname(url: string, info: IUIDRequestData, sessionID: string): INullResponseData
{
public reportNickname(url: string, info: IUIDRequestData, sessionID: string): INullResponseData {
return this.httpResponse.nullResponse();
}
@ -176,8 +161,7 @@ export class GameCallbacks implements OnLoad
* Handle singleplayer/settings/getRaidTime
* @returns string
*/
public getRaidTime(url: string, request: IGetRaidTimeRequest, sessionID: string): IGetRaidTimeResponse
{
public getRaidTime(url: string, request: IGetRaidTimeRequest, sessionID: string): IGetRaidTimeResponse {
return this.httpResponse.noBody(this.gameController.getRaidTime(sessionID, request));
}
@ -185,8 +169,7 @@ export class GameCallbacks implements OnLoad
* Handle /client/survey
* @returns INullResponseData
*/
public getSurvey(url: string, request: IEmptyRequestData, sessionID: string): INullResponseData
{
public getSurvey(url: string, request: IEmptyRequestData, sessionID: string): INullResponseData {
return this.httpResponse.nullResponse();
}
}

View File

@ -1,20 +1,16 @@
import { inject, injectable } from "tsyringe";
import { HandbookController } from "@spt/controllers/HandbookController";
import { OnLoad } from "@spt/di/OnLoad";
import { inject, injectable } from "tsyringe";
@injectable()
export class HandbookCallbacks implements OnLoad
{
constructor(@inject("HandbookController") protected handbookController: HandbookController)
{}
export class HandbookCallbacks implements OnLoad {
constructor(@inject("HandbookController") protected handbookController: HandbookController) {}
public async onLoad(): Promise<void>
{
public async onLoad(): Promise<void> {
this.handbookController.load();
}
public getRoute(): string
{
public getRoute(): string {
return "spt-handbook";
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { HealthController } from "@spt/controllers/HealthController";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
@ -10,16 +9,15 @@ import { IWorkoutData } from "@spt/models/eft/health/IWorkoutData";
import { IGetBodyResponseData } from "@spt/models/eft/httpResponse/IGetBodyResponseData";
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class HealthCallbacks
{
export class HealthCallbacks {
constructor(
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("HealthController") protected healthController: HealthController,
)
{}
) {}
/**
* Custom spt server request found in modules/HealthSynchronizer.cs
@ -28,8 +26,7 @@ export class HealthCallbacks
* @param sessionID session id
* @returns empty response, no data sent back to client
*/
public syncHealth(url: string, info: ISyncHealthRequestData, sessionID: string): IGetBodyResponseData<string>
{
public syncHealth(url: string, info: ISyncHealthRequestData, sessionID: string): IGetBodyResponseData<string> {
this.healthController.saveVitality(this.profileHelper.getPmcProfile(sessionID), info, sessionID);
return this.httpResponse.emptyResponse();
}
@ -41,8 +38,7 @@ export class HealthCallbacks
* @param sessionID session id
* @returns empty response, no data sent back to client
*/
public handleWorkoutEffects(url: string, info: IWorkoutData, sessionID: string): IGetBodyResponseData<string>
{
public handleWorkoutEffects(url: string, info: IWorkoutData, sessionID: string): IGetBodyResponseData<string> {
this.healthController.applyWorkoutChanges(this.profileHelper.getPmcProfile(sessionID), info, sessionID);
return this.httpResponse.emptyResponse();
}
@ -51,8 +47,7 @@ export class HealthCallbacks
* Handle Eat
* @returns IItemEventRouterResponse
*/
public offraidEat(pmcData: IPmcData, body: IOffraidEatRequestData, sessionID: string): IItemEventRouterResponse
{
public offraidEat(pmcData: IPmcData, body: IOffraidEatRequestData, sessionID: string): IItemEventRouterResponse {
return this.healthController.offraidEat(pmcData, body, sessionID);
}
@ -60,8 +55,7 @@ export class HealthCallbacks
* Handle Heal
* @returns IItemEventRouterResponse
*/
public offraidHeal(pmcData: IPmcData, body: IOffraidHealRequestData, sessionID: string): IItemEventRouterResponse
{
public offraidHeal(pmcData: IPmcData, body: IOffraidHealRequestData, sessionID: string): IItemEventRouterResponse {
return this.healthController.offraidHeal(pmcData, body, sessionID);
}
@ -73,8 +67,7 @@ export class HealthCallbacks
pmcData: IPmcData,
info: IHealthTreatmentRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.healthController.healthTreatment(pmcData, info, sessionID);
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { HideoutController } from "@spt/controllers/HideoutController";
import { OnUpdate } from "@spt/di/OnUpdate";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
@ -19,17 +18,16 @@ import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRo
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { IHideoutConfig } from "@spt/models/spt/config/IHideoutConfig";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { inject, injectable } from "tsyringe";
@injectable()
export class HideoutCallbacks implements OnUpdate
{
export class HideoutCallbacks implements OnUpdate {
protected hideoutConfig: IHideoutConfig;
constructor(
@inject("HideoutController") protected hideoutController: HideoutController, // TODO: delay needed
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
) {
this.hideoutConfig = this.configServer.getConfig(ConfigTypes.HIDEOUT);
}
@ -41,8 +39,7 @@ export class HideoutCallbacks implements OnUpdate
body: IHideoutUpgradeRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.hideoutController.startUpgrade(pmcData, body, sessionID, output);
return output;
@ -56,8 +53,7 @@ export class HideoutCallbacks implements OnUpdate
body: IHideoutUpgradeCompleteRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.hideoutController.upgradeComplete(pmcData, body, sessionID, output);
return output;
@ -70,8 +66,7 @@ export class HideoutCallbacks implements OnUpdate
pmcData: IPmcData,
body: IHideoutPutItemInRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.hideoutController.putItemsInAreaSlots(pmcData, body, sessionID);
}
@ -82,8 +77,7 @@ export class HideoutCallbacks implements OnUpdate
pmcData: IPmcData,
body: IHideoutTakeItemOutRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.hideoutController.takeItemsFromAreaSlots(pmcData, body, sessionID);
}
@ -94,8 +88,7 @@ export class HideoutCallbacks implements OnUpdate
pmcData: IPmcData,
body: IHideoutToggleAreaRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.hideoutController.toggleArea(pmcData, body, sessionID);
}
@ -106,8 +99,7 @@ export class HideoutCallbacks implements OnUpdate
pmcData: IPmcData,
body: IHideoutSingleProductionStartRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.hideoutController.singleProductionStart(pmcData, body, sessionID);
}
@ -118,8 +110,7 @@ export class HideoutCallbacks implements OnUpdate
pmcData: IPmcData,
body: IHideoutScavCaseStartRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.hideoutController.scavCaseProductionStart(pmcData, body, sessionID);
}
@ -130,8 +121,7 @@ export class HideoutCallbacks implements OnUpdate
pmcData: IPmcData,
body: IHideoutContinuousProductionStartRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.hideoutController.continuousProductionStart(pmcData, body, sessionID);
}
@ -142,8 +132,7 @@ export class HideoutCallbacks implements OnUpdate
pmcData: IPmcData,
body: IHideoutTakeProductionRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.hideoutController.takeProduction(pmcData, body, sessionID);
}
@ -155,8 +144,7 @@ export class HideoutCallbacks implements OnUpdate
request: IHandleQTEEventRequestData,
sessionId: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.hideoutController.handleQTEEventOutcome(sessionId, pmcData, request, output);
return output;
@ -170,8 +158,7 @@ export class HideoutCallbacks implements OnUpdate
request: IRecordShootingRangePoints,
sessionId: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.hideoutController.recordShootingRangePoints(sessionId, pmcData, request);
return output;
@ -184,8 +171,7 @@ export class HideoutCallbacks implements OnUpdate
pmcData: IPmcData,
request: IHideoutImproveAreaRequestData,
sessionId: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.hideoutController.improveArea(sessionId, pmcData, request);
}
@ -196,23 +182,19 @@ export class HideoutCallbacks implements OnUpdate
pmcData: IPmcData,
request: IHideoutCancelProductionRequestData,
sessionId: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.hideoutController.cancelProduction(sessionId, pmcData, request);
}
public async onUpdate(timeSinceLastRun: number): Promise<boolean>
{
if (timeSinceLastRun > this.hideoutConfig.runIntervalSeconds)
{
public async onUpdate(timeSinceLastRun: number): Promise<boolean> {
if (timeSinceLastRun > this.hideoutConfig.runIntervalSeconds) {
this.hideoutController.update();
return true;
}
return false;
}
public getRoute(): string
{
public getRoute(): string {
return "spt-hideout";
}
}

View File

@ -1,25 +1,20 @@
import { inject, injectable } from "tsyringe";
import { OnLoad } from "@spt/di/OnLoad";
import { HttpServer } from "@spt/servers/HttpServer";
import { inject, injectable } from "tsyringe";
@injectable()
export class HttpCallbacks implements OnLoad
{
constructor(@inject("HttpServer") protected httpServer: HttpServer)
{}
export class HttpCallbacks implements OnLoad {
constructor(@inject("HttpServer") protected httpServer: HttpServer) {}
public async onLoad(): Promise<void>
{
public async onLoad(): Promise<void> {
this.httpServer.load();
}
public getRoute(): string
{
public getRoute(): string {
return "spt-http";
}
public getImage(): string
{
public getImage(): string {
return "";
}
}

View File

@ -1,22 +1,20 @@
import { inject, injectable } from "tsyringe";
import { InraidController } from "@spt/controllers/InraidController";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
import { INullResponseData } from "@spt/models/eft/httpResponse/INullResponseData";
import { IRegisterPlayerRequestData } from "@spt/models/eft/inRaid/IRegisterPlayerRequestData";
import { IScavSaveRequestData } from "@spt/models/eft/inRaid/IScavSaveRequestData";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
/**
* Handle client requests
*/
@injectable()
export class InraidCallbacks
{
export class InraidCallbacks {
constructor(
@inject("InraidController") protected inraidController: InraidController,
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
)
{}
) {}
/**
* Handle client/location/getLocalloot
@ -26,8 +24,7 @@ export class InraidCallbacks
* @param sessionID Session id
* @returns Null http response
*/
public registerPlayer(url: string, info: IRegisterPlayerRequestData, sessionID: string): INullResponseData
{
public registerPlayer(url: string, info: IRegisterPlayerRequestData, sessionID: string): INullResponseData {
this.inraidController.addPlayer(sessionID, info);
return this.httpResponse.nullResponse();
}
@ -39,8 +36,7 @@ export class InraidCallbacks
* @param sessionID Session id
* @returns Null http response
*/
public saveProgress(url: string, info: IScavSaveRequestData, sessionID: string): INullResponseData
{
public saveProgress(url: string, info: IScavSaveRequestData, sessionID: string): INullResponseData {
this.inraidController.savePostRaidProfileForScav(info, sessionID);
return this.httpResponse.nullResponse();
}
@ -50,8 +46,7 @@ export class InraidCallbacks
* Handle singleplayer/settings/raid/endstate
* @returns
*/
public getRaidEndState(): string
{
public getRaidEndState(): string {
return this.httpResponse.noBody(this.inraidController.getInraidConfig().MIAOnRaidEnd);
}
@ -59,18 +54,15 @@ export class InraidCallbacks
* Handle singleplayer/settings/raid/menu
* @returns JSON as string
*/
public getRaidMenuSettings(): string
{
public getRaidMenuSettings(): string {
return this.httpResponse.noBody(this.inraidController.getInraidConfig().raidMenuSettings);
}
public getTraitorScavHostileChance(url: string, info: IEmptyRequestData, sessionId: string): string
{
public getTraitorScavHostileChance(url: string, info: IEmptyRequestData, sessionId: string): string {
return this.httpResponse.noBody(this.inraidController.getTraitorScavHostileChance(url, sessionId));
}
public getBossConvertSettings(url: string, info: IEmptyRequestData, sessionId: string): string
{
public getBossConvertSettings(url: string, info: IEmptyRequestData, sessionId: string): string {
return this.httpResponse.noBody(this.inraidController.getBossConvertSettings(url, sessionId));
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { InsuranceController } from "@spt/controllers/InsuranceController";
import { OnUpdate } from "@spt/di/OnUpdate";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
@ -12,18 +11,17 @@ import { IInsuranceConfig } from "@spt/models/spt/config/IInsuranceConfig";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { InsuranceService } from "@spt/services/InsuranceService";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class InsuranceCallbacks implements OnUpdate
{
export class InsuranceCallbacks implements OnUpdate {
protected insuranceConfig: IInsuranceConfig;
constructor(
@inject("InsuranceController") protected insuranceController: InsuranceController,
@inject("InsuranceService") protected insuranceService: InsuranceService,
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
) {
this.insuranceConfig = this.configServer.getConfig(ConfigTypes.INSURANCE);
}
@ -35,8 +33,7 @@ export class InsuranceCallbacks implements OnUpdate
url: string,
info: IGetInsuranceCostRequestData,
sessionID: string,
): IGetBodyResponseData<IGetInsuranceCostResponseData>
{
): IGetBodyResponseData<IGetInsuranceCostResponseData> {
return this.httpResponse.getBody(this.insuranceController.cost(info, sessionID));
}
@ -44,24 +41,20 @@ export class InsuranceCallbacks implements OnUpdate
* Handle Insure event
* @returns IItemEventRouterResponse
*/
public insure(pmcData: IPmcData, body: IInsureRequestData, sessionID: string): IItemEventRouterResponse
{
public insure(pmcData: IPmcData, body: IInsureRequestData, sessionID: string): IItemEventRouterResponse {
return this.insuranceController.insure(pmcData, body, sessionID);
}
public async onUpdate(secondsSinceLastRun: number): Promise<boolean>
{
public async onUpdate(secondsSinceLastRun: number): Promise<boolean> {
// People edit the config value to be 0 and break it, force value to no lower than 1
if (secondsSinceLastRun > Math.max(this.insuranceConfig.runIntervalSeconds, 1))
{
if (secondsSinceLastRun > Math.max(this.insuranceConfig.runIntervalSeconds, 1)) {
this.insuranceController.processReturn();
return true;
}
return false;
}
public getRoute(): string
{
public getRoute(): string {
return "spt-insurance";
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { InventoryController } from "@spt/controllers/InventoryController";
import { QuestController } from "@spt/controllers/QuestController";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
@ -23,15 +22,14 @@ import { IRedeemProfileRequestData } from "@spt/models/eft/inventory/IRedeemProf
import { ISetFavoriteItems } from "@spt/models/eft/inventory/ISetFavoriteItems";
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { IFailQuestRequestData } from "@spt/models/eft/quests/IFailQuestRequestData";
import { inject, injectable } from "tsyringe";
@injectable()
export class InventoryCallbacks
{
export class InventoryCallbacks {
constructor(
@inject("InventoryController") protected inventoryController: InventoryController,
@inject("QuestController") protected questController: QuestController,
)
{}
) {}
/** Handle client/game/profile/items/moving Move event */
public moveItem(
@ -39,8 +37,7 @@ export class InventoryCallbacks
body: IInventoryMoveRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.inventoryController.moveItem(pmcData, body, sessionID, output);
return output;
@ -52,8 +49,7 @@ export class InventoryCallbacks
body: IInventoryRemoveRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.inventoryController.discardItem(pmcData, body, sessionID, output);
return output;
@ -65,8 +61,7 @@ export class InventoryCallbacks
body: IInventorySplitRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.inventoryController.splitItem(pmcData, body, sessionID, output);
}
@ -75,8 +70,7 @@ export class InventoryCallbacks
body: IInventoryMergeRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.inventoryController.mergeItem(pmcData, body, sessionID, output);
}
@ -85,20 +79,17 @@ export class InventoryCallbacks
request: IInventoryTransferRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.inventoryController.transferItem(pmcData, request, sessionID, output);
}
/** Handle Swap */
// TODO: how is this triggered
public swapItem(pmcData: IPmcData, body: IInventorySwapRequestData, sessionID: string): IItemEventRouterResponse
{
public swapItem(pmcData: IPmcData, body: IInventorySwapRequestData, sessionID: string): IItemEventRouterResponse {
return this.inventoryController.swapItem(pmcData, body, sessionID);
}
public foldItem(pmcData: IPmcData, body: IInventoryFoldRequestData, sessionID: string): IItemEventRouterResponse
{
public foldItem(pmcData: IPmcData, body: IInventoryFoldRequestData, sessionID: string): IItemEventRouterResponse {
return this.inventoryController.foldItem(pmcData, body, sessionID);
}
@ -106,13 +97,11 @@ export class InventoryCallbacks
pmcData: IPmcData,
body: IInventoryToggleRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.inventoryController.toggleItem(pmcData, body, sessionID);
}
public tagItem(pmcData: IPmcData, body: IInventoryTagRequestData, sessionID: string): IItemEventRouterResponse
{
public tagItem(pmcData: IPmcData, body: IInventoryTagRequestData, sessionID: string): IItemEventRouterResponse {
return this.inventoryController.tagItem(pmcData, body, sessionID);
}
@ -121,8 +110,7 @@ export class InventoryCallbacks
body: IInventoryBindRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.inventoryController.bindItem(pmcData, body, sessionID);
return output;
@ -133,8 +121,7 @@ export class InventoryCallbacks
body: IInventoryBindRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.inventoryController.unbindItem(pmcData, body, sessionID, output);
return output;
@ -145,8 +132,7 @@ export class InventoryCallbacks
body: IInventoryExamineRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.inventoryController.examineItem(pmcData, body, sessionID, output);
}
@ -155,8 +141,7 @@ export class InventoryCallbacks
pmcData: IPmcData,
body: IInventoryReadEncyclopediaRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.inventoryController.readEncyclopedia(pmcData, body, sessionID);
}
@ -166,8 +151,7 @@ export class InventoryCallbacks
body: IInventorySortRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.inventoryController.sortInventory(pmcData, body, sessionID);
return output;
@ -178,8 +162,7 @@ export class InventoryCallbacks
body: IInventoryCreateMarkerRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.inventoryController.createMapMarker(pmcData, body, sessionID, output);
return output;
@ -190,8 +173,7 @@ export class InventoryCallbacks
body: IInventoryDeleteMarkerRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.inventoryController.deleteMapMarker(pmcData, body, sessionID, output);
return output;
@ -202,8 +184,7 @@ export class InventoryCallbacks
body: IInventoryEditMarkerRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.inventoryController.editMapMarker(pmcData, body, sessionID, output);
return output;
@ -215,8 +196,7 @@ export class InventoryCallbacks
body: IOpenRandomLootContainerRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.inventoryController.openRandomLootContainer(pmcData, body, sessionID, output);
return output;
@ -227,8 +207,7 @@ export class InventoryCallbacks
body: IRedeemProfileRequestData,
sessionId: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.inventoryController.redeemProfileReward(pmcData, body, sessionId);
return output;
@ -239,8 +218,7 @@ export class InventoryCallbacks
body: ISetFavoriteItems,
sessionId: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.inventoryController.setFavoriteItem(pmcData, body, sessionId);
return output;
@ -255,8 +233,7 @@ export class InventoryCallbacks
request: IFailQuestRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.questController.failQuest(pmcData, request, sessionID, output);
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { IGetBodyResponseData } from "@spt/models/eft/httpResponse/IGetBodyResponseData";
import { Warning } from "@spt/models/eft/itemEvent/IItemEventRouterBase";
import { IItemEventRouterRequest } from "@spt/models/eft/itemEvent/IItemEventRouterRequest";
@ -6,22 +5,20 @@ import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRo
import { BackendErrorCodes } from "@spt/models/enums/BackendErrorCodes";
import { ItemEventRouter } from "@spt/routers/ItemEventRouter";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class ItemEventCallbacks
{
export class ItemEventCallbacks {
constructor(
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("ItemEventRouter") protected itemEventRouter: ItemEventRouter,
)
{}
) {}
public async handleEvents(
url: string,
info: IItemEventRouterRequest,
sessionID: string,
): Promise<IGetBodyResponseData<IItemEventRouterResponse>>
{
): Promise<IGetBodyResponseData<IItemEventRouterResponse>> {
const eventResponse = await this.itemEventRouter.handleEvents(info, sessionID);
const result = this.isCriticalError(eventResponse.warnings)
? this.httpResponse.getBody(
@ -39,15 +36,12 @@ export class ItemEventCallbacks
* @param warnings The list of warnings to check for critical errors
* @returns
*/
private isCriticalError(warnings: Warning[]): boolean
{
private isCriticalError(warnings: Warning[]): boolean {
// List of non-critical error codes, we return true if any error NOT included is passed in
const nonCriticalErrorCodes: BackendErrorCodes[] = [BackendErrorCodes.NOTENOUGHSPACE];
for (const warning of warnings)
{
if (!nonCriticalErrorCodes.includes(+(warning?.code ?? "0")))
{
for (const warning of warnings) {
if (!nonCriticalErrorCodes.includes(+(warning?.code ?? "0"))) {
return true;
}
}
@ -55,10 +49,8 @@ export class ItemEventCallbacks
return false;
}
protected getErrorCode(warnings: Warning[]): number
{
if (warnings[0]?.code)
{
protected getErrorCode(warnings: Warning[]): number {
if (warnings[0]?.code) {
return Number(warnings[0].code);
}
return BackendErrorCodes.UNKNOWN_ERROR;

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { LauncherController } from "@spt/controllers/LauncherController";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
import { IChangeRequestData } from "@spt/models/eft/launcher/IChangeRequestData";
@ -8,86 +7,72 @@ import { IRemoveProfileData } from "@spt/models/eft/launcher/IRemoveProfileData"
import { SaveServer } from "@spt/servers/SaveServer";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { Watermark } from "@spt/utils/Watermark";
import { inject, injectable } from "tsyringe";
@injectable()
export class LauncherCallbacks
{
export class LauncherCallbacks {
constructor(
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("LauncherController") protected launcherController: LauncherController,
@inject("SaveServer") protected saveServer: SaveServer,
@inject("Watermark") protected watermark: Watermark,
)
{}
) {}
public connect(): string
{
public connect(): string {
return this.httpResponse.noBody(this.launcherController.connect());
}
public login(url: string, info: ILoginRequestData, sessionID: string): string
{
public login(url: string, info: ILoginRequestData, sessionID: string): string {
const output = this.launcherController.login(info);
return !output ? "FAILED" : output;
}
public register(url: string, info: IRegisterData, sessionID: string): "FAILED" | "OK"
{
public register(url: string, info: IRegisterData, sessionID: string): "FAILED" | "OK" {
const output = this.launcherController.register(info);
return !output ? "FAILED" : "OK";
}
public get(url: string, info: ILoginRequestData, sessionID: string): string
{
public get(url: string, info: ILoginRequestData, sessionID: string): string {
const output = this.launcherController.find(this.launcherController.login(info));
return this.httpResponse.noBody(output);
}
public changeUsername(url: string, info: IChangeRequestData, sessionID: string): "FAILED" | "OK"
{
public changeUsername(url: string, info: IChangeRequestData, sessionID: string): "FAILED" | "OK" {
const output = this.launcherController.changeUsername(info);
return !output ? "FAILED" : "OK";
}
public changePassword(url: string, info: IChangeRequestData, sessionID: string): "FAILED" | "OK"
{
public changePassword(url: string, info: IChangeRequestData, sessionID: string): "FAILED" | "OK" {
const output = this.launcherController.changePassword(info);
return !output ? "FAILED" : "OK";
}
public wipe(url: string, info: IRegisterData, sessionID: string): "FAILED" | "OK"
{
public wipe(url: string, info: IRegisterData, sessionID: string): "FAILED" | "OK" {
const output = this.launcherController.wipe(info);
return !output ? "FAILED" : "OK";
}
public getServerVersion(): string
{
public getServerVersion(): string {
return this.httpResponse.noBody(this.watermark.getVersionTag());
}
public ping(url: string, info: IEmptyRequestData, sessionID: string): string
{
public ping(url: string, info: IEmptyRequestData, sessionID: string): string {
return this.httpResponse.noBody("pong!");
}
public removeProfile(url: string, info: IRemoveProfileData, sessionID: string): string
{
public removeProfile(url: string, info: IRemoveProfileData, sessionID: string): string {
return this.httpResponse.noBody(this.saveServer.removeProfile(sessionID));
}
public getCompatibleTarkovVersion(): string
{
public getCompatibleTarkovVersion(): string {
return this.httpResponse.noBody(this.launcherController.getCompatibleTarkovVersion());
}
public getLoadedServerMods(): string
{
public getLoadedServerMods(): string {
return this.httpResponse.noBody(this.launcherController.getLoadedServerMods());
}
public getServerModsProfileUsed(url: string, info: IEmptyRequestData, sessionId: string): string
{
public getServerModsProfileUsed(url: string, info: IEmptyRequestData, sessionId: string): string {
return this.httpResponse.noBody(this.launcherController.getServerModsProfileUsed(sessionId));
}
}

View File

@ -1,27 +1,24 @@
import { inject, injectable } from "tsyringe";
import { LocationController } from "@spt/controllers/LocationController";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
import { ILocationsGenerateAllResponse } from "@spt/models/eft/common/ILocationsSourceDestinationBase";
import { IGetBodyResponseData } from "@spt/models/eft/httpResponse/IGetBodyResponseData";
import { IGetAirdropLootResponse } from "@spt/models/eft/location/IGetAirdropLootResponse";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class LocationCallbacks
{
export class LocationCallbacks {
constructor(
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("LocationController") protected locationController: LocationController,
)
{}
) {}
/** Handle client/locations */
public getLocationData(
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<ILocationsGenerateAllResponse>
{
): IGetBodyResponseData<ILocationsGenerateAllResponse> {
return this.httpResponse.getBody(this.locationController.generateAll(sessionID));
}
@ -30,8 +27,7 @@ export class LocationCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<IGetAirdropLootResponse>
{
): IGetBodyResponseData<IGetAirdropLootResponse> {
return this.httpResponse.getBody(this.locationController.getAirdropLoot());
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { MatchController } from "@spt/controllers/MatchController";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
import { IGetBodyResponseData } from "@spt/models/eft/httpResponse/IGetBodyResponseData";
@ -22,33 +21,29 @@ import { IUpdatePingRequestData } from "@spt/models/eft/match/IUpdatePingRequest
import { DatabaseService } from "@spt/services/DatabaseService";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { JsonUtil } from "@spt/utils/JsonUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class MatchCallbacks
{
export class MatchCallbacks {
constructor(
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("MatchController") protected matchController: MatchController,
@inject("DatabaseService") protected databaseService: DatabaseService,
)
{}
) {}
/** Handle client/match/updatePing */
public updatePing(url: string, info: IUpdatePingRequestData, sessionID: string): INullResponseData
{
public updatePing(url: string, info: IUpdatePingRequestData, sessionID: string): INullResponseData {
return this.httpResponse.nullResponse();
}
// Handle client/match/exit
public exitMatch(url: string, info: IEmptyRequestData, sessionID: string): INullResponseData
{
public exitMatch(url: string, info: IEmptyRequestData, sessionID: string): INullResponseData {
return this.httpResponse.nullResponse();
}
/** Handle client/match/group/exit_from_menu */
public exitToMenu(url: string, info: IEmptyRequestData, sessionID: string): INullResponseData
{
public exitToMenu(url: string, info: IEmptyRequestData, sessionID: string): INullResponseData {
return this.httpResponse.nullResponse();
}
@ -56,18 +51,15 @@ export class MatchCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<IMatchGroupCurrentResponse>
{
): IGetBodyResponseData<IMatchGroupCurrentResponse> {
return this.httpResponse.getBody({ squad: [] });
}
public startGroupSearch(url: string, info: IEmptyRequestData, sessionID: string): INullResponseData
{
public startGroupSearch(url: string, info: IEmptyRequestData, sessionID: string): INullResponseData {
return this.httpResponse.nullResponse();
}
public stopGroupSearch(url: string, info: IEmptyRequestData, sessionID: string): INullResponseData
{
public stopGroupSearch(url: string, info: IEmptyRequestData, sessionID: string): INullResponseData {
return this.httpResponse.nullResponse();
}
@ -76,8 +68,7 @@ export class MatchCallbacks
url: string,
info: IMatchGroupInviteSendRequest,
sessionID: string,
): IGetBodyResponseData<string>
{
): IGetBodyResponseData<string> {
return this.httpResponse.getBody("2427943f23698ay9f2863735");
}
@ -86,8 +77,7 @@ export class MatchCallbacks
url: string,
info: IRequestIdRequest,
sessionId: string,
): IGetBodyResponseData<IGroupCharacter[]>
{
): IGetBodyResponseData<IGroupCharacter[]> {
const result = [];
result.push({});
@ -95,14 +85,12 @@ export class MatchCallbacks
}
/** Handle client/match/group/invite/decline */
public declineGroupInvite(url: string, info: IRequestIdRequest, sessionId: string): IGetBodyResponseData<boolean>
{
public declineGroupInvite(url: string, info: IRequestIdRequest, sessionId: string): IGetBodyResponseData<boolean> {
return this.httpResponse.getBody(true);
}
/** Handle client/match/group/invite/cancel */
public cancelGroupInvite(url: string, info: IRequestIdRequest, sessionID: string): IGetBodyResponseData<boolean>
{
public cancelGroupInvite(url: string, info: IRequestIdRequest, sessionID: string): IGetBodyResponseData<boolean> {
return this.httpResponse.getBody(true);
}
@ -111,8 +99,7 @@ export class MatchCallbacks
url: string,
info: IMatchGroupTransferRequest,
sessionId: string,
): IGetBodyResponseData<boolean>
{
): IGetBodyResponseData<boolean> {
return this.httpResponse.getBody(true);
}
@ -121,19 +108,16 @@ export class MatchCallbacks
url: string,
info: IEmptyRequestData,
sessionId: string,
): IGetBodyResponseData<boolean>
{
): IGetBodyResponseData<boolean> {
return this.httpResponse.getBody(true);
}
public putMetrics(url: string, request: IPutMetricsRequestData, sessionId: string): INullResponseData
{
public putMetrics(url: string, request: IPutMetricsRequestData, sessionId: string): INullResponseData {
return this.httpResponse.nullResponse();
}
// Handle client/match/available
public serverAvailable(url: string, info: IEmptyRequestData, sessionId: string): IGetBodyResponseData<boolean>
{
public serverAvailable(url: string, info: IEmptyRequestData, sessionId: string): IGetBodyResponseData<boolean> {
const output = this.matchController.getEnabled();
return this.httpResponse.getBody(output);
@ -144,14 +128,12 @@ export class MatchCallbacks
url: string,
info: IMatchGroupStartGameRequest,
sessionID: string,
): IGetBodyResponseData<IProfileStatusResponse>
{
): IGetBodyResponseData<IProfileStatusResponse> {
return this.httpResponse.getBody(this.matchController.joinMatch(info, sessionID));
}
/** Handle client/getMetricsConfig */
public getMetrics(url: string, info: any, sessionID: string): IGetBodyResponseData<string>
{
public getMetrics(url: string, info: any, sessionID: string): IGetBodyResponseData<string> {
return this.httpResponse.getBody(this.jsonUtil.serialize(this.databaseService.getMatch().metrics));
}
@ -164,21 +146,18 @@ export class MatchCallbacks
url: string,
info: IMatchGroupStatusRequest,
sessionID: string,
): IGetBodyResponseData<IMatchGroupStatusResponse>
{
): IGetBodyResponseData<IMatchGroupStatusResponse> {
return this.httpResponse.getBody(this.matchController.getGroupStatus(info));
}
/** Handle client/match/group/delete */
public deleteGroup(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<boolean>
{
public deleteGroup(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<boolean> {
this.matchController.deleteGroup(info);
return this.httpResponse.getBody(true);
}
// Handle client/match/group/leave
public leaveGroup(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<boolean>
{
public leaveGroup(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<boolean> {
return this.httpResponse.getBody(true);
}
@ -187,8 +166,7 @@ export class MatchCallbacks
url: string,
info: IMatchGroupPlayerRemoveRequest,
sessionID: string,
): IGetBodyResponseData<boolean>
{
): IGetBodyResponseData<boolean> {
return this.httpResponse.getBody(true);
}
@ -197,18 +175,12 @@ export class MatchCallbacks
url: string,
info: IStartLocalRaidRequestData,
sessionID: string,
): IGetBodyResponseData<IStartLocalRaidResponseData>
{
): IGetBodyResponseData<IStartLocalRaidResponseData> {
return this.httpResponse.getBody(this.matchController.startLocalRaid(sessionID, info));
}
/** Handle client/match/local/end */
public endLocalRaid(
url: string,
info: IEndLocalRaidRequestData,
sessionID: string,
): INullResponseData
{
public endLocalRaid(url: string, info: IEndLocalRaidRequestData, sessionID: string): INullResponseData {
this.matchController.endLocalRaid(sessionID, info);
return this.httpResponse.nullResponse();
}
@ -218,8 +190,7 @@ export class MatchCallbacks
url: string,
info: IGetRaidConfigurationRequestData,
sessionID: string,
): INullResponseData
{
): INullResponseData {
this.matchController.configureOfflineRaid(info, sessionID);
return this.httpResponse.nullResponse();
}
@ -229,20 +200,17 @@ export class MatchCallbacks
url: string,
info: IGetRaidConfigurationRequestData,
sessionID: string,
): INullResponseData
{
): INullResponseData {
return this.httpResponse.nullResponse();
}
/** Handle client/match/group/raid/ready */
public raidReady(url: string, info: IEmptyRequestData, sessionId: string): IGetBodyResponseData<boolean>
{
public raidReady(url: string, info: IEmptyRequestData, sessionId: string): IGetBodyResponseData<boolean> {
return this.httpResponse.getBody(true);
}
/** Handle client/match/group/raid/not-ready */
public notRaidReady(url: string, info: IEmptyRequestData, sessionId: string): IGetBodyResponseData<boolean>
{
public notRaidReady(url: string, info: IEmptyRequestData, sessionId: string): IGetBodyResponseData<boolean> {
return this.httpResponse.getBody(true);
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { OnLoad } from "@spt/di/OnLoad";
import { PostSptModLoader } from "@spt/loaders/PostSptModLoader";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
@ -8,10 +7,10 @@ import { ConfigServer } from "@spt/servers/ConfigServer";
import { LocalisationService } from "@spt/services/LocalisationService";
import { HttpFileUtil } from "@spt/utils/HttpFileUtil";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class ModCallbacks implements OnLoad
{
export class ModCallbacks implements OnLoad {
protected httpConfig: IHttpConfig;
constructor(
@ -21,21 +20,17 @@ export class ModCallbacks implements OnLoad
@inject("PostSptModLoader") protected postSptModLoader: PostSptModLoader,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
) {
this.httpConfig = this.configServer.getConfig(ConfigTypes.HTTP);
}
public async onLoad(): Promise<void>
{
if (globalThis.G_MODS_ENABLED)
{
public async onLoad(): Promise<void> {
if (globalThis.G_MODS_ENABLED) {
await this.postSptModLoader.load();
}
}
public getRoute(): string
{
public getRoute(): string {
return "spt-mods";
}
}

View File

@ -1,30 +1,25 @@
import { inject, injectable } from "tsyringe";
import { NoteController } from "@spt/controllers/NoteController";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { INoteActionData } from "@spt/models/eft/notes/INoteActionData";
import { inject, injectable } from "tsyringe";
@injectable()
export class NoteCallbacks
{
constructor(@inject("NoteController") protected noteController: NoteController)
{}
export class NoteCallbacks {
constructor(@inject("NoteController") protected noteController: NoteController) {}
/** Handle AddNote event */
public addNote(pmcData: IPmcData, body: INoteActionData, sessionID: string): IItemEventRouterResponse
{
public addNote(pmcData: IPmcData, body: INoteActionData, sessionID: string): IItemEventRouterResponse {
return this.noteController.addNote(pmcData, body, sessionID);
}
/** Handle EditNote event */
public editNote(pmcData: IPmcData, body: INoteActionData, sessionID: string): IItemEventRouterResponse
{
public editNote(pmcData: IPmcData, body: INoteActionData, sessionID: string): IItemEventRouterResponse {
return this.noteController.editNote(pmcData, body, sessionID);
}
/** Handle DeleteNote event */
public deleteNote(pmcData: IPmcData, body: INoteActionData, sessionID: string): IItemEventRouterResponse
{
public deleteNote(pmcData: IPmcData, body: INoteActionData, sessionID: string): IItemEventRouterResponse {
return this.noteController.deleteNote(pmcData, body, sessionID);
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { NotifierController } from "@spt/controllers/NotifierController";
import { HttpServerHelper } from "@spt/helpers/HttpServerHelper";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
@ -8,17 +7,16 @@ import { INotifierChannel } from "@spt/models/eft/notifier/INotifier";
import { ISelectProfileResponse } from "@spt/models/eft/notifier/ISelectProfileResponse";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { JsonUtil } from "@spt/utils/JsonUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class NotifierCallbacks
{
export class NotifierCallbacks {
constructor(
@inject("HttpServerHelper") protected httpServerHelper: HttpServerHelper,
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("NotifierController") protected notifierController: NotifierController,
)
{}
) {}
/**
* If we don't have anything to send, it's ok to not send anything back
@ -26,8 +24,7 @@ export class NotifierCallbacks
* until we actually have something to send because otherwise we'd spam the client
* and the client would abort the connection due to spam.
*/
public sendNotification(sessionID: string, req: any, resp: any, data: any): void
{
public sendNotification(sessionID: string, req: any, resp: any, data: any): void {
const splittedUrl = req.url.split("/");
const tmpSessionID = splittedUrl[splittedUrl.length - 1].split("?last_id")[0];
@ -44,8 +41,7 @@ export class NotifierCallbacks
/** Handle push/notifier/get */
/** Handle push/notifier/getwebsocket */
// TODO: removed from client?
public getNotifier(url: string, info: any, sessionID: string): IGetBodyResponseData<any[]>
{
public getNotifier(url: string, info: any, sessionID: string): IGetBodyResponseData<any[]> {
return this.httpResponse.emptyArrayResponse();
}
@ -54,8 +50,7 @@ export class NotifierCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<INotifierChannel>
{
): IGetBodyResponseData<INotifierChannel> {
return this.httpResponse.getBody(this.notifierController.getChannel(sessionID));
}
@ -67,13 +62,11 @@ export class NotifierCallbacks
url: string,
info: IUIDRequestData,
sessionID: string,
): IGetBodyResponseData<ISelectProfileResponse>
{
): IGetBodyResponseData<ISelectProfileResponse> {
return this.httpResponse.getBody({ status: "ok" });
}
public notify(url: string, info: any, sessionID: string): string
{
public notify(url: string, info: any, sessionID: string): string {
return "NOTIFY";
}
}

View File

@ -1,20 +1,16 @@
import { inject, injectable } from "tsyringe";
import { PresetController } from "@spt/controllers/PresetController";
import { OnLoad } from "@spt/di/OnLoad";
import { inject, injectable } from "tsyringe";
@injectable()
export class PresetCallbacks implements OnLoad
{
constructor(@inject("PresetController") protected presetController: PresetController)
{}
export class PresetCallbacks implements OnLoad {
constructor(@inject("PresetController") protected presetController: PresetController) {}
public async onLoad(): Promise<void>
{
public async onLoad(): Promise<void> {
this.presetController.initialize();
}
public getRoute(): string
{
public getRoute(): string {
return "spt-presets";
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { ProfileController } from "@spt/controllers/ProfileController";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
@ -19,18 +18,17 @@ import { ISearchFriendResponse } from "@spt/models/eft/profile/ISearchFriendResp
import { IValidateNicknameRequestData } from "@spt/models/eft/profile/IValidateNicknameRequestData";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { inject, injectable } from "tsyringe";
/** Handle profile related client events */
@injectable()
export class ProfileCallbacks
{
export class ProfileCallbacks {
constructor(
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("ProfileController") protected profileController: ProfileController,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
)
{}
) {}
/**
* Handle client/game/profile/create
@ -39,8 +37,7 @@ export class ProfileCallbacks
url: string,
info: IProfileCreateRequestData,
sessionID: string,
): IGetBodyResponseData<ICreateProfileResponse>
{
): IGetBodyResponseData<ICreateProfileResponse> {
const id = this.profileController.createProfile(info, sessionID);
return this.httpResponse.getBody({ uid: id });
}
@ -49,8 +46,7 @@ export class ProfileCallbacks
* Handle client/game/profile/list
* Get the complete player profile (scav + pmc character)
*/
public getProfileData(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<IPmcData[]>
{
public getProfileData(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<IPmcData[]> {
return this.httpResponse.getBody(this.profileController.getCompleteProfile(sessionID));
}
@ -63,16 +59,14 @@ export class ProfileCallbacks
* @param sessionID Session id
* @returns Profile object
*/
public regenerateScav(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<IPmcData[]>
{
public regenerateScav(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<IPmcData[]> {
return this.httpResponse.getBody([this.profileController.generatePlayerScav(sessionID)]);
}
/**
* Handle client/game/profile/voice/change event
*/
public changeVoice(url: string, info: IProfileChangeVoiceRequestData, sessionID: string): INullResponseData
{
public changeVoice(url: string, info: IProfileChangeVoiceRequestData, sessionID: string): INullResponseData {
this.profileController.changeVoice(info, sessionID);
return this.httpResponse.nullResponse();
}
@ -85,17 +79,14 @@ export class ProfileCallbacks
url: string,
info: IProfileChangeNicknameRequestData,
sessionID: string,
): IGetBodyResponseData<any>
{
): IGetBodyResponseData<any> {
const output = this.profileController.changeNickname(info, sessionID);
if (output === "taken")
{
if (output === "taken") {
return this.httpResponse.getBody(undefined, 255, "The nickname is already in use");
}
if (output === "tooshort")
{
if (output === "tooshort") {
return this.httpResponse.getBody(undefined, 1, "The nickname is too short");
}
@ -109,17 +100,14 @@ export class ProfileCallbacks
url: string,
info: IValidateNicknameRequestData,
sessionID: string,
): IGetBodyResponseData<any>
{
): IGetBodyResponseData<any> {
const output = this.profileController.validateNickname(info, sessionID);
if (output === "taken")
{
if (output === "taken") {
return this.httpResponse.getBody(undefined, 255, "225 - ");
}
if (output === "tooshort")
{
if (output === "tooshort") {
return this.httpResponse.getBody(undefined, 256, "256 - ");
}
@ -129,8 +117,7 @@ export class ProfileCallbacks
/**
* Handle client/game/profile/nickname/reserved
*/
public getReservedNickname(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<string>
{
public getReservedNickname(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<string> {
return this.httpResponse.getBody("SPTarkov");
}
@ -142,8 +129,7 @@ export class ProfileCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<GetProfileStatusResponseData>
{
): IGetBodyResponseData<GetProfileStatusResponseData> {
return this.httpResponse.getBody(this.profileController.getProfileStatus(sessionID));
}
@ -155,8 +141,7 @@ export class ProfileCallbacks
url: string,
request: IGetOtherProfileRequest,
sessionID: string,
): IGetBodyResponseData<IGetOtherProfileResponse>
{
): IGetBodyResponseData<IGetOtherProfileResponse> {
return this.httpResponse.getBody(this.profileController.getOtherProfile(sessionID, request));
}
@ -167,8 +152,7 @@ export class ProfileCallbacks
url: string,
info: IGetProfileSettingsRequest,
sessionId: string,
): IGetBodyResponseData<boolean>
{
): IGetBodyResponseData<boolean> {
return this.httpResponse.getBody(this.profileController.setChosenProfileIcon(sessionId, info));
}
@ -179,24 +163,21 @@ export class ProfileCallbacks
url: string,
info: ISearchFriendRequestData,
sessionID: string,
): IGetBodyResponseData<ISearchFriendResponse[]>
{
): IGetBodyResponseData<ISearchFriendResponse[]> {
return this.httpResponse.getBody(this.profileController.getFriends(info, sessionID));
}
/**
* Handle launcher/profile/info
*/
public getMiniProfile(url: string, info: IGetMiniProfileRequestData, sessionID: string): string
{
public getMiniProfile(url: string, info: IGetMiniProfileRequestData, sessionID: string): string {
return this.httpResponse.noBody(this.profileController.getMiniProfile(sessionID));
}
/**
* Handle /launcher/profiles
*/
public getAllMiniProfiles(url: string, info: IEmptyRequestData, sessionID: string): string
{
public getAllMiniProfiles(url: string, info: IEmptyRequestData, sessionID: string): string {
return this.httpResponse.noBody(this.profileController.getMiniProfiles());
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { QuestController } from "@spt/controllers/QuestController";
import { RepeatableQuestController } from "@spt/controllers/RepeatableQuestController";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
@ -13,16 +12,15 @@ import { IHandoverQuestRequestData } from "@spt/models/eft/quests/IHandoverQuest
import { IListQuestsRequestData } from "@spt/models/eft/quests/IListQuestsRequestData";
import { IRepeatableQuestChangeRequest } from "@spt/models/eft/quests/IRepeatableQuestChangeRequest";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class QuestCallbacks
{
export class QuestCallbacks {
constructor(
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("QuestController") protected questController: QuestController,
@inject("RepeatableQuestController") protected repeatableQuestController: RepeatableQuestController,
)
{}
) {}
/**
* Handle RepeatableQuestChange event
@ -31,18 +29,15 @@ export class QuestCallbacks
pmcData: IPmcData,
body: IRepeatableQuestChangeRequest,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.repeatableQuestController.changeRepeatableQuest(pmcData, body, sessionID);
}
/**
* Handle QuestAccept event
*/
public acceptQuest(pmcData: IPmcData, body: IAcceptQuestRequestData, sessionID: string): IItemEventRouterResponse
{
if (body.type === "repeatable")
{
public acceptQuest(pmcData: IPmcData, body: IAcceptQuestRequestData, sessionID: string): IItemEventRouterResponse {
if (body.type === "repeatable") {
return this.questController.acceptRepeatableQuest(pmcData, body, sessionID);
}
return this.questController.acceptQuest(pmcData, body, sessionID);
@ -55,8 +50,7 @@ export class QuestCallbacks
pmcData: IPmcData,
body: ICompleteQuestRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.questController.completeQuest(pmcData, body, sessionID);
}
@ -67,16 +61,14 @@ export class QuestCallbacks
pmcData: IPmcData,
body: IHandoverQuestRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.questController.handoverQuest(pmcData, body, sessionID);
}
/**
* Handle client/quest/list
*/
public listQuests(url: string, info: IListQuestsRequestData, sessionID: string): IGetBodyResponseData<IQuest[]>
{
public listQuests(url: string, info: IListQuestsRequestData, sessionID: string): IGetBodyResponseData<IQuest[]> {
return this.httpResponse.getBody(this.questController.getClientQuests(sessionID));
}
@ -87,8 +79,7 @@ export class QuestCallbacks
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<IPmcDataRepeatableQuest[]>
{
): IGetBodyResponseData<IPmcDataRepeatableQuest[]> {
return this.httpResponse.getBody(this.repeatableQuestController.getClientRepeatableQuests(sessionID));
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { RagfairController } from "@spt/controllers/RagfairController";
import { OnLoad } from "@spt/di/OnLoad";
import { OnUpdate } from "@spt/di/OnUpdate";
@ -24,13 +23,13 @@ import { ConfigServer } from "@spt/servers/ConfigServer";
import { RagfairServer } from "@spt/servers/RagfairServer";
import { RagfairTaxService } from "@spt/services/RagfairTaxService";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
/**
* Handle ragfair related callback events
*/
@injectable()
export class RagfairCallbacks implements OnLoad, OnUpdate
{
export class RagfairCallbacks implements OnLoad, OnUpdate {
protected ragfairConfig: IRagfairConfig;
constructor(
@ -39,25 +38,20 @@ export class RagfairCallbacks implements OnLoad, OnUpdate
@inject("RagfairController") protected ragfairController: RagfairController,
@inject("RagfairTaxService") protected ragfairTaxService: RagfairTaxService,
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
) {
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
}
public async onLoad(): Promise<void>
{
public async onLoad(): Promise<void> {
await this.ragfairServer.load();
}
public getRoute(): string
{
public getRoute(): string {
return "spt-ragfair";
}
public async onUpdate(timeSinceLastRun: number): Promise<boolean>
{
if (timeSinceLastRun > this.ragfairConfig.runIntervalSeconds)
{
public async onUpdate(timeSinceLastRun: number): Promise<boolean> {
if (timeSinceLastRun > this.ragfairConfig.runIntervalSeconds) {
// There is a flag inside this class that only makes it run once.
this.ragfairServer.addPlayerOffers();
@ -76,8 +70,7 @@ export class RagfairCallbacks implements OnLoad, OnUpdate
* Handle client/ragfair/search
* Handle client/ragfair/find
*/
public search(url: string, info: ISearchRequestData, sessionID: string): IGetBodyResponseData<IGetOffersResult>
{
public search(url: string, info: ISearchRequestData, sessionID: string): IGetBodyResponseData<IGetOffersResult> {
return this.httpResponse.getBody(this.ragfairController.getOffers(sessionID, info));
}
@ -86,26 +79,22 @@ export class RagfairCallbacks implements OnLoad, OnUpdate
url: string,
info: IGetMarketPriceRequestData,
sessionID: string,
): IGetBodyResponseData<IGetItemPriceResult>
{
): IGetBodyResponseData<IGetItemPriceResult> {
return this.httpResponse.getBody(this.ragfairController.getItemMinAvgMaxFleaPriceValues(info));
}
/** Handle RagFairAddOffer event */
public addOffer(pmcData: IPmcData, info: IAddOfferRequestData, sessionID: string): IItemEventRouterResponse
{
public addOffer(pmcData: IPmcData, info: IAddOfferRequestData, sessionID: string): IItemEventRouterResponse {
return this.ragfairController.addPlayerOffer(pmcData, info, sessionID);
}
/** Handle RagFairRemoveOffer event */
public removeOffer(pmcData: IPmcData, info: IRemoveOfferRequestData, sessionID: string): IItemEventRouterResponse
{
public removeOffer(pmcData: IPmcData, info: IRemoveOfferRequestData, sessionID: string): IItemEventRouterResponse {
return this.ragfairController.removeOffer(info, sessionID);
}
/** Handle RagFairRenewOffer event */
public extendOffer(pmcData: IPmcData, info: IExtendOfferRequestData, sessionID: string): IItemEventRouterResponse
{
public extendOffer(pmcData: IPmcData, info: IExtendOfferRequestData, sessionID: string): IItemEventRouterResponse {
return this.ragfairController.extendOffer(info, sessionID);
}
@ -117,14 +106,12 @@ export class RagfairCallbacks implements OnLoad, OnUpdate
url: string,
request: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<Record<string, number>>
{
): IGetBodyResponseData<Record<string, number>> {
return this.httpResponse.getBody(this.ragfairController.getAllFleaPrices());
}
/** Handle client/reports/ragfair/send */
public sendReport(url: string, info: ISendRagfairReportRequestData, sessionID: string): INullResponseData
{
public sendReport(url: string, info: ISendRagfairReportRequestData, sessionID: string): INullResponseData {
return this.httpResponse.nullResponse();
}
@ -132,8 +119,7 @@ export class RagfairCallbacks implements OnLoad, OnUpdate
url: string,
request: IStorePlayerOfferTaxAmountRequestData,
sessionId: string,
): INullResponseData
{
): INullResponseData {
this.ragfairTaxService.storeClientOfferTaxValue(sessionId, request);
return this.httpResponse.nullResponse();
}
@ -143,8 +129,7 @@ export class RagfairCallbacks implements OnLoad, OnUpdate
url: string,
request: IGetRagfairOfferByIdRequest,
sessionID: string,
): IGetBodyResponseData<IRagfairOffer>
{
): IGetBodyResponseData<IRagfairOffer> {
return this.httpResponse.getBody(this.ragfairController.getOfferById(sessionID, request));
}
}

View File

@ -1,15 +1,13 @@
import { inject, injectable } from "tsyringe";
import { RepairController } from "@spt/controllers/RepairController";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { IRepairActionDataRequest } from "@spt/models/eft/repair/IRepairActionDataRequest";
import { ITraderRepairActionDataRequest } from "@spt/models/eft/repair/ITraderRepairActionDataRequest";
import { inject, injectable } from "tsyringe";
@injectable()
export class RepairCallbacks
{
constructor(@inject("RepairController") protected repairController: RepairController)
{}
export class RepairCallbacks {
constructor(@inject("RepairController") protected repairController: RepairController) {}
/**
* Handle TraderRepair event
@ -23,8 +21,7 @@ export class RepairCallbacks
pmcData: IPmcData,
traderRepairRequest: ITraderRepairActionDataRequest,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.repairController.traderRepair(sessionID, traderRepairRequest, pmcData);
}
@ -40,8 +37,7 @@ export class RepairCallbacks
pmcData: IPmcData,
repairRequest: IRepairActionDataRequest,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.repairController.repairWithKit(sessionID, repairRequest, pmcData);
}
}

View File

@ -1,39 +1,33 @@
import { inject, injectable } from "tsyringe";
import { OnLoad } from "@spt/di/OnLoad";
import { OnUpdate } from "@spt/di/OnUpdate";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { ICoreConfig } from "@spt/models/spt/config/ICoreConfig";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { SaveServer } from "@spt/servers/SaveServer";
import { inject, injectable } from "tsyringe";
@injectable()
export class SaveCallbacks implements OnLoad, OnUpdate
{
export class SaveCallbacks implements OnLoad, OnUpdate {
protected coreConfig: ICoreConfig;
constructor(
@inject("SaveServer") protected saveServer: SaveServer,
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
) {
this.coreConfig = this.configServer.getConfig(ConfigTypes.CORE);
}
public async onLoad(): Promise<void>
{
public async onLoad(): Promise<void> {
this.saveServer.load();
}
public getRoute(): string
{
public getRoute(): string {
return "spt-save";
}
public async onUpdate(secondsSinceLastRun: number): Promise<boolean>
{
public async onUpdate(secondsSinceLastRun: number): Promise<boolean> {
// run every 15 seconds
if (secondsSinceLastRun > this.coreConfig.profileSaveIntervalSeconds)
{
if (secondsSinceLastRun > this.coreConfig.profileSaveIntervalSeconds) {
this.saveServer.save();
return true;
}

View File

@ -1,16 +1,14 @@
import { inject, injectable } from "tsyringe";
import { TradeController } from "@spt/controllers/TradeController";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { IProcessBaseTradeRequestData } from "@spt/models/eft/trade/IProcessBaseTradeRequestData";
import { IProcessRagfairTradeRequestData } from "@spt/models/eft/trade/IProcessRagfairTradeRequestData";
import { ISellScavItemsToFenceRequestData } from "@spt/models/eft/trade/ISellScavItemsToFenceRequestData";
import { inject, injectable } from "tsyringe";
@injectable()
export class TradeCallbacks
{
constructor(@inject("TradeController") protected tradeController: TradeController)
{}
export class TradeCallbacks {
constructor(@inject("TradeController") protected tradeController: TradeController) {}
/**
* Handle client/game/profile/items/moving TradingConfirm event
@ -19,8 +17,7 @@ export class TradeCallbacks
pmcData: IPmcData,
body: IProcessBaseTradeRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
// body can be IProcessBuyTradeRequestData or IProcessSellTradeRequestData
return this.tradeController.confirmTrading(pmcData, body, sessionID);
}
@ -30,8 +27,7 @@ export class TradeCallbacks
pmcData: IPmcData,
body: IProcessRagfairTradeRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.tradeController.confirmRagfairTrading(pmcData, body, sessionID);
}
@ -40,8 +36,7 @@ export class TradeCallbacks
pmcData: IPmcData,
body: ISellScavItemsToFenceRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.tradeController.sellScavItemsToFence(pmcData, body, sessionID);
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { TraderController } from "@spt/controllers/TraderController";
import { OnLoad } from "@spt/di/OnLoad";
import { OnUpdate } from "@spt/di/OnUpdate";
@ -6,28 +5,24 @@ import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
import { ITraderAssort, ITraderBase } from "@spt/models/eft/common/tables/ITrader";
import { IGetBodyResponseData } from "@spt/models/eft/httpResponse/IGetBodyResponseData";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class TraderCallbacks implements OnLoad, OnUpdate
{
export class TraderCallbacks implements OnLoad, OnUpdate {
constructor(
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil, // TODO: delay required
@inject("TraderController") protected traderController: TraderController,
)
{}
) {}
public async onLoad(): Promise<void>
{
public async onLoad(): Promise<void> {
this.traderController.load();
}
public async onUpdate(): Promise<boolean>
{
public async onUpdate(): Promise<boolean> {
return this.traderController.update();
}
public getRoute(): string
{
public getRoute(): string {
return "spt-traders";
}
@ -36,21 +31,18 @@ export class TraderCallbacks implements OnLoad, OnUpdate
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<ITraderBase[]>
{
): IGetBodyResponseData<ITraderBase[]> {
return this.httpResponse.getBody(this.traderController.getAllTraders(sessionID));
}
/** Handle client/trading/api/getTrader */
public getTrader(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<ITraderBase>
{
public getTrader(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<ITraderBase> {
const traderID = url.replace("/client/trading/api/getTrader/", "");
return this.httpResponse.getBody(this.traderController.getTrader(sessionID, traderID));
}
/** Handle client/trading/api/getTraderAssort */
public getAssort(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<ITraderAssort>
{
public getAssort(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<ITraderAssort> {
const traderID = url.replace("/client/trading/api/getTraderAssort/", "");
return this.httpResponse.getBody(this.traderController.getAssort(sessionID, traderID));
}

View File

@ -1,31 +1,31 @@
import { inject, injectable } from "tsyringe";
import { WeatherController } from "@spt/controllers/WeatherController";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
import { IGetBodyResponseData } from "@spt/models/eft/httpResponse/IGetBodyResponseData";
import { IWeatherData } from "@spt/models/eft/weather/IWeatherData";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { IGetLocalWeatherResponseData } from "@spt/models/spt/weather/IGetLocalWeatherResponseData";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class WeatherCallbacks
{
export class WeatherCallbacks {
constructor(
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("WeatherController") protected weatherController: WeatherController,
)
{}
) {}
/**
* Handle client/weather
* @returns IWeatherData
*/
public getWeather(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<IWeatherData>
{
public getWeather(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<IWeatherData> {
return this.httpResponse.getBody(this.weatherController.generate());
}
public getLocalWeather(url: string, info: IEmptyRequestData, sessionID: string): IGetBodyResponseData<IGetLocalWeatherResponseData>
{
public getLocalWeather(
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<IGetLocalWeatherResponseData> {
return this.httpResponse.getBody(this.weatherController.generateLocal(sessionID));
}
}

View File

@ -1,24 +1,21 @@
import { inject, injectable } from "tsyringe";
import { WishlistController } from "@spt/controllers/WishlistController";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { IAddToWishlistRequest } from "@spt/models/eft/wishlist/IAddToWishlistRequest";
import { IChangeWishlistItemCategoryRequest } from "@spt/models/eft/wishlist/IChangeWishlistItemCategoryRequest";
import { IRemoveFromWishlistRequest } from "@spt/models/eft/wishlist/IRemoveFromWishlistRequest";
import { inject, injectable } from "tsyringe";
@injectable()
export class WishlistCallbacks
{
constructor(@inject("WishlistController") protected wishlistController: WishlistController)
{}
export class WishlistCallbacks {
constructor(@inject("WishlistController") protected wishlistController: WishlistController) {}
/** Handle AddToWishList event */
public addToWishlist(
pmcData: IPmcData,
request: IAddToWishlistRequest,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.wishlistController.addToWishList(pmcData, request, sessionID);
}
@ -27,8 +24,7 @@ export class WishlistCallbacks
pmcData: IPmcData,
request: IRemoveFromWishlistRequest,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.wishlistController.removeFromWishList(pmcData, request, sessionID);
}
@ -37,8 +33,7 @@ export class WishlistCallbacks
pmcData: IPmcData,
request: IChangeWishlistItemCategoryRequest,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
return this.wishlistController.changeWishlistItemCategory(pmcData, request, sessionID);
}
}

View File

@ -1,11 +1,10 @@
import { injectable } from "tsyringe";
import { ContextVariable } from "@spt/context/ContextVariable";
import { ContextVariableType } from "@spt/context/ContextVariableType";
import { LinkedList } from "@spt/utils/collections/lists/LinkedList";
import { injectable } from "tsyringe";
@injectable()
export class ApplicationContext
{
export class ApplicationContext {
private variables = new Map<ContextVariableType, LinkedList<ContextVariable>>();
private static holderMaxSize = 10;
@ -19,23 +18,18 @@ export class ApplicationContext
* const matchInfo = this.applicationContext.getLatestValue(ContextVariableType.RAID_CONFIGURATION).getValue<IGetRaidConfigurationRequestData>();
* ```
*/
public getLatestValue(type: ContextVariableType): ContextVariable | undefined
{
if (this.variables.has(type))
{
public getLatestValue(type: ContextVariableType): ContextVariable | undefined {
if (this.variables.has(type)) {
return this.variables.get(type)?.getTail();
}
return undefined;
}
public getValues(type: ContextVariableType): ContextVariable[] | undefined
{
if (this.variables.has(type))
{
public getValues(type: ContextVariableType): ContextVariable[] | undefined {
if (this.variables.has(type)) {
const res: ContextVariable[] = [];
for (const value of this.variables.get(type)!.values())
{
for (const value of this.variables.get(type)!.values()) {
res.push(value);
}
@ -44,20 +38,15 @@ export class ApplicationContext
return undefined;
}
public addValue(type: ContextVariableType, value: any): void
{
public addValue(type: ContextVariableType, value: any): void {
let list: LinkedList<ContextVariable>;
if (this.variables.has(type))
{
if (this.variables.has(type)) {
list = this.variables.get(type)!;
}
else
{
} else {
list = new LinkedList<ContextVariable>();
}
if (list.length >= ApplicationContext.holderMaxSize)
{
if (list.length >= ApplicationContext.holderMaxSize) {
list.shift();
}
@ -65,10 +54,8 @@ export class ApplicationContext
this.variables.set(type, list);
}
public clearValues(type: ContextVariableType): void
{
if (this.variables.has(type))
{
public clearValues(type: ContextVariableType): void {
if (this.variables.has(type)) {
this.variables.delete(type);
}
}

View File

@ -1,30 +1,25 @@
import { ContextVariableType } from "@spt/context/ContextVariableType";
export class ContextVariable
{
export class ContextVariable {
private value: any;
private timestamp: Date;
private type: ContextVariableType;
constructor(value: any, type: ContextVariableType)
{
constructor(value: any, type: ContextVariableType) {
this.value = value;
this.timestamp = new Date();
this.type = type;
}
public getValue<T>(): T
{
public getValue<T>(): T {
return this.value;
}
public getTimestamp(): Date
{
public getTimestamp(): Date {
return this.timestamp;
}
public getType(): ContextVariableType
{
public getType(): ContextVariableType {
return this.type;
}
}

View File

@ -1,5 +1,4 @@
export enum ContextVariableType
{
export enum ContextVariableType {
/** Logged in users session id */
SESSION_ID = 0,
/** Currently acive raid information */

View File

@ -1,27 +1,24 @@
import { inject, injectable } from "tsyringe";
import { ICompletedAchievementsResponse } from "@spt/models/eft/profile/ICompletedAchievementsResponse";
import { IGetAchievementsResponse } from "@spt/models/eft/profile/IGetAchievementsResponse";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { DatabaseService } from "@spt/services/DatabaseService";
import { inject, injectable } from "tsyringe";
/**
* Logic for handling In Raid callbacks
*/
@injectable()
export class AchievementController
{
export class AchievementController {
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("DatabaseService") protected databaseService: DatabaseService,
)
{}
) {}
/**
* Get base achievements
* @param sessionID Session id
*/
public getAchievements(sessionID: string): IGetAchievementsResponse
{
public getAchievements(sessionID: string): IGetAchievementsResponse {
return { elements: this.databaseService.getAchievements() };
}
@ -30,13 +27,11 @@ export class AchievementController
* @param sessionId Session id
* @returns ICompletedAchievementsResponse
*/
public getAchievementStatistics(sessionId: string): ICompletedAchievementsResponse
{
public getAchievementStatistics(sessionId: string): ICompletedAchievementsResponse {
const achievements = this.databaseService.getAchievements();
const stats = {};
for (const achievement of achievements)
{
for (const achievement of achievements) {
stats[achievement.id] = 0;
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { ApplicationContext } from "@spt/context/ApplicationContext";
import { ContextVariableType } from "@spt/context/ContextVariableType";
import { BotGenerator } from "@spt/generators/BotGenerator";
@ -25,12 +24,12 @@ import { DatabaseService } from "@spt/services/DatabaseService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { MatchBotDetailsCacheService } from "@spt/services/MatchBotDetailsCacheService";
import { SeasonalEventService } from "@spt/services/SeasonalEventService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class BotController
{
export class BotController {
protected botConfig: IBotConfig;
protected pmcConfig: IPmcConfig;
@ -50,8 +49,7 @@ export class BotController
@inject("ApplicationContext") protected applicationContext: ApplicationContext,
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
) {
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
}
@ -61,12 +59,10 @@ export class BotController
* @param type bot Type we want the load-out gen count for
* @returns number of bots to generate
*/
public getBotPresetGenerationLimit(type: string): number
{
public getBotPresetGenerationLimit(type: string): number {
const value = this.botConfig.presetBatch[type === "assaultGroup" ? "assault" : type];
if (!value)
{
if (!value) {
this.logger.warning(this.localisationService.getText("bot-bot_preset_count_value_missing", type));
return 30;
@ -80,8 +76,7 @@ export class BotController
* Get the core.json difficulty settings from database/bots
* @returns IBotCore
*/
public getBotCoreDifficulty(): IBotCore
{
public getBotCoreDifficulty(): IBotCore {
return this.databaseService.getBots().core;
}
@ -93,15 +88,13 @@ export class BotController
* @param ignoreRaidSettings should raid settings chosen pre-raid be ignored
* @returns Difficulty object
*/
public getBotDifficulty(type: string, diffLevel: string, ignoreRaidSettings = false): Difficulty
{
public getBotDifficulty(type: string, diffLevel: string, ignoreRaidSettings = false): Difficulty {
let difficulty = diffLevel.toLowerCase();
const raidConfig = this.applicationContext
.getLatestValue(ContextVariableType.RAID_CONFIGURATION)
?.getValue<IGetRaidConfigurationRequestData>();
if (!(raidConfig || ignoreRaidSettings))
{
if (!(raidConfig || ignoreRaidSettings)) {
this.logger.error(
this.localisationService.getText("bot-missing_application_context", "RAID_CONFIGURATION"),
);
@ -110,16 +103,14 @@ export class BotController
// Check value chosen in pre-raid difficulty dropdown
// If value is not 'asonline', change requested difficulty to be what was chosen in dropdown
const botDifficultyDropDownValue = raidConfig?.wavesSettings.botDifficulty.toLowerCase() ?? "asonline";
if (botDifficultyDropDownValue !== "asonline")
{
difficulty
= this.botDifficultyHelper.convertBotDifficultyDropdownToBotDifficulty(botDifficultyDropDownValue);
if (botDifficultyDropDownValue !== "asonline") {
difficulty =
this.botDifficultyHelper.convertBotDifficultyDropdownToBotDifficulty(botDifficultyDropDownValue);
}
let difficultySettings: Difficulty;
const lowercasedBotType = type.toLowerCase();
switch (lowercasedBotType)
{
switch (lowercasedBotType) {
case this.pmcConfig.bearType.toLowerCase():
difficultySettings = this.botDifficultyHelper.getPmcDifficultySettings(
"bear",
@ -144,14 +135,12 @@ export class BotController
return difficultySettings;
}
public getAllBotDifficulties(): Record<string, any>
{
public getAllBotDifficulties(): Record<string, any> {
const result = {};
const botTypesDb = this.databaseService.getBots().types;
const botTypes = Object.keys(WildSpawnTypeNumber).filter((v) => Number.isNaN(Number(v)));
for (let botType of botTypes)
{
for (let botType of botTypes) {
const enumType = botType.toLowerCase();
// pmcBEAR/pmcUSEC need to be converted into `usec`/`bear` so we can read difficulty settings from bots/types
botType = this.botHelper.isBotPmc(botType)
@ -159,15 +148,13 @@ export class BotController
: botType.toLowerCase();
const botDetails = botTypesDb[botType];
if (!botDetails?.difficulty)
{
if (!botDetails?.difficulty) {
continue;
}
const botDifficulties = Object.keys(botDetails.difficulty);
result[enumType] = {};
for (const difficulty of botDifficulties)
{
for (const difficulty of botDifficulties) {
result[enumType][difficulty] = this.getBotDifficulty(enumType, difficulty, true);
}
}
@ -181,14 +168,12 @@ export class BotController
* @param info bot generation request info
* @returns IBotBase array
*/
public async generate(sessionId: string, info: IGenerateBotsRequestData): Promise<IBotBase[]>
{
public async generate(sessionId: string, info: IGenerateBotsRequestData): Promise<IBotBase[]> {
const pmcProfile = this.profileHelper.getPmcProfile(sessionId);
// Use this opportunity to create and cache bots for later retreval
const multipleBotTypesRequested = info.conditions.length > 1;
if (multipleBotTypesRequested)
{
if (multipleBotTypesRequested) {
return this.generateMultipleBotsAndCache(info, pmcProfile, sessionId);
}
@ -206,26 +191,23 @@ export class BotController
request: IGenerateBotsRequestData,
pmcProfile: IPmcData,
sessionId: string,
): Promise<IBotBase[]>
{
): Promise<IBotBase[]> {
const raidSettings = this.applicationContext
.getLatestValue(ContextVariableType.RAID_CONFIGURATION)
?.getValue<IGetRaidConfigurationRequestData>();
if (raidSettings === undefined)
{
if (raidSettings === undefined) {
// throw new Error(this.localisationService.getText("bot-unable_to_load_raid_settings_from_appcontext"));
}
const pmcLevelRangeForMap
= this.pmcConfig.locationSpecificPmcLevelOverride[raidSettings?.location.toLowerCase()];
const pmcLevelRangeForMap =
this.pmcConfig.locationSpecificPmcLevelOverride[raidSettings?.location.toLowerCase()];
const allPmcsHaveSameNameAsPlayer = this.randomUtil.getChance100(
this.pmcConfig.allPMCsHavePlayerNameWithRandomPrefixChance,
);
const conditionPromises: Promise<void>[] = [];
for (const condition of request.conditions)
{
for (const condition of request.conditions) {
const botGenerationDetails = this.getBotGenerationDetailsForWave(
condition,
pmcProfile,
@ -260,8 +242,7 @@ export class BotController
pmcLevelRangeForMap: MinMax,
botCountToGenerate: number,
generateAsPmc: boolean,
): BotGenerationDetails
{
): BotGenerationDetails {
return {
isPmc: generateAsPmc,
side: "Savage",
@ -283,8 +264,7 @@ export class BotController
* @param pmcProfile Profile to get level from
* @returns Level as number
*/
protected getPlayerLevelFromProfile(pmcProfile: IPmcData): number
{
protected getPlayerLevelFromProfile(pmcProfile: IPmcData): number {
return pmcProfile.Info.Level;
}
@ -299,11 +279,9 @@ export class BotController
condition: Condition,
botGenerationDetails: BotGenerationDetails,
sessionId: string,
): Promise<void>
{
): Promise<void> {
const isEventBot = condition.Role.toLowerCase().includes("event");
if (isEventBot)
{
if (isEventBot) {
// Add eventRole data + reassign role property to be base type
botGenerationDetails.eventRole = condition.Role;
botGenerationDetails.role = this.seasonalEventService.getBaseRoleForEventBot(
@ -313,8 +291,7 @@ export class BotController
// Custom map waves can have spt roles in them
// Is bot type pmcUSEC/pmcBEAR, set is pmc true and set side
if (this.botHelper.botRoleIsPmc(condition.Role))
{
if (this.botHelper.botRoleIsPmc(condition.Role)) {
botGenerationDetails.isPmc = true;
botGenerationDetails.side = this.botHelper.getPmcSideByRole(condition.Role);
}
@ -328,20 +305,17 @@ export class BotController
// Get number of bots we have in cache
const botCacheCount = this.botGenerationCacheService.getCachedBotCount(cacheKey);
const botPromises: Promise<void>[] = [];
if (botCacheCount > botGenerationDetails.botCountToGenerate)
{
if (botCacheCount > botGenerationDetails.botCountToGenerate) {
return;
}
// We're below desired count, add bots to cache
for (let i = 0; i < botGenerationDetails.botCountToGenerate; i++)
{
for (let i = 0; i < botGenerationDetails.botCountToGenerate; i++) {
const detailsClone = this.cloner.clone(botGenerationDetails);
botPromises.push(this.generateSingleBotAndStoreInCache(detailsClone, sessionId, cacheKey));
}
return Promise.all(botPromises).then(() =>
{
return Promise.all(botPromises).then(() => {
this.logger.debug(
`Generated ${botGenerationDetails.botCountToGenerate} ${botGenerationDetails.role} (${
botGenerationDetails.eventRole ?? ""
@ -361,8 +335,7 @@ export class BotController
botGenerationDetails: BotGenerationDetails,
sessionId: string,
cacheKey: string,
): Promise<void>
{
): Promise<void> {
const botToCache = this.botGenerator.prepareAndGenerateBot(sessionId, botGenerationDetails);
this.botGenerationCacheService.storeBots(cacheKey, [botToCache]);
@ -379,8 +352,7 @@ export class BotController
protected async returnSingleBotFromCache(
sessionId: string,
request: IGenerateBotsRequestData,
): Promise<IBotBase[]>
{
): Promise<IBotBase[]> {
const pmcProfile = this.profileHelper.getPmcProfile(sessionId);
const requestedBot = request.conditions[0];
@ -388,12 +360,11 @@ export class BotController
.getLatestValue(ContextVariableType.RAID_CONFIGURATION)
?.getValue<IGetRaidConfigurationRequestData>();
if (raidSettings === undefined)
{
if (raidSettings === undefined) {
throw new Error(this.localisationService.getText("bot-unable_to_load_raid_settings_from_appcontext"));
}
const pmcLevelRangeForMap
= this.pmcConfig.locationSpecificPmcLevelOverride[raidSettings.location.toLowerCase()];
const pmcLevelRangeForMap =
this.pmcConfig.locationSpecificPmcLevelOverride[raidSettings.location.toLowerCase()];
// Create gen request for when cache is empty
const condition: Condition = {
@ -412,8 +383,7 @@ export class BotController
// Event bots need special actions to occur, set data up for them
const isEventBot = requestedBot.Role.toLowerCase().includes("event");
if (isEventBot)
{
if (isEventBot) {
// Add eventRole data + reassign role property
botGenerationDetails.eventRole = requestedBot.Role;
botGenerationDetails.role = this.seasonalEventService.getBaseRoleForEventBot(
@ -421,20 +391,17 @@ export class BotController
);
}
if (this.botHelper.isBotPmc(botGenerationDetails.role))
{
if (this.botHelper.isBotPmc(botGenerationDetails.role)) {
botGenerationDetails.isPmc = true;
botGenerationDetails.side = this.botHelper.getPmcSideByRole(requestedBot.Role);
}
// Roll chance to be pmc if type is allowed to be one
const botConvertRateMinMax = this.pmcConfig.convertIntoPmcChance[requestedBot.Role.toLowerCase()];
if (botConvertRateMinMax)
{
if (botConvertRateMinMax) {
// Should bot become PMC
const convertToPmc = this.botHelper.rollChanceToBePmc(requestedBot.Role, botConvertRateMinMax);
if (convertToPmc)
{
if (convertToPmc) {
botGenerationDetails.isPmc = true;
botGenerationDetails.role = this.botHelper.getRandomizedPmcRole();
botGenerationDetails.side = this.botHelper.getPmcSideByRole(botGenerationDetails.role);
@ -443,22 +410,16 @@ export class BotController
}
}
// Only convert to boss when not already converted to PMC & Boss Convert is enabled
const {
bossConvertEnabled,
bossConvertMinMax,
bossesToConvertToWeights } = this.botConfig.assaultToBossConversion;
if (bossConvertEnabled && !botGenerationDetails.isPmc)
{
const { bossConvertEnabled, bossConvertMinMax, bossesToConvertToWeights } =
this.botConfig.assaultToBossConversion;
if (bossConvertEnabled && !botGenerationDetails.isPmc) {
const bossConvertPercent = bossConvertMinMax[requestedBot.Role.toLowerCase()];
if (bossConvertPercent)
{
if (bossConvertPercent) {
// Roll a percentage check if we should convert scav to boss
if (this.randomUtil.getChance100(
this.randomUtil.getInt(bossConvertPercent.min, bossConvertPercent.max)))
{
this.updateBotGenerationDetailsToRandomBoss(
botGenerationDetails,
bossesToConvertToWeights);
if (
this.randomUtil.getChance100(this.randomUtil.getInt(bossConvertPercent.min, bossConvertPercent.max))
) {
this.updateBotGenerationDetailsToRandomBoss(botGenerationDetails, bossesToConvertToWeights);
}
}
}
@ -466,20 +427,18 @@ export class BotController
// Create a compound key to store bots in cache against
const cacheKey = this.botGenerationCacheService.createCacheKey(
botGenerationDetails.eventRole ?? botGenerationDetails.role,
botGenerationDetails.botDifficulty);
botGenerationDetails.botDifficulty,
);
// Check cache for bot using above key
if (!this.botGenerationCacheService.cacheHasBotOfRole(cacheKey))
{
if (!this.botGenerationCacheService.cacheHasBotOfRole(cacheKey)) {
const botPromises: Promise<void>[] = [];
// No bot in cache, generate new and return one
for (let i = 0; i < botGenerationDetails.botCountToGenerate; i++)
{
for (let i = 0; i < botGenerationDetails.botCountToGenerate; i++) {
botPromises.push(this.generateSingleBotAndStoreInCache(botGenerationDetails, sessionId, cacheKey));
}
await Promise.all(botPromises).then(() =>
{
await Promise.all(botPromises).then(() => {
this.logger.debug(
`Generated ${botGenerationDetails.botCountToGenerate} ${botGenerationDetails.role} (${
botGenerationDetails.eventRole ?? ""
@ -496,11 +455,10 @@ export class BotController
protected updateBotGenerationDetailsToRandomBoss(
botGenerationDetails: BotGenerationDetails,
possibleBossTypeWeights: Record<string, number>): void
{
possibleBossTypeWeights: Record<string, number>,
): void {
// Seems Actual bosses have the same Brain issues like PMC gaining Boss Brains We cant use all bosses
botGenerationDetails.role
= this.weightedRandomHelper.getWeightedValue(possibleBossTypeWeights);
botGenerationDetails.role = this.weightedRandomHelper.getWeightedValue(possibleBossTypeWeights);
// Bosses are only ever 'normal'
botGenerationDetails.botDifficulty = "normal";
@ -512,16 +470,13 @@ export class BotController
* @param requestedDifficulty
* @returns
*/
public getPMCDifficulty(requestedDifficulty: string): string
{
public getPMCDifficulty(requestedDifficulty: string): string {
// Maybe return a random difficulty...
if (this.pmcConfig.difficulty.toLowerCase() === "asonline")
{
if (this.pmcConfig.difficulty.toLowerCase() === "asonline") {
return requestedDifficulty;
}
if (this.pmcConfig.difficulty.toLowerCase() === "random")
{
if (this.pmcConfig.difficulty.toLowerCase() === "random") {
return this.botDifficultyHelper.chooseRandomDifficulty();
}
@ -534,24 +489,18 @@ export class BotController
* @param location The map location cap was requested for
* @returns cap number
*/
public getBotCap(location: string): number
{
public getBotCap(location: string): number {
const botCap = this.botConfig.maxBotCap[location.toLowerCase()];
if (location === "default")
{
if (location === "default") {
this.logger.warning(
this.localisationService.getText(
"bot-no_bot_cap_found_for_location",
location.toLowerCase(),
),
this.localisationService.getText("bot-no_bot_cap_found_for_location", location.toLowerCase()),
);
}
return botCap;
}
public getAiBotBrainTypes(): any
{
public getAiBotBrainTypes(): any {
return {
pmc: this.pmcConfig.pmcType,
assault: this.botConfig.assaultBrainType,

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { ISetMagazineRequest } from "@spt/models/eft/builds/ISetMagazineRequest";
@ -11,12 +10,12 @@ import { EventOutputHolder } from "@spt/routers/EventOutputHolder";
import { SaveServer } from "@spt/servers/SaveServer";
import { DatabaseService } from "@spt/services/DatabaseService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HashUtil } from "@spt/utils/HashUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class BuildController
{
export class BuildController {
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("HashUtil") protected hashUtil: HashUtil,
@ -27,16 +26,13 @@ export class BuildController
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("SaveServer") protected saveServer: SaveServer,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{}
) {}
/** Handle client/handbook/builds/my/list */
public getUserBuilds(sessionID: string): IUserBuilds
{
public getUserBuilds(sessionID: string): IUserBuilds {
const secureContainerSlotId = "SecuredContainer";
const profile = this.saveServer.getProfile(sessionID);
if (!profile.userbuilds)
{
if (!profile.userbuilds) {
profile.userbuilds = { equipmentBuilds: [], weaponBuilds: [], magazineBuilds: [] };
}
@ -50,15 +46,12 @@ export class BuildController
const firstDefaultItemsSecureContainer = defaultEquipmentPresetsClone[0]?.Items?.find(
(x) => x.slotId === secureContainerSlotId,
);
if (playerSecureContainer && playerSecureContainer?._tpl !== firstDefaultItemsSecureContainer?._tpl)
{
if (playerSecureContainer && playerSecureContainer?._tpl !== firstDefaultItemsSecureContainer?._tpl) {
// Default equipment presets' secure container tpl doesn't match players secure container tpl
for (const defaultPreset of defaultEquipmentPresetsClone)
{
for (const defaultPreset of defaultEquipmentPresetsClone) {
// Find presets secure container
const secureContainer = defaultPreset.Items.find((item) => item.slotId === secureContainerSlotId);
if (secureContainer)
{
if (secureContainer) {
secureContainer._tpl = playerSecureContainer._tpl;
}
}
@ -72,8 +65,7 @@ export class BuildController
}
/** Handle client/builds/weapon/save */
public saveWeaponBuild(sessionId: string, body: IPresetBuildActionRequestData): void
{
public saveWeaponBuild(sessionId: string, body: IPresetBuildActionRequestData): void {
const pmcData = this.profileHelper.getPmcProfile(sessionId);
// Replace duplicate Id's. The first item is the base item.
@ -86,28 +78,24 @@ export class BuildController
const savedWeaponBuilds = this.saveServer.getProfile(sessionId).userbuilds.weaponBuilds;
const existingBuild = savedWeaponBuilds.find((x) => x.Id === body.Id);
if (existingBuild)
{
if (existingBuild) {
// exists, replace
this.saveServer
.getProfile(sessionId)
.userbuilds.weaponBuilds.splice(savedWeaponBuilds.indexOf(existingBuild), 1, newBuild);
}
else
{
} else {
// Add fresh
this.saveServer.getProfile(sessionId).userbuilds.weaponBuilds.push(newBuild);
}
}
/** Handle client/builds/equipment/save event */
public saveEquipmentBuild(sessionID: string, request: IPresetBuildActionRequestData): void
{
public saveEquipmentBuild(sessionID: string, request: IPresetBuildActionRequestData): void {
const buildType = "equipmentBuilds";
const pmcData = this.profileHelper.getPmcProfile(sessionID);
const existingSavedEquipmentBuilds: IEquipmentBuild[]
= this.saveServer.getProfile(sessionID).userbuilds[buildType];
const existingSavedEquipmentBuilds: IEquipmentBuild[] =
this.saveServer.getProfile(sessionID).userbuilds[buildType];
// Replace duplicate ID's. The first item is the base item.
// Root ID and the base item ID need to match.
@ -124,28 +112,23 @@ export class BuildController
const existingBuild = existingSavedEquipmentBuilds.find(
(build) => build.Name === request.Name || build.Id === request.Id,
);
if (existingBuild)
{
if (existingBuild) {
// Already exists, replace
this.saveServer
.getProfile(sessionID)
.userbuilds[buildType].splice(existingSavedEquipmentBuilds.indexOf(existingBuild), 1, newBuild);
}
else
{
} else {
// Fresh, add new
this.saveServer.getProfile(sessionID).userbuilds[buildType].push(newBuild);
}
}
/** Handle client/builds/delete */
public removeBuild(sessionID: string, request: IRemoveBuildRequestData): void
{
public removeBuild(sessionID: string, request: IRemoveBuildRequestData): void {
this.removePlayerBuild(request.id, sessionID);
}
protected removePlayerBuild(idToRemove: string, sessionID: string): void
{
protected removePlayerBuild(idToRemove: string, sessionID: string): void {
const profile = this.saveServer.getProfile(sessionID);
const weaponBuilds = profile.userbuilds.weaponBuilds;
const equipmentBuilds = profile.userbuilds.equipmentBuilds;
@ -153,8 +136,7 @@ export class BuildController
// Check for id in weapon array first
const matchingWeaponBuild = weaponBuilds.find((weaponBuild) => weaponBuild.Id === idToRemove);
if (matchingWeaponBuild)
{
if (matchingWeaponBuild) {
weaponBuilds.splice(weaponBuilds.indexOf(matchingWeaponBuild), 1);
return;
@ -162,8 +144,7 @@ export class BuildController
// Id not found in weapons, try equipment
const matchingEquipmentBuild = equipmentBuilds.find((equipmentBuild) => equipmentBuild.Id === idToRemove);
if (matchingEquipmentBuild)
{
if (matchingEquipmentBuild) {
equipmentBuilds.splice(equipmentBuilds.indexOf(matchingEquipmentBuild), 1);
return;
@ -171,8 +152,7 @@ export class BuildController
// Id not found in weapons/equipment, try mags
const matchingMagazineBuild = magazineBuilds.find((magBuild) => magBuild.Id === idToRemove);
if (matchingMagazineBuild)
{
if (matchingMagazineBuild) {
magazineBuilds.splice(magazineBuilds.indexOf(matchingMagazineBuild), 1);
return;
@ -185,8 +165,7 @@ export class BuildController
/**
* Handle client/builds/magazine/save
*/
public createMagazineTemplate(sessionId: string, request: ISetMagazineRequest): void
{
public createMagazineTemplate(sessionId: string, request: ISetMagazineRequest): void {
const result: IMagazineBuild = {
Id: request.Id,
Name: request.Name,
@ -201,12 +180,9 @@ export class BuildController
profile.userbuilds.magazineBuilds ||= [];
const existingArrayId = profile.userbuilds.magazineBuilds.findIndex((item) => item.Name === request.Name);
if (existingArrayId === -1)
{
if (existingArrayId === -1) {
profile.userbuilds.magazineBuilds.push(result);
}
else
{
} else {
profile.userbuilds.magazineBuilds.splice(existingArrayId, 1, result);
}
}

View File

@ -1,21 +1,18 @@
import { inject, injectable } from "tsyringe";
import { IClientLogRequest } from "@spt/models/spt/logging/IClientLogRequest";
import { LogBackgroundColor } from "@spt/models/spt/logging/LogBackgroundColor";
import { LogLevel } from "@spt/models/spt/logging/LogLevel";
import { LogTextColor } from "@spt/models/spt/logging/LogTextColor";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { inject, injectable } from "tsyringe";
@injectable()
export class ClientLogController
{
constructor(@inject("PrimaryLogger") protected logger: ILogger)
{}
export class ClientLogController {
constructor(@inject("PrimaryLogger") protected logger: ILogger) {}
/**
* Handle /singleplayer/log
*/
public clientLog(logRequest: IClientLogRequest): void
{
public clientLog(logRequest: IClientLogRequest): void {
const message = `[${logRequest.Source}] ${logRequest.Message}`;
const color = logRequest.Color ?? LogTextColor.WHITE;
const backgroundColor = logRequest.BackgroundColor ?? LogBackgroundColor.DEFAULT;
@ -23,13 +20,11 @@ export class ClientLogController
// Allow supporting either string or enum levels
// Required due to the C# modules serializing enums as their name
let level = logRequest.Level;
if (typeof level === "string")
{
if (typeof level === "string") {
level = LogLevel[level.toUpperCase() as keyof typeof LogLevel];
}
switch (level)
{
switch (level) {
case LogLevel.ERROR:
this.logger.error(message);
break;

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { ISuit } from "@spt/models/eft/common/tables/ITrader";
@ -10,10 +9,10 @@ import { EventOutputHolder } from "@spt/routers/EventOutputHolder";
import { SaveServer } from "@spt/servers/SaveServer";
import { DatabaseService } from "@spt/services/DatabaseService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { inject, injectable } from "tsyringe";
@injectable()
export class CustomizationController
{
export class CustomizationController {
protected readonly clothingIds = {
lowerParentId: "5cd944d01388ce000a659df9",
upperParentId: "5cd944ca1388ce03a44dc2a4",
@ -26,8 +25,7 @@ export class CustomizationController
@inject("SaveServer") protected saveServer: SaveServer,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
)
{}
) {}
/**
* Get purchasable clothing items from trader that match players side (usec/bear)
@ -35,8 +33,7 @@ export class CustomizationController
* @param sessionID Session id
* @returns ISuit array
*/
public getTraderSuits(traderID: string, sessionID: string): ISuit[]
{
public getTraderSuits(traderID: string, sessionID: string): ISuit[] {
const pmcData = this.profileHelper.getPmcProfile(sessionID);
const clothing = this.databaseService.getCustomization();
const suits = this.databaseService.getTrader(traderID).suits;
@ -45,7 +42,9 @@ export class CustomizationController
const matchingSuits = suits?.filter((suit) => suit.suiteId in clothing);
// Return all suits that have a side array containing the players side (usec/bear)
const matchedSuits = matchingSuits?.filter((matchingSuit) => clothing[matchingSuit.suiteId]._props.Side.includes(pmcData.Info.Side));
const matchedSuits = matchingSuits?.filter((matchingSuit) =>
clothing[matchingSuit.suiteId]._props.Side.includes(pmcData.Info.Side),
);
if (matchingSuits === undefined)
throw new Error(this.localisationService.getText("customisation-unable_to_get_trader_suits", traderID));
@ -60,22 +59,18 @@ export class CustomizationController
pmcData: IPmcData,
wearClothingRequest: IWearClothingRequestData,
sessionID: string,
): IItemEventRouterResponse
{
for (const suitId of wearClothingRequest.suites)
{
): IItemEventRouterResponse {
for (const suitId of wearClothingRequest.suites) {
// Find desired clothing item in db
const dbSuit = this.databaseService.getCustomization()[suitId];
// Legs
if (dbSuit._parent === this.clothingIds.lowerParentId)
{
if (dbSuit._parent === this.clothingIds.lowerParentId) {
pmcData.Customization.Feet = dbSuit._props.Feet;
}
// Torso
if (dbSuit._parent === this.clothingIds.upperParentId)
{
if (dbSuit._parent === this.clothingIds.upperParentId) {
pmcData.Customization.Body = dbSuit._props.Body;
pmcData.Customization.Hands = dbSuit._props.Hands;
}
@ -96,13 +91,11 @@ export class CustomizationController
pmcData: IPmcData,
buyClothingRequest: IBuyClothingRequestData,
sessionId: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionId);
const traderOffer = this.getTraderClothingOffer(sessionId, buyClothingRequest.offer);
if (!traderOffer)
{
if (!traderOffer) {
this.logger.error(
this.localisationService.getText("customisation-unable_to_find_suit_by_id", buyClothingRequest.offer),
);
@ -111,8 +104,7 @@ export class CustomizationController
}
const suitId = traderOffer.suiteId;
if (this.outfitAlreadyPurchased(suitId, sessionId))
{
if (this.outfitAlreadyPurchased(suitId, sessionId)) {
const suitDetails = this.databaseService.getCustomization()[suitId];
this.logger.error(
this.localisationService.getText("customisation-item_already_purchased", {
@ -133,11 +125,9 @@ export class CustomizationController
return output;
}
protected getTraderClothingOffer(sessionId: string, offerId: string): ISuit
{
protected getTraderClothingOffer(sessionId: string, offerId: string): ISuit {
const foundSuit = this.getAllTraderSuits(sessionId).find((x) => x._id === offerId);
if (foundSuit === undefined)
{
if (foundSuit === undefined) {
throw new Error(this.localisationService.getText("customisation-unable_to_find_suit_with_id", offerId));
}
@ -150,8 +140,7 @@ export class CustomizationController
* @param sessionID Session id of profile to check for clothing in
* @returns true if already purchased
*/
protected outfitAlreadyPurchased(suitId: string, sessionID: string): boolean
{
protected outfitAlreadyPurchased(suitId: string, sessionID: string): boolean {
return this.saveServer.getProfile(sessionID).suits.includes(suitId);
}
@ -167,10 +156,8 @@ export class CustomizationController
pmcData: IPmcData,
clothingItems: ClothingItem[],
output: IItemEventRouterResponse,
): void
{
for (const sellItem of clothingItems)
{
): void {
for (const sellItem of clothingItems) {
this.payForClothingItem(sessionId, pmcData, sellItem, output);
}
}
@ -187,11 +174,9 @@ export class CustomizationController
pmcData: IPmcData,
clothingItem: ClothingItem,
output: IItemEventRouterResponse,
): void
{
): void {
const relatedItem = pmcData.Inventory.items.find((x) => x._id === clothingItem.id);
if (!relatedItem)
{
if (!relatedItem) {
this.logger.error(
this.localisationService.getText(
"customisation-unable_to_find_clothing_item_in_inventory",
@ -202,19 +187,18 @@ export class CustomizationController
return;
}
if (clothingItem.del === true)
{
if (clothingItem.del === true) {
output.profileChanges[sessionId].items.del.push(relatedItem);
pmcData.Inventory.items.splice(pmcData.Inventory.items.indexOf(relatedItem), 1);
}
if (!relatedItem.upd || !relatedItem.upd.StackObjectsCount)
{
throw new Error(this.localisationService.getText("customisation-suit_lacks_upd_or_stack_property", relatedItem._tpl));
if (!relatedItem.upd || !relatedItem.upd.StackObjectsCount) {
throw new Error(
this.localisationService.getText("customisation-suit_lacks_upd_or_stack_property", relatedItem._tpl),
);
}
if (relatedItem.upd.StackObjectsCount > clothingItem.count)
{
if (relatedItem.upd.StackObjectsCount > clothingItem.count) {
relatedItem.upd.StackObjectsCount -= clothingItem.count;
output.profileChanges[sessionId].items.change.push({
_id: relatedItem._id,
@ -227,15 +211,12 @@ export class CustomizationController
}
}
protected getAllTraderSuits(sessionID: string): ISuit[]
{
protected getAllTraderSuits(sessionID: string): ISuit[] {
const traders = this.databaseService.getTraders();
let result: ISuit[] = [];
for (const traderID in traders)
{
if (traders[traderID].base.customization_seller === true)
{
for (const traderID in traders) {
if (traders[traderID].base.customization_seller === true) {
result = [...result, ...this.getTraderSuits(traderID, sessionID)];
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectAll, injectable } from "tsyringe";
import { IDialogueChatBot } from "@spt/helpers/Dialogue/IDialogueChatBot";
import { DialogueHelper } from "@spt/helpers/DialogueHelper";
import { IFriendRequestData } from "@spt/models/eft/dialog/IFriendRequestData";
@ -18,10 +17,10 @@ import { SaveServer } from "@spt/servers/SaveServer";
import { LocalisationService } from "@spt/services/LocalisationService";
import { MailSendService } from "@spt/services/MailSendService";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { inject, injectAll, injectable } from "tsyringe";
@injectable()
export class DialogueController
{
export class DialogueController {
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("SaveServer") protected saveServer: SaveServer,
@ -31,37 +30,36 @@ export class DialogueController
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer,
@injectAll("DialogueChatBot") protected dialogueChatBots: IDialogueChatBot[],
)
{
) {
const coreConfigs = this.configServer.getConfig<ICoreConfig>(ConfigTypes.CORE);
// if give command is disabled or commando commands are disabled
if (!coreConfigs.features?.chatbotFeatures?.commandoEnabled)
{
const sptCommando = this.dialogueChatBots.find((c) => c.getChatBot()._id.toLocaleLowerCase() === "sptcommando")!;
if (!coreConfigs.features?.chatbotFeatures?.commandoEnabled) {
const sptCommando = this.dialogueChatBots.find(
(c) => c.getChatBot()._id.toLocaleLowerCase() === "sptcommando",
)!;
this.dialogueChatBots.splice(this.dialogueChatBots.indexOf(sptCommando), 1);
}
if (!coreConfigs.features?.chatbotFeatures?.sptFriendEnabled)
{
const sptFriend = this.dialogueChatBots.find((c) => c.getChatBot()._id.toLocaleLowerCase() === "sptFriend")!;
if (!coreConfigs.features?.chatbotFeatures?.sptFriendEnabled) {
const sptFriend = this.dialogueChatBots.find(
(c) => c.getChatBot()._id.toLocaleLowerCase() === "sptFriend",
)!;
this.dialogueChatBots.splice(this.dialogueChatBots.indexOf(sptFriend), 1);
}
}
public registerChatBot(chatBot: IDialogueChatBot): void
{
if (this.dialogueChatBots.some((cb) => cb.getChatBot()._id === chatBot.getChatBot()._id))
{
throw new Error(this.localisationService.getText("dialog-chatbot_id_already_exists", chatBot.getChatBot()._id));
public registerChatBot(chatBot: IDialogueChatBot): void {
if (this.dialogueChatBots.some((cb) => cb.getChatBot()._id === chatBot.getChatBot()._id)) {
throw new Error(
this.localisationService.getText("dialog-chatbot_id_already_exists", chatBot.getChatBot()._id),
);
}
this.dialogueChatBots.push(chatBot);
}
/** Handle onUpdate spt event */
public update(): void
{
public update(): void {
const profiles = this.saveServer.getProfiles();
for (const sessionID in profiles)
{
for (const sessionID in profiles) {
this.removeExpiredItemsFromMessages(sessionID);
}
}
@ -70,8 +68,7 @@ export class DialogueController
* Handle client/friend/list
* @returns IGetFriendListDataResponse
*/
public getFriendList(sessionID: string): IGetFriendListDataResponse
{
public getFriendList(sessionID: string): IGetFriendListDataResponse {
// Force a fake friend called SPT into friend list
return { Friends: this.dialogueChatBots.map((v) => v.getChatBot()), Ignore: [], InIgnoreList: [] };
}
@ -83,11 +80,9 @@ export class DialogueController
* @param sessionID Session Id
* @returns array of dialogs
*/
public generateDialogueList(sessionID: string): DialogueInfo[]
{
public generateDialogueList(sessionID: string): DialogueInfo[] {
const data: DialogueInfo[] = [];
for (const dialogueId in this.dialogueHelper.getDialogsForProfile(sessionID))
{
for (const dialogueId in this.dialogueHelper.getDialogsForProfile(sessionID)) {
data.push(this.getDialogueInfo(dialogueId, sessionID));
}
@ -100,8 +95,7 @@ export class DialogueController
* @param sessionID Session Id
* @returns DialogueInfo
*/
public getDialogueInfo(dialogueID: string, sessionID: string): DialogueInfo
{
public getDialogueInfo(dialogueID: string, sessionID: string): DialogueInfo {
const dialogs = this.dialogueHelper.getDialogsForProfile(sessionID);
const dialogue = dialogs[dialogueID];
@ -125,18 +119,19 @@ export class DialogueController
* @param sessionID Player id
* @returns IUserDialogInfo array
*/
public getDialogueUsers(dialog: Dialogue, messageType: MessageType, sessionID: string): IUserDialogInfo[] | undefined
{
public getDialogueUsers(
dialog: Dialogue,
messageType: MessageType,
sessionID: string,
): IUserDialogInfo[] | undefined {
const profile = this.saveServer.getProfile(sessionID);
// User to user messages are special in that they need the player to exist in them, add if they don't
if (
messageType === MessageType.USER_MESSAGE
&& !dialog.Users?.some((userDialog) => userDialog._id === profile.characters.pmc.sessionId)
)
{
if (!dialog.Users)
{
messageType === MessageType.USER_MESSAGE &&
!dialog.Users?.some((userDialog) => userDialog._id === profile.characters.pmc.sessionId)
) {
if (!dialog.Users) {
dialog.Users = [];
}
@ -168,8 +163,7 @@ export class DialogueController
public generateDialogueView(
request: IGetMailDialogViewRequestData,
sessionId: string,
): IGetMailDialogViewResponseData
{
): IGetMailDialogViewResponseData {
const dialogueId = request.dialogId;
const fullProfile = this.saveServer.getProfile(sessionId);
const dialogue = this.getDialogByIdFromProfile(fullProfile, request);
@ -193,10 +187,8 @@ export class DialogueController
* @param request get dialog request (params used when dialog doesnt exist in profile)
* @returns Dialogue
*/
protected getDialogByIdFromProfile(profile: ISptProfile, request: IGetMailDialogViewRequestData): Dialogue
{
if (!profile.dialogues[request.dialogId])
{
protected getDialogByIdFromProfile(profile: ISptProfile, request: IGetMailDialogViewRequestData): Dialogue {
if (!profile.dialogues[request.dialogId]) {
profile.dialogues[request.dialogId] = {
_id: request.dialogId,
attachmentsNew: 0,
@ -206,14 +198,11 @@ export class DialogueController
type: request.type,
};
if (request.type === MessageType.USER_MESSAGE)
{
if (request.type === MessageType.USER_MESSAGE) {
profile.dialogues[request.dialogId].Users = [];
const chatBot = this.dialogueChatBots.find((cb) => cb.getChatBot()._id === request.dialogId);
if (chatBot)
{
if (!profile.dialogues[request.dialogId].Users)
{
if (chatBot) {
if (!profile.dialogues[request.dialogId].Users) {
profile.dialogues[request.dialogId].Users = [];
}
profile.dialogues[request.dialogId].Users!.push(chatBot.getChatBot());
@ -230,15 +219,12 @@ export class DialogueController
* @param dialogUsers The participants of the mail
* @returns IUserDialogInfo array
*/
protected getProfilesForMail(fullProfile: ISptProfile, dialogUsers?: IUserDialogInfo[]): IUserDialogInfo[]
{
protected getProfilesForMail(fullProfile: ISptProfile, dialogUsers?: IUserDialogInfo[]): IUserDialogInfo[] {
const result: IUserDialogInfo[] = [];
if (dialogUsers)
{
if (dialogUsers) {
result.push(...dialogUsers);
if (!result.some((userDialog) => userDialog._id === fullProfile.info.id))
{
if (!result.some((userDialog) => userDialog._id === fullProfile.info.id)) {
// Player doesnt exist, add them in before returning
const pmcProfile = fullProfile.characters.pmc;
result.push({
@ -264,14 +250,11 @@ export class DialogueController
* @param dialogueID Dialog id
* @returns Count of messages with attachments
*/
protected getUnreadMessagesWithAttachmentsCount(sessionID: string, dialogueID: string): number
{
protected getUnreadMessagesWithAttachmentsCount(sessionID: string, dialogueID: string): number {
let newAttachmentCount = 0;
const activeMessages = this.getActiveMessagesFromDialog(sessionID, dialogueID);
for (const message of activeMessages)
{
if (message.hasRewards && !message.rewardCollected)
{
for (const message of activeMessages) {
if (message.hasRewards && !message.rewardCollected) {
newAttachmentCount++;
}
}
@ -284,8 +267,7 @@ export class DialogueController
* @param messages Messages to check
* @returns true if uncollected rewards found
*/
protected messagesHaveUncollectedRewards(messages: Message[]): boolean
{
protected messagesHaveUncollectedRewards(messages: Message[]): boolean {
return messages.some((message) => (message.items?.data?.length ?? 0) > 0);
}
@ -295,13 +277,16 @@ export class DialogueController
* @param dialogueId id of the dialog to remove
* @param sessionId Player id
*/
public removeDialogue(dialogueId: string, sessionId: string): void
{
public removeDialogue(dialogueId: string, sessionId: string): void {
const profile = this.saveServer.getProfile(sessionId);
const dialog = profile.dialogues[dialogueId];
if (!dialog)
{
this.logger.error(this.localisationService.getText("dialogue-unable_to_find_in_profile", { sessionId: sessionId, dialogueId: dialogueId }));
if (!dialog) {
this.logger.error(
this.localisationService.getText("dialogue-unable_to_find_in_profile", {
sessionId: sessionId,
dialogueId: dialogueId,
}),
);
return;
}
@ -310,12 +295,15 @@ export class DialogueController
}
/** Handle client/mail/dialog/pin && Handle client/mail/dialog/unpin */
public setDialoguePin(dialogueId: string, shouldPin: boolean, sessionId: string): void
{
public setDialoguePin(dialogueId: string, shouldPin: boolean, sessionId: string): void {
const dialog = this.dialogueHelper.getDialogsForProfile(sessionId)[dialogueId];
if (!dialog)
{
this.logger.error(this.localisationService.getText("dialogue-unable_to_find_in_profile", { sessionId: sessionId, dialogueId: dialogueId }));
if (!dialog) {
this.logger.error(
this.localisationService.getText("dialogue-unable_to_find_in_profile", {
sessionId: sessionId,
dialogueId: dialogueId,
}),
);
return;
}
@ -329,18 +317,19 @@ export class DialogueController
* @param dialogueIds Dialog ids to set as read
* @param sessionId Player profile id
*/
public setRead(dialogueIds: string[], sessionId: string): void
{
public setRead(dialogueIds: string[], sessionId: string): void {
const dialogs = this.dialogueHelper.getDialogsForProfile(sessionId);
if (!dialogs)
{
this.logger.error(this.localisationService.getText("dialogue-unable_to_find_dialogs_in_profile", { sessionId: sessionId }));
if (!dialogs) {
this.logger.error(
this.localisationService.getText("dialogue-unable_to_find_dialogs_in_profile", {
sessionId: sessionId,
}),
);
return;
}
for (const dialogId of dialogueIds)
{
for (const dialogId of dialogueIds) {
dialogs[dialogId].new = 0;
dialogs[dialogId].attachmentsNew = 0;
}
@ -353,13 +342,16 @@ export class DialogueController
* @param sessionId Session id
* @returns IGetAllAttachmentsResponse
*/
public getAllAttachments(dialogueId: string, sessionId: string): IGetAllAttachmentsResponse | undefined
{
public getAllAttachments(dialogueId: string, sessionId: string): IGetAllAttachmentsResponse | undefined {
const dialogs = this.dialogueHelper.getDialogsForProfile(sessionId);
const dialog = dialogs[dialogueId];
if (!dialog)
{
this.logger.error(this.localisationService.getText("dialogue-unable_to_find_in_profile", { sessionId: sessionId, dialogueId: dialogueId }));
if (!dialog) {
this.logger.error(
this.localisationService.getText("dialogue-unable_to_find_in_profile", {
sessionId: sessionId,
dialogueId: dialogueId,
}),
);
return undefined;
}
@ -378,8 +370,7 @@ export class DialogueController
}
/** client/mail/msg/send */
public sendMessage(sessionId: string, request: ISendMessageRequest): string
{
public sendMessage(sessionId: string, request: ISendMessageRequest): string {
this.mailSendService.sendPlayerMessageToNpc(sessionId, request.dialogId, request.text);
return (
@ -395,8 +386,7 @@ export class DialogueController
* @param dialogueId Dialog to get mail attachments from
* @returns Message array
*/
protected getActiveMessagesFromDialog(sessionId: string, dialogueId: string): Message[]
{
protected getActiveMessagesFromDialog(sessionId: string, dialogueId: string): Message[] {
const timeNow = this.timeUtil.getTimestamp();
const dialogs = this.dialogueHelper.getDialogsForProfile(sessionId);
return dialogs[dialogueId].messages.filter((message) => timeNow < message.dt + (message.maxStorageTime ?? 0));
@ -407,8 +397,7 @@ export class DialogueController
* @param messages Messages to parse
* @returns messages with items to collect
*/
protected getMessagesWithAttachments(messages: Message[]): Message[]
{
protected getMessagesWithAttachments(messages: Message[]): Message[] {
return messages.filter((message) => (message.items?.data?.length ?? 0) > 0);
}
@ -416,10 +405,8 @@ export class DialogueController
* Delete expired items from all messages in player profile. triggers when updating traders.
* @param sessionId Session id
*/
protected removeExpiredItemsFromMessages(sessionId: string): void
{
for (const dialogueId in this.dialogueHelper.getDialogsForProfile(sessionId))
{
protected removeExpiredItemsFromMessages(sessionId: string): void {
for (const dialogueId in this.dialogueHelper.getDialogsForProfile(sessionId)) {
this.removeExpiredItemsFromMessage(sessionId, dialogueId);
}
}
@ -429,19 +416,15 @@ export class DialogueController
* @param sessionId Session id
* @param dialogueId Dialog id
*/
protected removeExpiredItemsFromMessage(sessionId: string, dialogueId: string): void
{
protected removeExpiredItemsFromMessage(sessionId: string, dialogueId: string): void {
const dialogs = this.dialogueHelper.getDialogsForProfile(sessionId);
const dialog = dialogs[dialogueId];
if (!dialog.messages)
{
if (!dialog.messages) {
return;
}
for (const message of dialog.messages)
{
if (this.messageHasExpired(message))
{
for (const message of dialog.messages) {
if (this.messageHasExpired(message)) {
message.items = {};
}
}
@ -452,14 +435,12 @@ export class DialogueController
* @param message Message to check expiry of
* @returns true or false
*/
protected messageHasExpired(message: Message): boolean
{
protected messageHasExpired(message: Message): boolean {
return this.timeUtil.getTimestamp() > message.dt + (message.maxStorageTime ?? 0);
}
/** Handle client/friend/request/send */
public sendFriendRequest(sessionID: string, request: IFriendRequestData): IFriendRequestSendResponse
{
public sendFriendRequest(sessionID: string, request: IFriendRequestData): IFriendRequestSendResponse {
return { status: 0, requestId: "12345", retryAfter: 600 };
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { ApplicationContext } from "@spt/context/ApplicationContext";
import { ContextVariableType } from "@spt/context/ContextVariableType";
import { HideoutHelper } from "@spt/helpers/HideoutHelper";
@ -44,14 +43,14 @@ import { ProfileActivityService } from "@spt/services/ProfileActivityService";
import { ProfileFixerService } from "@spt/services/ProfileFixerService";
import { RaidTimeAdjustmentService } from "@spt/services/RaidTimeAdjustmentService";
import { SeasonalEventService } from "@spt/services/SeasonalEventService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HashUtil } from "@spt/utils/HashUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class GameController
{
export class GameController {
protected httpConfig: IHttpConfig;
protected coreConfig: ICoreConfig;
protected locationConfig: ILocationConfig;
@ -83,8 +82,7 @@ export class GameController
@inject("ApplicationContext") protected applicationContext: ApplicationContext,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
) {
this.httpConfig = this.configServer.getConfig(ConfigTypes.HTTP);
this.coreConfig = this.configServer.getConfig(ConfigTypes.CORE);
this.locationConfig = this.configServer.getConfig(ConfigTypes.LOCATION);
@ -95,8 +93,7 @@ export class GameController
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
}
public load(): void
{
public load(): void {
// Regenerate base cache now mods are loaded and game is starting
// Mods that add items and use the baseClass service generate the cache including their items, the next mod that
// add items gets left out,causing warnings
@ -107,30 +104,25 @@ export class GameController
/**
* Handle client/game/start
*/
public gameStart(_url: string, _info: IEmptyRequestData, sessionID: string, startTimeStampMS: number): void
{
public gameStart(_url: string, _info: IEmptyRequestData, sessionID: string, startTimeStampMS: number): void {
// Store client start time in app context
this.applicationContext.addValue(ContextVariableType.CLIENT_START_TIMESTAMP, startTimeStampMS);
this.profileActivityService.setActivityTimestamp(sessionID);
if (this.coreConfig.fixes.fixShotgunDispersion)
{
if (this.coreConfig.fixes.fixShotgunDispersion) {
this.fixShotgunDispersions();
}
if (this.locationConfig.addOpenZonesToAllMaps)
{
if (this.locationConfig.addOpenZonesToAllMaps) {
this.openZoneService.applyZoneChangesToAllMaps();
}
if (this.locationConfig.addCustomBotWavesToMaps)
{
if (this.locationConfig.addCustomBotWavesToMaps) {
this.customLocationWaveService.applyWaveChangesToAllMaps();
}
if (this.locationConfig.enableBotTypeLimits)
{
if (this.locationConfig.enableBotTypeLimits) {
this.adjustMapBotLimits();
}
@ -143,22 +135,18 @@ export class GameController
// repeatableQuests are stored by in profile.Quests due to the responses of the client (e.g. Quests in
// offraidData). Since we don't want to clutter the Quests list, we need to remove all completed (failed or
// successful) repeatable quests. We also have to remove the Counters from the repeatableQuests
if (sessionID)
{
if (sessionID) {
const fullProfile = this.profileHelper.getFullProfile(sessionID);
if (fullProfile.info.wipe)
{
if (fullProfile.info.wipe) {
// Don't bother doing any fixes, we're resetting profile
return;
}
if (Array.isArray(fullProfile.characters.pmc.WishList))
{
if (Array.isArray(fullProfile.characters.pmc.WishList)) {
fullProfile.characters.pmc.WishList = {};
}
if (Array.isArray(fullProfile.characters.scav.WishList))
{
if (Array.isArray(fullProfile.characters.scav.WishList)) {
fullProfile.characters.scav.WishList = {};
}
@ -167,34 +155,28 @@ export class GameController
this.logger.debug(`Started game with sessionId: ${sessionID} ${pmcProfile.Info?.Nickname}`);
// Migrate aki object data into spt for 3.9.0 release
if ((fullProfile as any).aki)
{
if ((fullProfile as any).aki) {
fullProfile.spt = this.cloner.clone((fullProfile as any).aki);
delete (fullProfile as any).aki;
}
if (this.coreConfig.fixes.fixProfileBreakingInventoryItemIssues)
{
if (this.coreConfig.fixes.fixProfileBreakingInventoryItemIssues) {
this.profileFixerService.fixProfileBreakingInventoryItemIssues(pmcProfile);
}
if (pmcProfile.Health)
{
if (pmcProfile.Health) {
this.updateProfileHealthValues(pmcProfile);
}
if (this.locationConfig.fixEmptyBotWavesSettings.enabled)
{
if (this.locationConfig.fixEmptyBotWavesSettings.enabled) {
this.fixBrokenOfflineMapWaves();
}
if (this.locationConfig.rogueLighthouseSpawnTimeSettings.enabled)
{
if (this.locationConfig.rogueLighthouseSpawnTimeSettings.enabled) {
this.fixRoguesSpawningInstantlyOnLighthouse();
}
if (this.locationConfig.splitWaveIntoSingleSpawnsSettings.enabled)
{
if (this.locationConfig.splitWaveIntoSingleSpawnsSettings.enabled) {
this.splitBotWavesIntoSingleWaves();
}
@ -202,8 +184,7 @@ export class GameController
this.profileFixerService.addMissingHideoutAreasToProfile(fullProfile);
if (pmcProfile.Inventory)
{
if (pmcProfile.Inventory) {
// MUST occur prior to `profileFixerService.checkForAndFixPmcProfileIssues()`
this.profileFixerService.fixIncorrectAidValue(fullProfile);
@ -218,8 +199,7 @@ export class GameController
this.profileFixerService.addMissingSptVersionTagToProfile(fullProfile);
if (pmcProfile.Hideout)
{
if (pmcProfile.Hideout) {
this.profileFixerService.addMissingHideoutBonusesToProfile(pmcProfile);
this.profileFixerService.addMissingUpgradesPropertyToHideout(pmcProfile);
this.hideoutHelper.setHideoutImprovementsToCompleted(pmcProfile);
@ -241,40 +221,33 @@ export class GameController
this.validateQuestAssortUnlocksExist();
if (pmcProfile.Info)
{
if (pmcProfile.Info) {
this.addPlayerToPMCNames(pmcProfile);
this.checkForAndRemoveUndefinedDialogs(fullProfile);
}
if (this.seasonalEventService.isAutomaticEventDetectionEnabled())
{
if (this.seasonalEventService.isAutomaticEventDetectionEnabled()) {
this.seasonalEventService.enableSeasonalEvents(sessionID);
}
if (pmcProfile?.Skills?.Common)
{
if (pmcProfile?.Skills?.Common) {
this.warnOnActiveBotReloadSkill(pmcProfile);
}
// Flea bsg blacklist is off
if (!this.ragfairConfig.dynamic.blacklist.enableBsgList)
{
if (!this.ragfairConfig.dynamic.blacklist.enableBsgList) {
this.setAllDbItemsAsSellableOnFlea();
}
}
}
protected adjustHideoutCraftTimes(overrideSeconds: number): void
{
if (overrideSeconds === -1)
{
protected adjustHideoutCraftTimes(overrideSeconds: number): void {
if (overrideSeconds === -1) {
return;
}
for (const craft of this.databaseService.getHideout().production)
{
for (const craft of this.databaseService.getHideout().production) {
// Only adjust crafts ABOVE the override
craft.productionTime = Math.min(craft.productionTime, overrideSeconds);
}
@ -283,32 +256,25 @@ export class GameController
/**
* Adjust all hideout craft times to be no higher than the override
*/
protected adjustHideoutBuildTimes(overrideSeconds: number): void
{
if (overrideSeconds === -1)
{
protected adjustHideoutBuildTimes(overrideSeconds: number): void {
if (overrideSeconds === -1) {
return;
}
for (const area of this.databaseService.getHideout().areas)
{
for (const stage of Object.values(area.stages))
{
for (const area of this.databaseService.getHideout().areas) {
for (const stage of Object.values(area.stages)) {
// Only adjust crafts ABOVE the override
stage.constructionTime = Math.min(stage.constructionTime, overrideSeconds);
}
}
}
protected adjustLocationBotValues(): void
{
protected adjustLocationBotValues(): void {
const mapsDb = this.databaseService.getLocations();
for (const locationKey in this.botConfig.maxBotCap)
{
for (const locationKey in this.botConfig.maxBotCap) {
const map: ILocation = mapsDb[locationKey];
if (!map)
{
if (!map) {
continue;
}
@ -322,15 +288,16 @@ export class GameController
/**
* Out of date/incorrectly made trader mods forget this data
*/
protected checkTraderRepairValuesExist(): void
{
protected checkTraderRepairValuesExist(): void {
const traders = this.databaseService.getTraders();
for (const trader of Object.values(traders))
{
if (!trader?.base?.repair)
{
this.logger.warning(this.localisationService.getText("trader-missing_repair_property_using_default",
{ traderId: trader.base._id, nickname: trader.base.nickname }));
for (const trader of Object.values(traders)) {
if (!trader?.base?.repair) {
this.logger.warning(
this.localisationService.getText("trader-missing_repair_property_using_default", {
traderId: trader.base._id,
nickname: trader.base.nickname,
}),
);
// use ragfair trader as a default
trader.base.repair = this.cloner.clone(traders.ragfair.base.repair);
@ -338,49 +305,46 @@ export class GameController
return;
}
if (trader.base.repair?.quality === undefined)
{
this.logger.warning(this.localisationService.getText("trader-missing_repair_quality_property_using_default",
{ traderId: trader.base._id, nickname: trader.base.nickname }));
if (trader.base.repair?.quality === undefined) {
this.logger.warning(
this.localisationService.getText("trader-missing_repair_quality_property_using_default", {
traderId: trader.base._id,
nickname: trader.base.nickname,
}),
);
// use ragfair trader as a default
trader.base.repair.quality = this.cloner.clone(
traders.ragfair.base.repair.quality,
);
trader.base.repair.quality = this.cloner.clone(traders.ragfair.base.repair.quality);
trader.base.repair.quality = traders.ragfair.base.repair.quality;
}
}
}
protected addCustomLooseLootPositions(): void
{
protected addCustomLooseLootPositions(): void {
const looseLootPositionsToAdd = this.lootConfig.looseLoot;
for (const [mapId, positionsToAdd] of Object.entries(looseLootPositionsToAdd))
{
if (!mapId)
{
this.logger.warning(this.localisationService.getText("location-unable_to_add_custom_loot_position", mapId));
for (const [mapId, positionsToAdd] of Object.entries(looseLootPositionsToAdd)) {
if (!mapId) {
this.logger.warning(
this.localisationService.getText("location-unable_to_add_custom_loot_position", mapId),
);
continue;
}
const mapLooseLoot = this.databaseService.getLocation(mapId).looseLoot;
if (!mapLooseLoot)
{
if (!mapLooseLoot) {
this.logger.warning(this.localisationService.getText("location-map_has_no_loose_loot_data", mapId));
continue;
}
for (const positionToAdd of positionsToAdd)
{
for (const positionToAdd of positionsToAdd) {
// Exists already, add new items to existing positions pool
const existingLootPosition = mapLooseLoot.spawnpoints.find(
(x) => x.template.Id === positionToAdd.template.Id,
);
if (existingLootPosition)
{
if (existingLootPosition) {
existingLootPosition.template.Items.push(...positionToAdd.template.Items);
existingLootPosition.itemDistribution.push(...positionToAdd.itemDistribution);
@ -393,27 +357,27 @@ export class GameController
}
}
protected adjustLooseLootSpawnProbabilities(): void
{
protected adjustLooseLootSpawnProbabilities(): void {
const adjustments = this.lootConfig.looseLootSpawnPointAdjustments;
for (const [mapId, mapAdjustments] of Object.entries(adjustments))
{
for (const [mapId, mapAdjustments] of Object.entries(adjustments)) {
const mapLooseLootData = this.databaseService.getLocation(mapId).looseLoot;
if (!mapLooseLootData)
{
if (!mapLooseLootData) {
this.logger.warning(this.localisationService.getText("location-map_has_no_loose_loot_data", mapId));
continue;
}
for (const [lootKey, newChanceValue] of Object.entries(mapAdjustments))
{
const lootPostionToAdjust = mapLooseLootData.spawnpoints
.find((spawnPoint) => spawnPoint.template.Id === lootKey);
if (!lootPostionToAdjust)
{
this.logger.warning(this.localisationService.getText("location-unable_to_adjust_loot_position_on_map",
{ lootKey: lootKey, mapId: mapId }));
for (const [lootKey, newChanceValue] of Object.entries(mapAdjustments)) {
const lootPostionToAdjust = mapLooseLootData.spawnpoints.find(
(spawnPoint) => spawnPoint.template.Id === lootKey,
);
if (!lootPostionToAdjust) {
this.logger.warning(
this.localisationService.getText("location-unable_to_adjust_loot_position_on_map", {
lootKey: lootKey,
mapId: mapId,
}),
);
continue;
}
@ -424,36 +388,28 @@ export class GameController
}
/** Apply custom limits on bot types as defined in configs/location.json/botTypeLimits */
protected adjustMapBotLimits(): void
{
protected adjustMapBotLimits(): void {
const mapsDb = this.databaseService.getLocations();
if (!this.locationConfig.botTypeLimits)
{
if (!this.locationConfig.botTypeLimits) {
return;
}
for (const mapId in this.locationConfig.botTypeLimits)
{
for (const mapId in this.locationConfig.botTypeLimits) {
const map: ILocation = mapsDb[mapId];
if (!map)
{
if (!map) {
this.logger.warning(
this.localisationService.getText("bot-unable_to_edit_limits_of_unknown_map", mapId),
);
}
for (const botToLimit of this.locationConfig.botTypeLimits[mapId])
{
for (const botToLimit of this.locationConfig.botTypeLimits[mapId]) {
const index = map.base.MinMaxBots.findIndex((x) => x.WildSpawnType === botToLimit.type);
if (index !== -1)
{
if (index !== -1) {
// Existing bot type found in MinMaxBots array, edit
const limitObjectToUpdate = map.base.MinMaxBots[index];
limitObjectToUpdate.min = botToLimit.min;
limitObjectToUpdate.max = botToLimit.max;
}
else
{
} else {
// Bot type not found, add new object
map.base.MinMaxBots.push({
// Bot type not found, add new object
@ -469,11 +425,10 @@ export class GameController
/**
* Handle client/game/config
*/
public getGameConfig(sessionID: string): IGameConfigResponse
{
public getGameConfig(sessionID: string): IGameConfigResponse {
const profile = this.profileHelper.getPmcProfile(sessionID);
const gameTime
= profile.Stats?.Eft.OverallCounters.Items?.find(
const gameTime =
profile.Stats?.Eft.OverallCounters.Items?.find(
(counter) => counter.Key.includes("LifeTime") && counter.Key.includes("Pmc"),
)?.Value ?? 0;
@ -504,40 +459,35 @@ export class GameController
/**
* Handle client/game/mode
*/
public getGameMode(sessionID: string, info: IGameModeRequestData): any
{
public getGameMode(sessionID: string, info: IGameModeRequestData): any {
return { gameMode: ESessionMode.PVE, backendUrl: this.httpServerHelper.getBackendUrl() };
}
/**
* Handle client/server/list
*/
public getServer(sessionId: string): IServerDetails[]
{
public getServer(sessionId: string): IServerDetails[] {
return [{ ip: this.httpConfig.backendIp, port: Number.parseInt(this.httpConfig.backendPort) }];
}
/**
* Handle client/match/group/current
*/
public getCurrentGroup(sessionId: string): ICurrentGroupResponse
{
public getCurrentGroup(sessionId: string): ICurrentGroupResponse {
return { squad: [] };
}
/**
* Handle client/checkVersion
*/
public getValidGameVersion(sessionId: string): ICheckVersionResponse
{
public getValidGameVersion(sessionId: string): ICheckVersionResponse {
return { isvalid: true, latestVersion: this.coreConfig.compatibleTarkovVersion };
}
/**
* Handle client/game/keepalive
*/
public getKeepAlive(sessionId: string): IGameKeepAliveResponse
{
public getKeepAlive(sessionId: string): IGameKeepAliveResponse {
this.profileActivityService.setActivityTimestamp(sessionId);
return { msg: "OK", utc_time: new Date().getTime() / 1000 };
}
@ -545,8 +495,7 @@ export class GameController
/**
* Handle singleplayer/settings/getRaidTime
*/
public getRaidTime(sessionId: string, request: IGetRaidTimeRequest): IGetRaidTimeResponse
{
public getRaidTime(sessionId: string, request: IGetRaidTimeRequest): IGetRaidTimeResponse {
// Set interval times to in-raid value
this.ragfairConfig.runIntervalSeconds = this.ragfairConfig.runIntervalValues.inRaid;
@ -558,19 +507,12 @@ export class GameController
/**
* BSG have two values for shotgun dispersion, we make sure both have the same value
*/
protected fixShotgunDispersions(): void
{
protected fixShotgunDispersions(): void {
const itemDb = this.databaseService.getItems();
const shotguns = [
Weapons.SHOTGUN_12G_SAIGA_12K,
Weapons.SHOTGUN_20G_TOZ_106,
Weapons.SHOTGUN_12G_M870,
];
for (const shotgunId of shotguns)
{
if (itemDb[shotgunId]._props.ShotgunDispersion)
{
const shotguns = [Weapons.SHOTGUN_12G_SAIGA_12K, Weapons.SHOTGUN_20G_TOZ_106, Weapons.SHOTGUN_12G_M870];
for (const shotgunId of shotguns) {
if (itemDb[shotgunId]._props.ShotgunDispersion) {
itemDb[shotgunId]._props.shotgunDispersion = itemDb[shotgunId]._props.ShotgunDispersion;
}
}
@ -580,22 +522,17 @@ export class GameController
* Players set botReload to a high value and don't expect the crazy fast reload speeds, give them a warn about it
* @param pmcProfile Player profile
*/
protected warnOnActiveBotReloadSkill(pmcProfile: IPmcData): void
{
protected warnOnActiveBotReloadSkill(pmcProfile: IPmcData): void {
const botReloadSkill = this.profileHelper.getSkillFromProfile(pmcProfile, SkillTypes.BOT_RELOAD);
if (botReloadSkill?.Progress > 0)
{
if (botReloadSkill?.Progress > 0) {
this.logger.warning(this.localisationService.getText("server_start_player_active_botreload_skill"));
}
}
protected setAllDbItemsAsSellableOnFlea(): void
{
protected setAllDbItemsAsSellableOnFlea(): void {
const dbItems = Object.values(this.databaseService.getItems());
for (const item of dbItems)
{
if (item._type === "Item" && !item._props?.CanSellOnRagfair)
{
for (const item of dbItems) {
if (item._type === "Item" && !item._props?.CanSellOnRagfair) {
item._props.CanSellOnRagfair = true;
}
}
@ -605,15 +542,13 @@ export class GameController
* When player logs in, iterate over all active effects and reduce timer
* @param pmcProfile Profile to adjust values for
*/
protected updateProfileHealthValues(pmcProfile: IPmcData): void
{
protected updateProfileHealthValues(pmcProfile: IPmcData): void {
const healthLastUpdated = pmcProfile.Health.UpdateTime;
const currentTimeStamp = this.timeUtil.getTimestamp();
const diffSeconds = currentTimeStamp - healthLastUpdated;
// Last update is in past
if (healthLastUpdated < currentTimeStamp)
{
if (healthLastUpdated < currentTimeStamp) {
// Base values
let energyRegenPerHour = 60;
let hydrationRegenPerHour = 60;
@ -632,53 +567,42 @@ export class GameController
);
// Player has energy deficit
if (pmcProfile.Health.Energy.Current !== pmcProfile.Health.Energy.Maximum)
{
if (pmcProfile.Health.Energy.Current !== pmcProfile.Health.Energy.Maximum) {
// Set new value, whatever is smallest
pmcProfile.Health.Energy.Current += Math.round(energyRegenPerHour * (diffSeconds / 3600));
if (pmcProfile.Health.Energy.Current > pmcProfile.Health.Energy.Maximum)
{
if (pmcProfile.Health.Energy.Current > pmcProfile.Health.Energy.Maximum) {
pmcProfile.Health.Energy.Current = pmcProfile.Health.Energy.Maximum;
}
}
// Player has hydration deficit
if (pmcProfile.Health.Hydration.Current !== pmcProfile.Health.Hydration.Maximum)
{
if (pmcProfile.Health.Hydration.Current !== pmcProfile.Health.Hydration.Maximum) {
pmcProfile.Health.Hydration.Current += Math.round(hydrationRegenPerHour * (diffSeconds / 3600));
if (pmcProfile.Health.Hydration.Current > pmcProfile.Health.Hydration.Maximum)
{
if (pmcProfile.Health.Hydration.Current > pmcProfile.Health.Hydration.Maximum) {
pmcProfile.Health.Hydration.Current = pmcProfile.Health.Hydration.Maximum;
}
}
// Check all body parts
for (const bodyPartKey in pmcProfile.Health.BodyParts)
{
for (const bodyPartKey in pmcProfile.Health.BodyParts) {
const bodyPart = pmcProfile.Health.BodyParts[bodyPartKey] as BodyPartHealth;
// Check part hp
if (bodyPart.Health.Current < bodyPart.Health.Maximum)
{
if (bodyPart.Health.Current < bodyPart.Health.Maximum) {
bodyPart.Health.Current += Math.round(hpRegenPerHour * (diffSeconds / 3600));
}
if (bodyPart.Health.Current > bodyPart.Health.Maximum)
{
if (bodyPart.Health.Current > bodyPart.Health.Maximum) {
bodyPart.Health.Current = bodyPart.Health.Maximum;
}
// Look for effects
if (Object.keys(bodyPart.Effects ?? {}).length > 0)
{
if (Object.keys(bodyPart.Effects ?? {}).length > 0) {
// Decrement effect time value by difference between current time and time health was last updated
for (const effectKey in bodyPart.Effects)
{
for (const effectKey in bodyPart.Effects) {
// remove effects below 1, .e.g. bleeds at -1
if (bodyPart.Effects[effectKey].Time < 1)
{
if (bodyPart.Effects[effectKey].Time < 1) {
// More than 30 mins has passed
if (diffSeconds > 1800)
{
if (diffSeconds > 1800) {
delete bodyPart.Effects[effectKey];
}
@ -686,8 +610,7 @@ export class GameController
}
bodyPart.Effects[effectKey].Time -= diffSeconds;
if (bodyPart.Effects[effectKey].Time < 1)
{
if (bodyPart.Effects[effectKey].Time < 1) {
// effect time was sub 1, set floor it can be
bodyPart.Effects[effectKey].Time = 1;
}
@ -701,31 +624,25 @@ export class GameController
/**
* Waves with an identical min/max values spawn nothing, the number of bots that spawn is the difference between min and max
*/
protected fixBrokenOfflineMapWaves(): void
{
protected fixBrokenOfflineMapWaves(): void {
const locations = this.databaseService.getLocations();
for (const locationKey in locations)
{
for (const locationKey in locations) {
// Skip ignored maps
if (this.locationConfig.fixEmptyBotWavesSettings.ignoreMaps.includes(locationKey))
{
if (this.locationConfig.fixEmptyBotWavesSettings.ignoreMaps.includes(locationKey)) {
continue;
}
// Loop over all of the locations waves and look for waves with identical min and max slots
const location: ILocation = locations[locationKey];
if (!location.base)
{
if (!location.base) {
this.logger.warning(
this.localisationService.getText("location-unable_to_fix_broken_waves_missing_base", locationKey),
);
continue;
}
for (const wave of location.base.waves ?? [])
{
if (wave.slots_max - wave.slots_min === 0)
{
for (const wave of location.base.waves ?? []) {
if (wave.slots_max - wave.slots_min === 0) {
this.logger.debug(
`Fixed ${wave.WildSpawnType} Spawn: ${locationKey} wave: ${wave.number} of type: ${wave.WildSpawnType} in zone: ${wave.SpawnPoints} with Max Slots of ${wave.slots_max}`,
);
@ -738,21 +655,18 @@ export class GameController
/**
* Make Rogues spawn later to allow for scavs to spawn first instead of rogues filling up all spawn positions
*/
protected fixRoguesSpawningInstantlyOnLighthouse(): void
{
protected fixRoguesSpawningInstantlyOnLighthouse(): void {
const rogueSpawnDelaySeconds = this.locationConfig.rogueLighthouseSpawnTimeSettings.waitTimeSeconds;
const lighthouse = this.databaseService.getLocations().lighthouse?.base;
if (!lighthouse)
{
if (!lighthouse) {
return;
}
// Find Rogues that spawn instantly
const instantRogueBossSpawns = lighthouse.BossLocationSpawn
.filter((spawn) => spawn.BossName === "exUsec"
&& spawn.Time === -1);
for (const wave of instantRogueBossSpawns)
{
const instantRogueBossSpawns = lighthouse.BossLocationSpawn.filter(
(spawn) => spawn.BossName === "exUsec" && spawn.Time === -1,
);
for (const wave of instantRogueBossSpawns) {
wave.Time = rogueSpawnDelaySeconds;
}
}
@ -761,21 +675,18 @@ export class GameController
* Send starting gifts to profile after x days
* @param pmcProfile Profile to add gifts to
*/
protected sendPraporGiftsToNewProfiles(pmcProfile: IPmcData): void
{
protected sendPraporGiftsToNewProfiles(pmcProfile: IPmcData): void {
const timeStampProfileCreated = pmcProfile.Info.RegistrationDate;
const oneDaySeconds = this.timeUtil.getHoursAsSeconds(24);
const currentTimeStamp = this.timeUtil.getTimestamp();
// One day post-profile creation
if (currentTimeStamp > timeStampProfileCreated + oneDaySeconds)
{
if (currentTimeStamp > timeStampProfileCreated + oneDaySeconds) {
this.giftService.sendPraporStartingGift(pmcProfile.sessionId, 1);
}
// Two day post-profile creation
if (currentTimeStamp > timeStampProfileCreated + oneDaySeconds * 2)
{
if (currentTimeStamp > timeStampProfileCreated + oneDaySeconds * 2) {
this.giftService.sendPraporStartingGift(pmcProfile.sessionId, 2);
}
}
@ -784,26 +695,21 @@ export class GameController
* Find and split waves with large numbers of bots into smaller waves - BSG appears to reduce the size of these
* waves to one bot when they're waiting to spawn for too long
*/
protected splitBotWavesIntoSingleWaves(): void
{
protected splitBotWavesIntoSingleWaves(): void {
const locations = this.databaseService.getLocations();
for (const locationKey in locations)
{
if (this.locationConfig.splitWaveIntoSingleSpawnsSettings.ignoreMaps.includes(locationKey))
{
for (const locationKey in locations) {
if (this.locationConfig.splitWaveIntoSingleSpawnsSettings.ignoreMaps.includes(locationKey)) {
continue;
}
// Iterate over all maps
const location: ILocation = locations[locationKey];
for (const wave of location.base.waves)
{
for (const wave of location.base.waves) {
// Wave has size that makes it candidate for splitting
if (
wave.slots_max - wave.slots_min
>= this.locationConfig.splitWaveIntoSingleSpawnsSettings.waveSizeThreshold
)
{
wave.slots_max - wave.slots_min >=
this.locationConfig.splitWaveIntoSingleSpawnsSettings.waveSizeThreshold
) {
// Get count of bots to be spawned in wave
const waveSize = wave.slots_max - wave.slots_min;
@ -819,14 +725,12 @@ export class GameController
// Add new waves to fill gap from bots we removed in above wave
let wavesAddedCount = 0;
for (let index = indexOfWaveToSplit + 1; index < indexOfWaveToSplit + waveSize; index++)
{
for (let index = indexOfWaveToSplit + 1; index < indexOfWaveToSplit + waveSize; index++) {
// Clone wave ready to insert into array
const waveToAddClone = this.cloner.clone(wave);
// Some waves have value of 0 for some reason, preserve
if (waveToAddClone.number !== 0)
{
if (waveToAddClone.number !== 0) {
// Update wave number to new location in array
waveToAddClone.number = index;
}
@ -841,11 +745,9 @@ export class GameController
let index = indexOfWaveToSplit + wavesAddedCount + 1;
index < location.base.waves.length;
index++
)
{
) {
// Some waves have value of 0, leave them as-is
if (location.base.waves[index].number !== 0)
{
if (location.base.waves[index].number !== 0) {
location.base.waves[index].number += wavesAddedCount;
}
}
@ -858,28 +760,24 @@ export class GameController
* Get a list of installed mods and save their details to the profile being used
* @param fullProfile Profile to add mod details to
*/
protected saveActiveModsToProfile(fullProfile: ISptProfile): void
{
protected saveActiveModsToProfile(fullProfile: ISptProfile): void {
// Add empty mod array if undefined
if (!fullProfile.spt.mods)
{
if (!fullProfile.spt.mods) {
fullProfile.spt.mods = [];
}
// Get active mods
const activeMods = this.preSptModLoader.getImportedModDetails();
for (const modKey in activeMods)
{
for (const modKey in activeMods) {
const modDetails = activeMods[modKey];
if (
fullProfile.spt.mods.some(
(mod) =>
mod.author === modDetails.author
&& mod.name === modDetails.name
&& mod.version === modDetails.version,
mod.author === modDetails.author &&
mod.name === modDetails.name &&
mod.version === modDetails.version,
)
)
{
) {
// Exists already, skip
continue;
}
@ -897,17 +795,14 @@ export class GameController
/**
* Check for any missing assorts inside each traders assort.json data, checking against traders questassort.json
*/
protected validateQuestAssortUnlocksExist(): void
{
protected validateQuestAssortUnlocksExist(): void {
const db = this.databaseService.getTables();
const traders = db.traders!;
const quests = db.templates!.quests;
for (const traderId of Object.values(Traders))
{
for (const traderId of Object.values(Traders)) {
const traderData = traders[traderId];
const traderAssorts = traderData?.assort;
if (!traderAssorts)
{
if (!traderAssorts) {
continue;
}
@ -919,11 +814,9 @@ export class GameController
};
// Loop over all assorts for trader
for (const [assortKey, questKey] of Object.entries(mergedQuestAssorts))
{
for (const [assortKey, questKey] of Object.entries(mergedQuestAssorts)) {
// Does assort key exist in trader assort file
if (!traderAssorts.loyal_level_items[assortKey])
{
if (!traderAssorts.loyal_level_items[assortKey]) {
// Reverse lookup of enum key by value
const messageValues = {
traderName: Object.keys(Traders)[Object.values(Traders).indexOf(traderId)],
@ -941,20 +834,16 @@ export class GameController
* Add the logged in players name to PMC name pool
* @param pmcProfile Profile of player to get name from
*/
protected addPlayerToPMCNames(pmcProfile: IPmcData): void
{
protected addPlayerToPMCNames(pmcProfile: IPmcData): void {
const playerName = pmcProfile.Info.Nickname;
if (playerName)
{
if (playerName) {
const bots = this.databaseService.getBots().types;
if (bots.bear)
{
if (bots.bear) {
bots.bear.firstName.push(playerName);
}
if (bots.usec)
{
if (bots.usec) {
bots.usec.firstName.push(playerName);
}
}
@ -964,11 +853,9 @@ export class GameController
* Check for a dialog with the key 'undefined', and remove it
* @param fullProfile Profile to check for dialog in
*/
protected checkForAndRemoveUndefinedDialogs(fullProfile: ISptProfile): void
{
protected checkForAndRemoveUndefinedDialogs(fullProfile: ISptProfile): void {
const undefinedDialog = fullProfile.dialogues.undefined;
if (undefinedDialog)
{
if (undefinedDialog) {
delete fullProfile.dialogues.undefined;
}
}
@ -976,12 +863,10 @@ export class GameController
/**
* Blank out the "test" mail message from prapor
*/
protected removePraporTestMessage(): void
{
protected removePraporTestMessage(): void {
// Iterate over all languages (e.g. "en", "fr")
const locales = this.databaseService.getLocales();
for (const localeKey in locales.global)
{
for (const localeKey in locales.global) {
locales.global[localeKey]["61687e2c3e526901fa76baf9"] = "";
}
}
@ -989,24 +874,21 @@ export class GameController
/**
* Make non-trigger-spawned raiders spawn earlier + always
*/
protected adjustLabsRaiderSpawnRate(): void
{
protected adjustLabsRaiderSpawnRate(): void {
const labsBase = this.databaseService.getLocations().laboratory!.base;
// Find spawns with empty string for triggerId/TriggerName
const nonTriggerLabsBossSpawns = labsBase.BossLocationSpawn
.filter((bossSpawn) => !bossSpawn.TriggerId
&& !bossSpawn.TriggerName);
const nonTriggerLabsBossSpawns = labsBase.BossLocationSpawn.filter(
(bossSpawn) => !bossSpawn.TriggerId && !bossSpawn.TriggerName,
);
for (const boss of nonTriggerLabsBossSpawns)
{
for (const boss of nonTriggerLabsBossSpawns) {
boss.BossChance = 100;
boss.Time /= 10;
}
}
protected logProfileDetails(fullProfile: ISptProfile): void
{
protected logProfileDetails(fullProfile: ISptProfile): void {
this.logger.debug(`Profile made with: ${fullProfile.spt.version}`);
this.logger.debug(
`Server version: ${globalThis.G_SPTVERSION || this.coreConfig.sptVersion} ${globalThis.G_COMMIT}`,

View File

@ -1,18 +1,15 @@
import { inject, injectable } from "tsyringe";
import { HandbookHelper } from "@spt/helpers/HandbookHelper";
import { DatabaseServer } from "@spt/servers/DatabaseServer";
import { inject, injectable } from "tsyringe";
@injectable()
export class HandbookController
{
export class HandbookController {
constructor(
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("HandbookHelper") protected handbookHelper: HandbookHelper,
)
{}
) {}
public load(): void
{
public load(): void {
return;
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { HealthHelper } from "@spt/helpers/HealthHelper";
import { InventoryHelper } from "@spt/helpers/InventoryHelper";
import { ItemHelper } from "@spt/helpers/ItemHelper";
@ -15,12 +14,12 @@ import { ILogger } from "@spt/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt/routers/EventOutputHolder";
import { LocalisationService } from "@spt/services/LocalisationService";
import { PaymentService } from "@spt/services/PaymentService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class HealthController
{
export class HealthController {
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
@ -31,8 +30,7 @@ export class HealthController
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("HealthHelper") protected healthHelper: HealthHelper,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{}
) {}
/**
* stores in-raid player health
@ -48,8 +46,7 @@ export class HealthController
sessionID: string,
addEffects = true,
deleteExistingEffects = true,
): void
{
): void {
this.healthHelper.saveVitality(pmcData, info, sessionID, addEffects, deleteExistingEffects);
}
@ -64,14 +61,12 @@ export class HealthController
pmcData: IPmcData,
request: IOffraidHealRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionID);
// Update medkit used (hpresource)
const healingItemToUse = pmcData.Inventory.items.find((item) => item._id === request.item);
if (!healingItemToUse)
{
if (!healingItemToUse) {
const errorMessage = this.localisationService.getText(
"health-healing_item_not_found",
healingItemToUse._id,
@ -84,20 +79,16 @@ export class HealthController
// Ensure item has a upd object
this.itemHelper.addUpdObjectToItem(healingItemToUse);
if (healingItemToUse.upd.MedKit)
{
if (healingItemToUse.upd.MedKit) {
healingItemToUse.upd.MedKit.HpResource -= request.count;
}
else
{
} else {
// Get max healing from db
const maxhp = this.itemHelper.getItem(healingItemToUse._tpl)[1]._props.MaxHpResource;
healingItemToUse.upd.MedKit = { HpResource: maxhp - request.count }; // Subtract amout used from max
}
// Resource in medkit is spent, delete it
if (healingItemToUse.upd.MedKit.HpResource <= 0)
{
if (healingItemToUse.upd.MedKit.HpResource <= 0) {
this.inventoryHelper.removeItem(pmcData, request.item, sessionID, output);
}
@ -112,14 +103,12 @@ export class HealthController
* @param sessionID Session id
* @returns IItemEventRouterResponse
*/
public offraidEat(pmcData: IPmcData, request: IOffraidEatRequestData, sessionID: string): IItemEventRouterResponse
{
public offraidEat(pmcData: IPmcData, request: IOffraidEatRequestData, sessionID: string): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionID);
let resourceLeft = 0;
const itemToConsume = pmcData.Inventory.items.find((item) => item._id === request.item);
if (!itemToConsume)
{
if (!itemToConsume) {
// Item not found, very bad
return this.httpResponse.appendErrorToOutput(
output,
@ -128,17 +117,13 @@ export class HealthController
}
const consumedItemMaxResource = this.itemHelper.getItem(itemToConsume._tpl)[1]._props.MaxResource;
if (consumedItemMaxResource > 1)
{
if (consumedItemMaxResource > 1) {
// Ensure item has a upd object
this.itemHelper.addUpdObjectToItem(itemToConsume);
if (itemToConsume.upd.FoodDrink === undefined)
{
if (itemToConsume.upd.FoodDrink === undefined) {
itemToConsume.upd.FoodDrink = { HpPercent: consumedItemMaxResource - request.count };
}
else
{
} else {
itemToConsume.upd.FoodDrink.HpPercent -= request.count;
}
@ -146,8 +131,7 @@ export class HealthController
}
// Remove item from inventory if resource has dropped below threshold
if (consumedItemMaxResource === 1 || resourceLeft < 1)
{
if (consumedItemMaxResource === 1 || resourceLeft < 1) {
this.inventoryHelper.removeItem(pmcData, request.item, sessionID, output);
}
@ -166,8 +150,7 @@ export class HealthController
pmcData: IPmcData,
healthTreatmentRequest: IHealthTreatmentRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionID);
const payMoneyRequest: IProcessBuyTradeRequestData = {
Action: healthTreatmentRequest.Action,
@ -180,36 +163,30 @@ export class HealthController
};
this.paymentService.payMoney(pmcData, payMoneyRequest, sessionID, output);
if (output.warnings.length > 0)
{
if (output.warnings.length > 0) {
return output;
}
for (const bodyPartKey in healthTreatmentRequest.difference.BodyParts)
{
for (const bodyPartKey in healthTreatmentRequest.difference.BodyParts) {
// Get body part from request + from pmc profile
const partRequest: BodyPart = healthTreatmentRequest.difference.BodyParts[bodyPartKey];
const profilePart = pmcData.Health.BodyParts[bodyPartKey];
// Bodypart healing is chosen when part request hp is above 0
if (partRequest.Health > 0)
{
if (partRequest.Health > 0) {
// Heal bodypart
profilePart.Health.Current = profilePart.Health.Maximum;
}
// Check for effects to remove
if (partRequest.Effects?.length > 0)
{
if (partRequest.Effects?.length > 0) {
// Found some, loop over them and remove from pmc profile
for (const effect of partRequest.Effects)
{
for (const effect of partRequest.Effects) {
delete pmcData.Health.BodyParts[bodyPartKey].Effects[effect];
}
// Remove empty effect object
if (Object.keys(pmcData.Health.BodyParts[bodyPartKey].Effects).length === 0)
{
if (Object.keys(pmcData.Health.BodyParts[bodyPartKey].Effects).length === 0) {
delete pmcData.Health.BodyParts[bodyPartKey].Effects;
}
}
@ -227,8 +204,7 @@ export class HealthController
* @param info Request data
* @param sessionID
*/
public applyWorkoutChanges(pmcData: IPmcData, info: IWorkoutData, sessionId: string): void
{
public applyWorkoutChanges(pmcData: IPmcData, info: IWorkoutData, sessionId: string): void {
// https://dev.sp-tarkov.com/SPT/Server/issues/2674
// TODO:
// Health effects (fractures etc) are handled in /player/health/sync.

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { ScavCaseRewardGenerator } from "@spt/generators/ScavCaseRewardGenerator";
import { HideoutHelper } from "@spt/helpers/HideoutHelper";
import { InventoryHelper } from "@spt/helpers/InventoryHelper";
@ -7,12 +6,7 @@ import { PaymentHelper } from "@spt/helpers/PaymentHelper";
import { PresetHelper } from "@spt/helpers/PresetHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import {
HideoutArea,
ITaskConditionCounter,
Product,
ScavCase,
} from "@spt/models/eft/common/tables/IBotBase";
import { HideoutArea, ITaskConditionCounter, Product, ScavCase } from "@spt/models/eft/common/tables/IBotBase";
import { Item } from "@spt/models/eft/common/tables/IItem";
import { HideoutUpgradeCompleteRequestData } from "@spt/models/eft/hideout/HideoutUpgradeCompleteRequestData";
import { IHandleQTEEventRequestData } from "@spt/models/eft/hideout/IHandleQTEEventRequestData";
@ -47,15 +41,15 @@ import { FenceService } from "@spt/services/FenceService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { PlayerService } from "@spt/services/PlayerService";
import { ProfileActivityService } from "@spt/services/ProfileActivityService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HashUtil } from "@spt/utils/HashUtil";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class HideoutController
{
export class HideoutController {
/** Key used in TaskConditionCounters array */
protected static nameTaskConditionCountersCrafting = "CounterHoursCrafting";
protected hideoutConfig: IHideoutConfig;
@ -82,8 +76,7 @@ export class HideoutController
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("FenceService") protected fenceService: FenceService,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
) {
this.hideoutConfig = this.configServer.getConfig(ConfigTypes.HIDEOUT);
}
@ -100,19 +93,15 @@ export class HideoutController
request: IHideoutUpgradeRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): void
{
const items = request.items.map((reqItem) =>
{
): void {
const items = request.items.map((reqItem) => {
const item = pmcData.Inventory.items.find((invItem) => invItem._id === reqItem.id);
return { inventoryItem: item, requestedItem: reqItem };
});
// If it's not money, its construction / barter items
for (const item of items)
{
if (!item.inventoryItem)
{
for (const item of items) {
if (!item.inventoryItem) {
this.logger.error(
this.localisationService.getText("hideout-unable_to_find_item_in_inventory", item.requestedItem.id),
);
@ -122,24 +111,20 @@ export class HideoutController
}
if (
this.paymentHelper.isMoneyTpl(item.inventoryItem._tpl)
&& item.inventoryItem.upd
&& item.inventoryItem.upd.StackObjectsCount
&& item.inventoryItem.upd.StackObjectsCount > item.requestedItem.count
)
{
this.paymentHelper.isMoneyTpl(item.inventoryItem._tpl) &&
item.inventoryItem.upd &&
item.inventoryItem.upd.StackObjectsCount &&
item.inventoryItem.upd.StackObjectsCount > item.requestedItem.count
) {
item.inventoryItem.upd.StackObjectsCount -= item.requestedItem.count;
}
else
{
} else {
this.inventoryHelper.removeItem(pmcData, item.inventoryItem._id, sessionID, output);
}
}
// Construction time management
const profileHideoutArea = pmcData.Hideout.Areas.find((area) => area.type === request.areaType);
if (!profileHideoutArea)
{
if (!profileHideoutArea) {
this.logger.error(this.localisationService.getText("hideout-unable_to_find_area", request.areaType));
this.httpResponse.appendErrorToOutput(output);
@ -149,8 +134,7 @@ export class HideoutController
const hideoutDataDb = this.databaseService
.getTables()
.hideout.areas.find((area) => area.type === request.areaType);
if (!hideoutDataDb)
{
if (!hideoutDataDb) {
this.logger.error(
this.localisationService.getText("hideout-unable_to_find_area_in_database", request.areaType),
);
@ -160,10 +144,8 @@ export class HideoutController
}
let ctime = hideoutDataDb.stages[profileHideoutArea.level + 1].constructionTime;
if (ctime > 0)
{
if (this.profileHelper.isDeveloperAccount(sessionID))
{
if (ctime > 0) {
if (this.profileHelper.isDeveloperAccount(sessionID)) {
ctime = 40;
}
const timestamp = this.timeUtil.getTimestamp();
@ -186,14 +168,12 @@ export class HideoutController
request: HideoutUpgradeCompleteRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): void
{
): void {
const hideout = this.databaseService.getHideout();
const globals = this.databaseService.getGlobals();
const profileHideoutArea = pmcData.Hideout.Areas.find((area) => area.type === request.areaType);
if (!profileHideoutArea)
{
if (!profileHideoutArea) {
this.logger.error(this.localisationService.getText("hideout-unable_to_find_area", request.areaType));
this.httpResponse.appendErrorToOutput(output);
@ -206,8 +186,7 @@ export class HideoutController
profileHideoutArea.constructing = false;
const hideoutData = hideout.areas.find((area) => area.type === profileHideoutArea.type);
if (!hideoutData)
{
if (!hideoutData) {
this.logger.error(
this.localisationService.getText("hideout-unable_to_find_area_in_database", request.areaType),
);
@ -219,17 +198,14 @@ export class HideoutController
// Apply bonuses
const hideoutStage = hideoutData.stages[profileHideoutArea.level];
const bonuses = hideoutStage.bonuses;
if (bonuses?.length > 0)
{
for (const bonus of bonuses)
{
if (bonuses?.length > 0) {
for (const bonus of bonuses) {
this.hideoutHelper.applyPlayerUpgradesBonuses(pmcData, bonus);
}
}
// Upgrade includes a container improvement/addition
if (hideoutStage?.container)
{
if (hideoutStage?.container) {
this.addContainerImprovementToProfile(
output,
sessionID,
@ -242,10 +218,9 @@ export class HideoutController
// Upgrading water collector / med station
if (
profileHideoutArea.type === HideoutAreas.WATER_COLLECTOR
|| profileHideoutArea.type === HideoutAreas.MEDSTATION
)
{
profileHideoutArea.type === HideoutAreas.WATER_COLLECTOR ||
profileHideoutArea.type === HideoutAreas.MEDSTATION
) {
this.checkAndUpgradeWall(pmcData);
}
@ -261,15 +236,12 @@ export class HideoutController
* Upgrade wall status to visible in profile if medstation/water collector are both level 1
* @param pmcData Player profile
*/
protected checkAndUpgradeWall(pmcData: IPmcData): void
{
protected checkAndUpgradeWall(pmcData: IPmcData): void {
const medStation = pmcData.Hideout.Areas.find((area) => area.type === HideoutAreas.MEDSTATION);
const waterCollector = pmcData.Hideout.Areas.find((area) => area.type === HideoutAreas.WATER_COLLECTOR);
if (medStation?.level >= 1 && waterCollector?.level >= 1)
{
if (medStation?.level >= 1 && waterCollector?.level >= 1) {
const wall = pmcData.Hideout.Areas.find((area) => area.type === HideoutAreas.EMERGENCY_WALL);
if (wall?.level === 0)
{
if (wall?.level === 0) {
wall.level = 3;
}
}
@ -290,11 +262,9 @@ export class HideoutController
profileParentHideoutArea: HideoutArea,
dbHideoutArea: IHideoutArea,
hideoutStage: Stage,
): void
{
): void {
// Add key/value to `hideoutAreaStashes` dictionary - used to link hideout area to inventory stash by its id
if (!pmcData.Inventory.hideoutAreaStashes[dbHideoutArea.type])
{
if (!pmcData.Inventory.hideoutAreaStashes[dbHideoutArea.type]) {
pmcData.Inventory.hideoutAreaStashes[dbHideoutArea.type] = dbHideoutArea._id;
}
@ -302,29 +272,24 @@ export class HideoutController
this.addUpdateInventoryItemToProfile(pmcData, dbHideoutArea, hideoutStage);
// Dont inform client when upgraded area is hall of fame, BSG doesnt inform client upgrade has occurred, will break client if data is sent
if (dbHideoutArea.type !== HideoutAreas.PLACE_OF_FAME)
{
if (dbHideoutArea.type !== HideoutAreas.PLACE_OF_FAME) {
// Inform client of changes
this.addContainerUpgradeToClientOutput(output, sessionID, dbHideoutArea.type, dbHideoutArea, hideoutStage);
}
// Some areas like gun stand have a child area linked to it, it needs to do the same as above
const childDbArea = this.databaseService.getHideout().areas
.find((area) => area.parentArea === dbHideoutArea._id);
if (childDbArea)
{
const childDbArea = this.databaseService
.getHideout()
.areas.find((area) => area.parentArea === dbHideoutArea._id);
if (childDbArea) {
// Add key/value to `hideoutAreaStashes` dictionary - used to link hideout area to inventory stash by its id
if (!pmcData.Inventory.hideoutAreaStashes[childDbArea.type])
{
if (!pmcData.Inventory.hideoutAreaStashes[childDbArea.type]) {
pmcData.Inventory.hideoutAreaStashes[childDbArea.type] = childDbArea._id;
}
// Set child area level to same as parent area
pmcData.Hideout.Areas
.find((hideoutArea) => hideoutArea.type === childDbArea.type).level
= pmcData.Hideout.Areas
.find((x) => x.type === profileParentHideoutArea.type,
).level;
pmcData.Hideout.Areas.find((hideoutArea) => hideoutArea.type === childDbArea.type).level =
pmcData.Hideout.Areas.find((x) => x.type === profileParentHideoutArea.type).level;
// Add/upgrade stash item in player inventory
const childDbAreaStage = childDbArea.stages[profileParentHideoutArea.level];
@ -345,11 +310,9 @@ export class HideoutController
pmcData: IPmcData,
dbHideoutData: IHideoutArea,
hideoutStage: Stage,
): void
{
): void {
const existingInventoryItem = pmcData.Inventory.items.find((item) => item._id === dbHideoutData._id);
if (existingInventoryItem)
{
if (existingInventoryItem) {
// Update existing items container tpl to point to new id (tpl)
existingInventoryItem._tpl = hideoutStage.container;
@ -373,10 +336,8 @@ export class HideoutController
areaType: HideoutAreas,
hideoutDbData: IHideoutArea,
hideoutStage: Stage,
): void
{
if (!output.profileChanges[sessionID].changedHideoutStashes)
{
): void {
if (!output.profileChanges[sessionID].changedHideoutStashes) {
output.profileChanges[sessionID].changedHideoutStashes = {};
}
@ -398,19 +359,16 @@ export class HideoutController
pmcData: IPmcData,
addItemToHideoutRequest: IHideoutPutItemInRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionID);
const itemsToAdd = Object.entries(addItemToHideoutRequest.items).map((kvp) =>
{
const itemsToAdd = Object.entries(addItemToHideoutRequest.items).map((kvp) => {
const item = pmcData.Inventory.items.find((invItem) => invItem._id === kvp[1].id);
return { inventoryItem: item, requestedItem: kvp[1], slot: kvp[0] };
});
const hideoutArea = pmcData.Hideout.Areas.find((area) => area.type === addItemToHideoutRequest.areaType);
if (!hideoutArea)
{
if (!hideoutArea) {
this.logger.error(
this.localisationService.getText(
"hideout-unable_to_find_area_in_database",
@ -420,10 +378,8 @@ export class HideoutController
return this.httpResponse.appendErrorToOutput(output);
}
for (const item of itemsToAdd)
{
if (!item.inventoryItem)
{
for (const item of itemsToAdd) {
if (!item.inventoryItem) {
this.logger.error(
this.localisationService.getText("hideout-unable_to_find_item_in_inventory", {
itemId: item.requestedItem.id,
@ -465,19 +421,16 @@ export class HideoutController
pmcData: IPmcData,
request: IHideoutTakeItemOutRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionID);
const hideoutArea = pmcData.Hideout.Areas.find((area) => area.type === request.areaType);
if (!hideoutArea)
{
if (!hideoutArea) {
this.logger.error(this.localisationService.getText("hideout-unable_to_find_area", request.areaType));
return this.httpResponse.appendErrorToOutput(output);
}
if (!hideoutArea.slots || hideoutArea.slots.length === 0)
{
if (!hideoutArea.slots || hideoutArea.slots.length === 0) {
this.logger.error(
this.localisationService.getText("hideout-unable_to_find_item_to_remove_from_area", hideoutArea.type),
);
@ -492,8 +445,7 @@ export class HideoutController
HideoutAreas.GENERATOR,
HideoutAreas.BITCOIN_FARM,
].includes(hideoutArea.type)
)
{
) {
const response = this.removeResourceFromArea(sessionID, pmcData, request, output, hideoutArea);
// Force a refresh of productions/hideout areas with resources
@ -521,13 +473,11 @@ export class HideoutController
removeResourceRequest: IHideoutTakeItemOutRequestData,
output: IItemEventRouterResponse,
hideoutArea: HideoutArea,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const slotIndexToRemove = removeResourceRequest.slots[0];
// Assume only one item in slot
const itemToReturn = hideoutArea.slots
.find((slot) => slot.locationIndex === slotIndexToRemove).item[0];
const itemToReturn = hideoutArea.slots.find((slot) => slot.locationIndex === slotIndexToRemove).item[0];
const request: IAddItemDirectRequest = {
itemWithModsToAdd: [itemToReturn],
@ -537,8 +487,7 @@ export class HideoutController
};
this.inventoryHelper.addItemToStash(sessionID, request, pmcData, output);
if (output.warnings && output.warnings.length > 0)
{
if (output.warnings && output.warnings.length > 0) {
// Adding to stash failed, drop out - dont remove item from hideout area slot
return output;
}
@ -562,16 +511,14 @@ export class HideoutController
pmcData: IPmcData,
request: IHideoutToggleAreaRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionID);
// Force a production update (occur before area is toggled as it could be generator and doing it after generator enabled would cause incorrect calculaton of production progress)
this.hideoutHelper.updatePlayerHideout(sessionID);
const hideoutArea = pmcData.Hideout.Areas.find((area) => area.type === request.areaType);
if (!hideoutArea)
{
if (!hideoutArea) {
this.logger.error(this.localisationService.getText("hideout-unable_to_find_area", request.areaType));
return this.httpResponse.appendErrorToOutput(output);
}
@ -593,14 +540,14 @@ export class HideoutController
pmcData: IPmcData,
body: IHideoutSingleProductionStartRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
// Start production
this.hideoutHelper.registerProduction(pmcData, body, sessionID);
// Find the recipe of the production
const recipe = this.databaseService.getHideout().production
.find((production) => production._id === body.recipeId);
const recipe = this.databaseService
.getHideout()
.production.find((production) => production._id === body.recipeId);
// Find the actual amount of items we need to remove because body can send weird data
const recipeRequirementsClone = this.cloner.clone(
@ -609,8 +556,7 @@ export class HideoutController
const output = this.eventOutputHolder.getOutput(sessionID);
const itemsToDelete = body.items.concat(body.tools);
for (const itemToDelete of itemsToDelete)
{
for (const itemToDelete of itemsToDelete) {
const itemToCheck = pmcData.Inventory.items.find((i) => i._id === itemToDelete.id);
const requirement = recipeRequirementsClone.find(
(requirement) => requirement.templateId === itemToCheck._tpl,
@ -618,16 +564,14 @@ export class HideoutController
// Handle tools not having a `count`, but always only requiring 1
const requiredCount = requirement.count ?? 1;
if (requiredCount <= 0)
{
if (requiredCount <= 0) {
continue;
}
this.inventoryHelper.removeItemByCount(pmcData, itemToDelete.id, requiredCount, sessionID, output);
// Tools don't have a count
if (requirement.type !== "Tool")
{
if (requirement.type !== "Tool") {
requirement.count -= itemToDelete.count;
}
}
@ -647,15 +591,12 @@ export class HideoutController
pmcData: IPmcData,
body: IHideoutScavCaseStartRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionID);
for (const requestedItem of body.items)
{
for (const requestedItem of body.items) {
const inventoryItem = pmcData.Inventory.items.find((item) => item._id === requestedItem.id);
if (!inventoryItem)
{
if (!inventoryItem) {
this.logger.error(
this.localisationService.getText(
"hideout-unable_to_find_scavcase_requested_item_in_profile_inventory",
@ -665,20 +606,15 @@ export class HideoutController
return this.httpResponse.appendErrorToOutput(output);
}
if (inventoryItem.upd?.StackObjectsCount && inventoryItem.upd.StackObjectsCount > requestedItem.count)
{
if (inventoryItem.upd?.StackObjectsCount && inventoryItem.upd.StackObjectsCount > requestedItem.count) {
inventoryItem.upd.StackObjectsCount -= requestedItem.count;
}
else
{
} else {
this.inventoryHelper.removeItem(pmcData, requestedItem.id, sessionID, output);
}
}
const recipe = this.databaseService.getHideout().scavcase
.find((r) => r._id === body.recipeId);
if (!recipe)
{
const recipe = this.databaseService.getHideout().scavcase.find((r) => r._id === body.recipeId);
if (!recipe) {
this.logger.error(
this.localisationService.getText("hideout-unable_to_find_scav_case_recipie_in_database", body.recipeId),
);
@ -689,9 +625,9 @@ export class HideoutController
// @Important: Here we need to be very exact:
// - normal recipe: Production time value is stored in attribute "productionTime" with small "p"
// - scav case recipe: Production time value is stored in attribute "ProductionTime" with capital "P"
const adjustedCraftTime
= recipe.ProductionTime
- this.hideoutHelper.getSkillProductionTimeReduction(
const adjustedCraftTime =
recipe.ProductionTime -
this.hideoutHelper.getSkillProductionTimeReduction(
pmcData,
recipe.ProductionTime,
SkillTypes.CRAFTING,
@ -717,11 +653,9 @@ export class HideoutController
* @param productionTime Time to complete scav case in seconds
* @returns Adjusted scav case time in seconds
*/
protected getScavCaseTime(pmcData: IPmcData, productionTime: number): number
{
protected getScavCaseTime(pmcData: IPmcData, productionTime: number): number {
const fenceLevel = this.fenceService.getFenceInfo(pmcData);
if (!fenceLevel)
{
if (!fenceLevel) {
return productionTime;
}
@ -734,8 +668,7 @@ export class HideoutController
* @param rewards reward items to add to profile
* @param recipeId recipe id to save into Production dict
*/
protected addScavCaseRewardsToProfile(pmcData: IPmcData, rewards: Product[], recipeId: string): void
{
protected addScavCaseRewardsToProfile(pmcData: IPmcData, rewards: Product[], recipeId: string): void {
pmcData.Hideout.Production[`ScavCase${recipeId}`] = { Products: rewards };
}
@ -750,8 +683,7 @@ export class HideoutController
pmcData: IPmcData,
request: IHideoutContinuousProductionStartRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.hideoutHelper.registerProduction(pmcData, request, sessionID);
return this.eventOutputHolder.getOutput(sessionID);
@ -769,13 +701,11 @@ export class HideoutController
pmcData: IPmcData,
request: IHideoutTakeProductionRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionID);
const hideoutDb = this.databaseService.getHideout();
if (request.recipeId === HideoutHelper.bitcoinFarm)
{
if (request.recipeId === HideoutHelper.bitcoinFarm) {
// Ensure server and client are in-sync when player presses 'get items' on farm
this.hideoutHelper.updatePlayerHideout(sessionID);
this.hideoutHelper.getBTC(pmcData, request, sessionID, output);
@ -784,16 +714,14 @@ export class HideoutController
}
const recipe = hideoutDb.production.find((r) => r._id === request.recipeId);
if (recipe)
{
if (recipe) {
this.handleRecipe(sessionID, recipe, pmcData, request, output);
return output;
}
const scavCase = hideoutDb.scavcase.find((r) => r._id === request.recipeId);
if (scavCase)
{
if (scavCase) {
this.handleScavCase(sessionID, pmcData, request, output);
return output;
@ -823,24 +751,19 @@ export class HideoutController
pmcData: IPmcData,
request: IHideoutTakeProductionRequestData,
output: IItemEventRouterResponse,
): void
{
): void {
// Validate that we have a matching production
const productionDict = Object.entries(pmcData.Hideout.Production);
let prodId: string;
for (const [productionId, production] of productionDict)
{
for (const [productionId, production] of productionDict) {
// Skip undefined production objects
if (!production)
{
if (!production) {
continue;
}
if (this.hideoutHelper.isProductionType(production))
{
if (this.hideoutHelper.isProductionType(production)) {
// Production or ScavCase
if (production.RecipeId === request.recipeId)
{
if (production.RecipeId === request.recipeId) {
prodId = productionId; // Set to objects key
break;
}
@ -848,8 +771,7 @@ export class HideoutController
}
// If we're unable to find the production, send an error to the client
if (prodId === undefined)
{
if (prodId === undefined) {
this.logger.error(
this.localisationService.getText(
"hideout-unable_to_find_production_in_profile_by_recipie_id",
@ -879,8 +801,7 @@ export class HideoutController
// Reward is weapon/armor preset, handle differently compared to 'normal' items
const rewardIsPreset = this.presetHelper.hasPreset(recipe.endProduct);
if (rewardIsPreset)
{
if (rewardIsPreset) {
const preset = this.presetHelper.getDefaultPreset(recipe.endProduct);
// Ensure preset has unique ids and is cloned so we don't alter the preset data stored in memory
@ -893,8 +814,7 @@ export class HideoutController
}
const rewardIsStackable = this.itemHelper.isItemTplStackable(recipe.endProduct);
if (rewardIsStackable)
{
if (rewardIsStackable) {
// Create root item
const rewardToAdd: Item = {
_id: this.hashUtil.generate(),
@ -905,32 +825,26 @@ export class HideoutController
// Split item into separate items with acceptable stack sizes
const splitReward = this.itemHelper.splitStackIntoSeparateItems(rewardToAdd);
itemAndChildrenToSendToPlayer.push(...splitReward);
}
else
{
} else {
// Not stackable, may have to send send multiple of reward
// Add the first reward item to array when not a preset (first preset added above earlier)
if (!rewardIsPreset)
{
if (!rewardIsPreset) {
itemAndChildrenToSendToPlayer.push([{ _id: this.hashUtil.generate(), _tpl: recipe.endProduct }]);
}
// Add multiple of item if recipe requests it
// Start index at one so we ignore first item in array
const countOfItemsToReward = recipe.count;
for (let index = 1; index < countOfItemsToReward; index++)
{
for (let index = 1; index < countOfItemsToReward; index++) {
const itemAndMods: Item[] = this.itemHelper.replaceIDs(itemAndChildrenToSendToPlayer[0]);
itemAndChildrenToSendToPlayer.push(...[itemAndMods]);
}
}
// Recipe has an `isEncoded` requirement for reward(s), Add `RecodableComponent` property
if (recipe.isEncoded)
{
for (const reward of itemAndChildrenToSendToPlayer)
{
if (recipe.isEncoded) {
for (const reward of itemAndChildrenToSendToPlayer) {
this.itemHelper.addUpdObjectToItem(reward[0]);
reward[0].upd.RecodableComponent = { IsEncoded: true };
@ -940,18 +854,15 @@ export class HideoutController
// Build an array of the tools that need to be returned to the player
const toolsToSendToPlayer: Item[][] = [];
const production = pmcData.Hideout.Production[prodId];
if (production.sptRequiredTools?.length > 0)
{
for (const tool of production.sptRequiredTools)
{
if (production.sptRequiredTools?.length > 0) {
for (const tool of production.sptRequiredTools) {
toolsToSendToPlayer.push([tool]);
}
}
// Check if the recipe is the same as the last one - get bonus when crafting same thing multiple times
const area = pmcData.Hideout.Areas.find((area) => area.type === recipe.areaType);
if (area && request.recipeId !== area.lastRecipe)
{
if (area && request.recipeId !== area.lastRecipe) {
// 1 point per craft upon the end of production for alternating between 2 different crafting recipes in the same module
craftingExpAmount += this.hideoutConfig.expCraftAmount; // Default is 10
}
@ -959,8 +870,7 @@ export class HideoutController
// Update variable with time spent crafting item(s)
// 1 point per 8 hours of crafting
hoursCrafting += recipe.productionTime;
if (hoursCrafting / this.hideoutConfig.hoursForSkillCrafting >= 1)
{
if (hoursCrafting / this.hideoutConfig.hoursForSkillCrafting >= 1) {
// Spent enough time crafting to get a bonus xp multipler
const multiplierCrafting = Math.floor(hoursCrafting / this.hideoutConfig.hoursForSkillCrafting);
craftingExpAmount += 1 * multiplierCrafting;
@ -969,8 +879,7 @@ export class HideoutController
// Make sure we can fit both the craft result and tools in the stash
const totalResultItems = toolsToSendToPlayer.concat(itemAndChildrenToSendToPlayer);
if (!this.inventoryHelper.canPlaceItemsInInventory(sessionID, totalResultItems))
{
if (!this.inventoryHelper.canPlaceItemsInInventory(sessionID, totalResultItems)) {
this.httpResponse.appendErrorToOutput(
output,
this.localisationService.getText("inventory-no_stash_space"),
@ -980,8 +889,7 @@ export class HideoutController
}
// Add the tools to the stash, we have to do this individually due to FiR state potentially being different
for (const toolItem of toolsToSendToPlayer)
{
for (const toolItem of toolsToSendToPlayer) {
// Note: FIR state will be based on the first item's SpawnedInSession property per item group
const addToolsRequest: IAddItemsDirectRequest = {
itemsWithModsToAdd: [toolItem],
@ -991,8 +899,7 @@ export class HideoutController
};
this.inventoryHelper.addItemsToStash(sessionID, addToolsRequest, pmcData, output);
if (output.warnings.length > 0)
{
if (output.warnings.length > 0) {
return;
}
}
@ -1005,8 +912,7 @@ export class HideoutController
callback: undefined,
};
this.inventoryHelper.addItemsToStash(sessionID, addItemsRequest, pmcData, output);
if (output.warnings.length > 0)
{
if (output.warnings.length > 0) {
return;
}
@ -1023,13 +929,11 @@ export class HideoutController
);
// Add Crafting skill to player profile
if (craftingExpAmount > 0)
{
if (craftingExpAmount > 0) {
this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.CRAFTING, craftingExpAmount);
const intellectAmountToGive = 0.5 * Math.round(craftingExpAmount / 15);
if (intellectAmountToGive > 0)
{
if (intellectAmountToGive > 0) {
this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.INTELLECT, intellectAmountToGive);
}
}
@ -1043,8 +947,7 @@ export class HideoutController
pmcData.Hideout.Production[prodId].sptIsContinuous = recipe.continuous;
// Flag normal (non continious) crafts as complete
if (!recipe.continuous)
{
if (!recipe.continuous) {
pmcData.Hideout.Production[prodId].inProgress = false;
}
}
@ -1058,11 +961,9 @@ export class HideoutController
protected getHoursCraftingTaskConditionCounter(
pmcData: IPmcData,
recipe: IHideoutProduction,
): ITaskConditionCounter
{
): ITaskConditionCounter {
let counterHoursCrafting = pmcData.TaskConditionCounters[HideoutController.nameTaskConditionCountersCrafting];
if (!counterHoursCrafting)
{
if (!counterHoursCrafting) {
// Doesn't exist, create
pmcData.TaskConditionCounters[HideoutController.nameTaskConditionCountersCrafting] = {
id: recipe._id,
@ -1087,25 +988,20 @@ export class HideoutController
pmcData: IPmcData,
request: IHideoutTakeProductionRequestData,
output: IItemEventRouterResponse,
): void
{
): void {
const ongoingProductions = Object.entries(pmcData.Hideout.Production);
let prodId: string;
for (const production of ongoingProductions)
{
if (this.hideoutHelper.isProductionType(production[1]))
{
for (const production of ongoingProductions) {
if (this.hideoutHelper.isProductionType(production[1])) {
// Production or ScavCase
if ((production[1] as ScavCase).RecipeId === request.recipeId)
{
if ((production[1] as ScavCase).RecipeId === request.recipeId) {
prodId = production[0]; // Set to objects key
break;
}
}
}
if (prodId === undefined)
{
if (prodId === undefined) {
this.logger.error(
this.localisationService.getText(
"hideout-unable_to_find_production_in_profile_by_recipie_id",
@ -1129,8 +1025,7 @@ export class HideoutController
};
this.inventoryHelper.addItemsToStash(sessionID, addItemsRequest, pmcData, output);
if (output.warnings.length > 0)
{
if (output.warnings.length > 0) {
return;
}
@ -1150,8 +1045,7 @@ export class HideoutController
* @param sessionId Session id
* @returns IQteData array
*/
public getQteList(sessionId: string): IQteData[]
{
public getQteList(sessionId: string): IQteData[] {
return this.databaseService.getHideout().qte;
}
@ -1167,8 +1061,7 @@ export class HideoutController
pmcData: IPmcData,
request: IHandleQTEEventRequestData,
output: IItemEventRouterResponse,
): void
{
): void {
// {
// "Action": "HideoutQuickTimeEvent",
// "results": [true, false, true, true, true, true, true, true, true, false, false, false, false, false, false],
@ -1180,14 +1073,12 @@ export class HideoutController
// /client/hideout/workout (applyWorkoutChanges).
pmcData.Health.Energy.Current -= 50;
if (pmcData.Health.Energy.Current < 1)
{
if (pmcData.Health.Energy.Current < 1) {
pmcData.Health.Energy.Current = 1;
}
pmcData.Health.Hydration.Current -= 50;
if (pmcData.Health.Hydration.Current < 1)
{
if (pmcData.Health.Hydration.Current < 1) {
pmcData.Health.Hydration.Current = 1;
}
}
@ -1199,15 +1090,13 @@ export class HideoutController
* @param request shooting range score request
* @returns IItemEventRouterResponse
*/
public recordShootingRangePoints(sessionId: string, pmcData: IPmcData, request: IRecordShootingRangePoints): void
{
public recordShootingRangePoints(sessionId: string, pmcData: IPmcData, request: IRecordShootingRangePoints): void {
const shootingRangeKey = "ShootingRangePoints";
const overallCounterItems = pmcData.Stats.Eft.OverallCounters.Items;
// Find counter by key
let shootingRangeHighScore = overallCounterItems.find((counter) => counter.Key.includes(shootingRangeKey));
if (!shootingRangeHighScore)
{
if (!shootingRangeHighScore) {
// Counter not found, add blank one
overallCounterItems.push({ Key: [shootingRangeKey], Value: 0 });
shootingRangeHighScore = overallCounterItems.find((counter) => counter.Key.includes(shootingRangeKey));
@ -1226,22 +1115,18 @@ export class HideoutController
sessionId: string,
pmcData: IPmcData,
request: IHideoutImproveAreaRequestData,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionId);
// Create mapping of required item with corrisponding item from player inventory
const items = request.items.map((reqItem) =>
{
const items = request.items.map((reqItem) => {
const item = pmcData.Inventory.items.find((invItem) => invItem._id === reqItem.id);
return { inventoryItem: item, requestedItem: reqItem };
});
// If it's not money, its construction / barter items
for (const item of items)
{
if (!item.inventoryItem)
{
for (const item of items) {
if (!item.inventoryItem) {
this.logger.error(
this.localisationService.getText("hideout-unable_to_find_item_in_inventory", item.requestedItem.id),
);
@ -1249,31 +1134,25 @@ export class HideoutController
}
if (
this.paymentHelper.isMoneyTpl(item.inventoryItem._tpl)
&& item.inventoryItem.upd
&& item.inventoryItem.upd.StackObjectsCount
&& item.inventoryItem.upd.StackObjectsCount > item.requestedItem.count
)
{
this.paymentHelper.isMoneyTpl(item.inventoryItem._tpl) &&
item.inventoryItem.upd &&
item.inventoryItem.upd.StackObjectsCount &&
item.inventoryItem.upd.StackObjectsCount > item.requestedItem.count
) {
item.inventoryItem.upd.StackObjectsCount -= item.requestedItem.count;
}
else
{
} else {
this.inventoryHelper.removeItem(pmcData, item.inventoryItem._id, sessionId, output);
}
}
const profileHideoutArea = pmcData.Hideout.Areas.find((x) => x.type === request.areaType);
if (!profileHideoutArea)
{
if (!profileHideoutArea) {
this.logger.error(this.localisationService.getText("hideout-unable_to_find_area", request.areaType));
return this.httpResponse.appendErrorToOutput(output);
}
const hideoutDbData = this.databaseService.getHideout().areas
.find((area) => area.type === request.areaType);
if (!hideoutDbData)
{
const hideoutDbData = this.databaseService.getHideout().areas.find((area) => area.type === request.areaType);
if (!hideoutDbData) {
this.logger.error(
this.localisationService.getText("hideout-unable_to_find_area_in_database", request.areaType),
);
@ -1284,13 +1163,11 @@ export class HideoutController
const improvements = hideoutDbData.stages[profileHideoutArea.level].improvements;
const timestamp = this.timeUtil.getTimestamp();
if (!output.profileChanges[sessionId].improvements)
{
if (!output.profileChanges[sessionId].improvements) {
output.profileChanges[sessionId].improvements = {};
}
for (const improvement of improvements)
{
for (const improvement of improvements) {
const improvementDetails = {
completed: false,
improveCompleteTimestamp: timestamp + improvement.improvementTime,
@ -1313,13 +1190,11 @@ export class HideoutController
sessionId: string,
pmcData: IPmcData,
request: IHideoutCancelProductionRequestData,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionId);
const craftToCancel = pmcData.Hideout.Production[request.recipeId];
if (!craftToCancel)
{
if (!craftToCancel) {
const errorMessage = `Unable to find craft ${request.recipeId} to cancel`;
this.logger.error(errorMessage);
@ -1337,18 +1212,15 @@ export class HideoutController
/**
* Function called every x seconds as part of onUpdate event
*/
public update(): void
{
for (const sessionID in this.saveServer.getProfiles())
{
public update(): void {
for (const sessionID in this.saveServer.getProfiles()) {
if (
"Hideout" in this.saveServer.getProfile(sessionID).characters.pmc
&& this.profileActivityService.activeWithinLastMinutes(
"Hideout" in this.saveServer.getProfile(sessionID).characters.pmc &&
this.profileActivityService.activeWithinLastMinutes(
sessionID,
this.hideoutConfig.updateProfileHideoutWhenActiveWithinMinutes,
)
)
{
) {
this.hideoutHelper.updatePlayerHideout(sessionID);
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { ApplicationContext } from "@spt/context/ApplicationContext";
import { ContextVariableType } from "@spt/context/ContextVariableType";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
@ -11,13 +10,13 @@ import { ILogger } from "@spt/models/spt/utils/ILogger";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { SaveServer } from "@spt/servers/SaveServer";
import { LocalisationService } from "@spt/services/LocalisationService";
import { inject, injectable } from "tsyringe";
/**
* Logic for handling In Raid callbacks
*/
@injectable()
export class InraidController
{
export class InraidController {
protected inRaidConfig: IInRaidConfig;
protected botConfig: IBotConfig;
@ -28,8 +27,7 @@ export class InraidController
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ApplicationContext") protected applicationContext: ApplicationContext,
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
) {
this.inRaidConfig = this.configServer.getConfig(ConfigTypes.IN_RAID);
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
}
@ -39,18 +37,15 @@ export class InraidController
* @param sessionID Session id
* @param info Register player request
*/
public addPlayer(sessionID: string, info: IRegisterPlayerRequestData): void
{
public addPlayer(sessionID: string, info: IRegisterPlayerRequestData): void {
this.applicationContext.addValue(ContextVariableType.REGISTER_PLAYER_REQUEST, info);
const profile = this.saveServer.getProfile(sessionID);
if (!profile)
{
if (!profile) {
this.logger.error(this.localisationService.getText("inraid-no_profile_found", sessionID));
return;
}
if (!profile.inraid)
{
if (!profile.inraid) {
profile.inraid = { character: sessionID, location: info.locationId };
return;
@ -66,8 +61,7 @@ export class InraidController
* @param offraidData post-raid request data
* @param sessionID Session id
*/
public savePostRaidProfileForScav(offraidData: IScavSaveRequestData, sessionID: string): void
{
public savePostRaidProfileForScav(offraidData: IScavSaveRequestData, sessionID: string): void {
const serverScavProfile = this.profileHelper.getScavProfile(sessionID);
serverScavProfile.Inventory.items = offraidData.profile.Inventory.items;
@ -77,18 +71,15 @@ export class InraidController
* Get the inraid config from configs/inraid.json
* @returns InRaid Config
*/
public getInraidConfig(): IInRaidConfig
{
public getInraidConfig(): IInRaidConfig {
return this.inRaidConfig;
}
public getTraitorScavHostileChance(url: string, sessionID: string): number
{
public getTraitorScavHostileChance(url: string, sessionID: string): number {
return this.inRaidConfig.playerScavHostileChancePercent;
}
public getBossConvertSettings(url: string, sessionId: string): string[]
{
public getBossConvertSettings(url: string, sessionId: string): string[] {
return Object.keys(this.botConfig.assaultToBossConversion.bossesToConvertToWeights);
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { DialogueHelper } from "@spt/helpers/DialogueHelper";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
@ -26,15 +25,15 @@ import { LocalisationService } from "@spt/services/LocalisationService";
import { MailSendService } from "@spt/services/MailSendService";
import { PaymentService } from "@spt/services/PaymentService";
import { RagfairPriceService } from "@spt/services/RagfairPriceService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HashUtil } from "@spt/utils/HashUtil";
import { MathUtil } from "@spt/utils/MathUtil";
import { ProbabilityObject, ProbabilityObjectArray, RandomUtil } from "@spt/utils/RandomUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class InsuranceController
{
export class InsuranceController {
protected insuranceConfig: IInsuranceConfig;
constructor(
@ -58,8 +57,7 @@ export class InsuranceController
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
) {
this.insuranceConfig = this.configServer.getConfig(ConfigTypes.INSURANCE);
}
@ -68,11 +66,9 @@ export class InsuranceController
*
* @returns void
*/
public processReturn(): void
{
public processReturn(): void {
// Process each installed profile.
for (const sessionID in this.saveServer.getProfiles())
{
for (const sessionID in this.saveServer.getProfiles()) {
this.processReturnByProfile(sessionID);
}
}
@ -82,14 +78,12 @@ export class InsuranceController
*
* @returns void
*/
public processReturnByProfile(sessionID: string): void
{
public processReturnByProfile(sessionID: string): void {
// Filter out items that don't need to be processed yet.
const insuranceDetails = this.filterInsuredItems(sessionID);
// Skip profile if no insured items to process
if (insuranceDetails.length === 0)
{
if (insuranceDetails.length === 0) {
return;
}
@ -103,14 +97,12 @@ export class InsuranceController
* @param time The time to check ready status against. Current time by default.
* @returns All insured items that are ready to be processed.
*/
protected filterInsuredItems(sessionID: string, time?: number): Insurance[]
{
protected filterInsuredItems(sessionID: string, time?: number): Insurance[] {
// Use the current time by default.
const insuranceTime = time || this.timeUtil.getTimestamp();
const profileInsuranceDetails = this.saveServer.getProfile(sessionID).insurance;
if (profileInsuranceDetails.length > 0)
{
if (profileInsuranceDetails.length > 0) {
this.logger.debug(`Found ${profileInsuranceDetails.length} insurance packages in profile ${sessionID}`);
}
@ -124,8 +116,7 @@ export class InsuranceController
* @param sessionID The session ID that should receive the processed items.
* @returns void
*/
protected processInsuredItems(insuranceDetails: Insurance[], sessionID: string): void
{
protected processInsuredItems(insuranceDetails: Insurance[], sessionID: string): void {
this.logger.debug(
`Processing ${insuranceDetails.length} insurance packages, which includes a total of ${this.countAllInsuranceItems(
insuranceDetails,
@ -136,8 +127,7 @@ export class InsuranceController
const rootItemParentID = this.insuranceService.getRootItemParentID(sessionID);
// Iterate over each of the insurance packages.
for (const insured of insuranceDetails)
{
for (const insured of insuranceDetails) {
// Find items that should be deleted from the insured items.
const itemsToDelete = this.findItemsToDelete(rootItemParentID, insured);
@ -160,8 +150,7 @@ export class InsuranceController
* @param insurance
* @returns
*/
protected countAllInsuranceItems(insurance: Insurance[]): number
{
protected countAllInsuranceItems(insurance: Insurance[]): number {
return this.mathUtil.arraySum(insurance.map((ins) => ins.items.length));
}
@ -172,15 +161,14 @@ export class InsuranceController
* @param index The array index of the insurance package to remove.
* @returns void
*/
protected removeInsurancePackageFromProfile(sessionID: string, insPackage: Insurance): void
{
protected removeInsurancePackageFromProfile(sessionID: string, insPackage: Insurance): void {
const profile = this.saveServer.getProfile(sessionID);
profile.insurance = profile.insurance.filter(
(insurance) =>
insurance.traderId !== insPackage.traderId
|| insurance.systemData.date !== insPackage.systemData.date
|| insurance.systemData.time !== insPackage.systemData.time
|| insurance.systemData.location !== insPackage.systemData.location,
insurance.traderId !== insPackage.traderId ||
insurance.systemData.date !== insPackage.systemData.date ||
insurance.systemData.time !== insPackage.systemData.time ||
insurance.systemData.location !== insPackage.systemData.location,
);
this.logger.debug(`Removed processed insurance package. Remaining packages: ${profile.insurance.length}`);
@ -193,8 +181,7 @@ export class InsuranceController
* @param insured - The insurance object containing the items to evaluate for deletion.
* @returns A Set containing the IDs of items that should be deleted.
*/
protected findItemsToDelete(rootItemParentID: string, insured: Insurance): Set<string>
{
protected findItemsToDelete(rootItemParentID: string, insured: Insurance): Set<string> {
const toDelete = new Set<string>();
// Populate a Map object of items for quick lookup by their ID and use it to populate a Map of main-parent items
@ -208,14 +195,12 @@ export class InsuranceController
);
// Process all items that are not attached, attachments; those are handled separately, by value.
if (hasRegularItems)
{
if (hasRegularItems) {
this.processRegularItems(insured, toDelete, parentAttachmentsMap);
}
// Process attached, attachments, by value, only if there are any.
if (parentAttachmentsMap.size > 0)
{
if (parentAttachmentsMap.size > 0) {
// Remove attachments that can not be moddable in-raid from the parentAttachmentsMap. We only want to
// process moddable attachments from here on out.
parentAttachmentsMap = this.removeNonModdableAttachments(parentAttachmentsMap, itemsMap);
@ -224,8 +209,7 @@ export class InsuranceController
}
// Log the number of items marked for deletion, if any
if (toDelete.size)
{
if (toDelete.size) {
this.logger.debug(`Marked ${toDelete.size} items for deletion from insurance.`);
}
@ -246,65 +230,58 @@ export class InsuranceController
rootItemParentID: string,
insured: Insurance,
itemsMap: Map<string, Item>,
): Map<string, Item[]>
{
): Map<string, Item[]> {
const mainParentToAttachmentsMap = new Map<string, Item[]>();
for (const insuredItem of insured.items)
{
for (const insuredItem of insured.items) {
// Use the parent ID from the item to get the parent item.
const parentItem = insured.items.find((item) => item._id === insuredItem.parentId);
// The parent (not the hideout) could not be found. Skip and warn.
if (!parentItem && insuredItem.parentId !== rootItemParentID)
{
this.logger.warning(this.localisationService
.getText("insurance-unable_to_find_parent_of_item",
{
if (!parentItem && insuredItem.parentId !== rootItemParentID) {
this.logger.warning(
this.localisationService.getText("insurance-unable_to_find_parent_of_item", {
insuredItemId: insuredItem._id,
insuredItemTpl: insuredItem._tpl,
parentId: insuredItem.parentId,
}));
}),
);
continue;
}
// Check if this is an attachment currently attached to its parent.
if (this.itemHelper.isAttachmentAttached(insuredItem))
{
if (this.itemHelper.isAttachmentAttached(insuredItem)) {
// Make sure the template for the item exists.
if (!this.itemHelper.getItem(insuredItem._tpl)[0])
{
this.logger.warning(this.localisationService.getText("insurance-unable_to_find_attachment_in_db",
{
if (!this.itemHelper.getItem(insuredItem._tpl)[0]) {
this.logger.warning(
this.localisationService.getText("insurance-unable_to_find_attachment_in_db", {
insuredItemId: insuredItem._id,
insuredItemTpl: insuredItem._tpl,
}));
}),
);
continue;
}
// Get the main parent of this attachment. (e.g., The gun that this suppressor is attached to.)
const mainParent = this.itemHelper.getAttachmentMainParent(insuredItem._id, itemsMap);
if (!mainParent)
{
if (!mainParent) {
// Odd. The parent couldn't be found. Skip this attachment and warn.
this.logger.warning(this.localisationService.getText("insurance-unable_to_find_main_parent_for_attachment",
{
this.logger.warning(
this.localisationService.getText("insurance-unable_to_find_main_parent_for_attachment", {
insuredItemId: insuredItem._id,
insuredItemTpl: insuredItem._tpl,
parentId: insuredItem.parentId,
}));
}),
);
continue;
}
// Update (or add to) the main-parent to attachments map.
if (mainParentToAttachmentsMap.has(mainParent._id))
{
if (mainParentToAttachmentsMap.has(mainParent._id)) {
mainParentToAttachmentsMap.get(mainParent._id).push(insuredItem);
}
else
{
} else {
mainParentToAttachmentsMap.set(mainParent._id, [insuredItem]);
}
}
@ -323,40 +300,33 @@ export class InsuranceController
protected removeNonModdableAttachments(
parentAttachmentsMap: Map<string, Item[]>,
itemsMap: Map<string, Item>,
): Map<string, Item[]>
{
): Map<string, Item[]> {
const updatedMap = new Map<string, Item[]>();
for (const [parentId, attachmentItems] of parentAttachmentsMap)
{
for (const [parentId, attachmentItems] of parentAttachmentsMap) {
const parentItem = itemsMap.get(parentId);
const moddableAttachments: Item[] = [];
for (const attachment of attachmentItems)
{
for (const attachment of attachmentItems) {
// By default, assume the parent of the current attachment is the main-parent included in the map.
let attachmentParentItem = parentItem;
// If the attachment includes a parentId, use it to find its direct parent item, even if it's another
// attachment on the main-parent. For example, if the attachment is a stock, we need to check to see if
// it's moddable in the upper receiver (attachment/parent), which is attached to the gun (main-parent).
if (attachment.parentId)
{
if (attachment.parentId) {
const directParentItem = itemsMap.get(attachment.parentId);
if (directParentItem)
{
if (directParentItem) {
attachmentParentItem = directParentItem;
}
}
if (this.itemHelper.isRaidModdable(attachment, attachmentParentItem))
{
if (this.itemHelper.isRaidModdable(attachment, attachmentParentItem)) {
moddableAttachments.push(attachment);
}
}
// If any moddable attachments remain, add them to the updated map.
if (moddableAttachments.length > 0)
{
if (moddableAttachments.length > 0) {
updatedMap.set(parentId, moddableAttachments);
}
}
@ -378,40 +348,32 @@ export class InsuranceController
insured: Insurance,
toDelete: Set<string>,
parentAttachmentsMap: Map<string, Item[]>,
): void
{
for (const insuredItem of insured.items)
{
): void {
for (const insuredItem of insured.items) {
// Skip if the item is an attachment. These are handled separately.
if (this.itemHelper.isAttachmentAttached(insuredItem))
{
if (this.itemHelper.isAttachmentAttached(insuredItem)) {
continue;
}
// Roll for item deletion
const itemRoll = this.rollForDelete(insured.traderId, insuredItem);
if (itemRoll)
{
if (itemRoll) {
// Check to see if this item is a parent in the parentAttachmentsMap. If so, do a look-up for *all* of
// its children and mark them for deletion as well. Additionally remove the parent (and its children)
// from the parentAttachmentsMap so that it's children are not rolled for later in the process.
if (parentAttachmentsMap.has(insuredItem._id))
{
if (parentAttachmentsMap.has(insuredItem._id)) {
// This call will also return the parent item itself, queueing it for deletion as well.
const itemAndChildren = this.itemHelper.findAndReturnChildrenAsItems(
insured.items,
insuredItem._id,
);
for (const item of itemAndChildren)
{
for (const item of itemAndChildren) {
toDelete.add(item._id);
}
// Remove the parent (and its children) from the parentAttachmentsMap.
parentAttachmentsMap.delete(insuredItem._id);
}
else
{
} else {
// This item doesn't have any children. Simply mark it for deletion.
toDelete.add(insuredItem._id);
}
@ -432,14 +394,11 @@ export class InsuranceController
itemsMap: Map<string, Item>,
traderId: string,
toDelete: Set<string>,
): void
{
for (const [parentId, attachmentItems] of mainParentToAttachmentsMap)
{
): void {
for (const [parentId, attachmentItems] of mainParentToAttachmentsMap) {
// Skip processing if parentId is already marked for deletion, as all attachments for that parent will
// already be marked for deletion as well.
if (toDelete.has(parentId))
{
if (toDelete.has(parentId)) {
continue;
}
@ -464,8 +423,7 @@ export class InsuranceController
* @param toDelete The array that accumulates the IDs of the items to be deleted.
* @returns void
*/
protected processAttachmentByParent(attachments: Item[], traderId: string, toDelete: Set<string>): void
{
protected processAttachmentByParent(attachments: Item[], traderId: string, toDelete: Set<string>): void {
// Create dict of item ids + their flea/handbook price (highest is chosen)
const weightedAttachmentByPrice = this.weightAttachmentsByPrice(attachments);
@ -474,8 +432,7 @@ export class InsuranceController
// Create prob array and add all attachments with rouble price as the weight
const attachmentsProbabilityArray = new ProbabilityObjectArray<string, number>(this.mathUtil, this.cloner);
for (const attachmentTpl of Object.keys(weightedAttachmentByPrice))
{
for (const attachmentTpl of Object.keys(weightedAttachmentByPrice)) {
attachmentsProbabilityArray.push(
new ProbabilityObject(attachmentTpl, weightedAttachmentByPrice[attachmentTpl]),
);
@ -483,8 +440,7 @@ export class InsuranceController
// Draw x attachments from weighted array to remove from parent, remove from pool after being picked
const attachmentIdsToRemove = attachmentsProbabilityArray.draw(countOfAttachmentsToRemove, false);
for (const attachmentId of attachmentIdsToRemove)
{
for (const attachmentId of attachmentIdsToRemove) {
toDelete.add(attachmentId);
}
@ -497,11 +453,9 @@ export class InsuranceController
attachmentIdsToRemove: string[],
attachments: Item[],
attachmentPrices: Record<string, number>,
): void
{
): void {
let index = 1;
for (const attachmentId of attachmentIdsToRemove)
{
for (const attachmentId of attachmentIdsToRemove) {
this.logger.debug(
`Attachment ${index} Id: ${attachmentId} Tpl: ${
attachments.find((x) => x._id === attachmentId)?._tpl
@ -511,16 +465,13 @@ export class InsuranceController
}
}
protected weightAttachmentsByPrice(attachments: Item[]): Record<string, number>
{
protected weightAttachmentsByPrice(attachments: Item[]): Record<string, number> {
const result: Record<string, number> = {};
// Get a dictionary of item tpls + their rouble price
for (const attachment of attachments)
{
for (const attachment of attachments) {
const price = this.ragfairPriceService.getDynamicItemPrice(attachment._tpl, Money.ROUBLES);
if (price)
{
if (price) {
result[attachment._id] = Math.round(price);
}
}
@ -536,25 +487,20 @@ export class InsuranceController
* @param traderId Trader attachment insured against
* @returns Attachment count to remove
*/
protected getAttachmentCountToRemove(weightedAttachmentByPrice: Record<string, number>, traderId: string): number
{
protected getAttachmentCountToRemove(weightedAttachmentByPrice: Record<string, number>, traderId: string): number {
let removeCount = 0;
if (this.randomUtil.getChance100(this.insuranceConfig.chanceNoAttachmentsTakenPercent))
{
if (this.randomUtil.getChance100(this.insuranceConfig.chanceNoAttachmentsTakenPercent)) {
return removeCount;
}
for (const attachmentId of Object.keys(weightedAttachmentByPrice))
{
for (const attachmentId of Object.keys(weightedAttachmentByPrice)) {
// Below min price to be taken, skip
if (weightedAttachmentByPrice[attachmentId] < this.insuranceConfig.minAttachmentRoublePriceToBeTaken)
{
if (weightedAttachmentByPrice[attachmentId] < this.insuranceConfig.minAttachmentRoublePriceToBeTaken) {
continue;
}
if (this.rollForDelete(traderId))
{
if (this.rollForDelete(traderId)) {
removeCount++;
}
}
@ -569,8 +515,7 @@ export class InsuranceController
* @param toDelete The items that should be deleted.
* @returns void
*/
protected removeItemsFromInsurance(insured: Insurance, toDelete: Set<string>): void
{
protected removeItemsFromInsurance(insured: Insurance, toDelete: Set<string>): void {
insured.items = insured.items.filter((item) => !toDelete.has(item._id));
}
@ -581,8 +526,7 @@ export class InsuranceController
* @param insurance The context of insurance to use.
* @returns void
*/
protected sendMail(sessionID: string, insurance: Insurance): void
{
protected sendMail(sessionID: string, insurance: Insurance): void {
const labsId = "laboratory";
// After all of the item filtering that we've done, if there are no items remaining, the insurance has
// successfully "failed" to return anything and an appropriate message should be sent to the player.
@ -590,21 +534,17 @@ export class InsuranceController
// Map is labs + insurance is disabled in base.json
if (
insurance.systemData?.location?.toLowerCase() === labsId
&& !(this.databaseService.getLocation(labsId).base.Insurance)
)
{
insurance.systemData?.location?.toLowerCase() === labsId &&
!this.databaseService.getLocation(labsId).base.Insurance
) {
// Trader has labs-specific messages
// Wipe out returnable items
if (traderDialogMessages.insuranceFailedLabs?.length > 0)
{
if (traderDialogMessages.insuranceFailedLabs?.length > 0) {
const insuranceFailedLabTemplates = traderDialogMessages.insuranceFailedLabs;
insurance.messageTemplateId = this.randomUtil.getArrayValue(insuranceFailedLabTemplates);
insurance.items = [];
}
}
else if (insurance.items.length === 0)
{
} else if (insurance.items.length === 0) {
// Not labs and no items to return
const insuranceFailedTemplates = traderDialogMessages.insuranceFailed;
insurance.messageTemplateId = this.randomUtil.getArrayValue(insuranceFailedTemplates);
@ -630,11 +570,9 @@ export class InsuranceController
* @param insuredItem Optional. The item to roll for. Only used for logging.
* @returns true if the insured item should be removed from inventory, false otherwise, or undefined on error.
*/
protected rollForDelete(traderId: string, insuredItem?: Item): boolean | undefined
{
protected rollForDelete(traderId: string, insuredItem?: Item): boolean | undefined {
const trader = this.traderHelper.getTraderById(traderId);
if (!trader)
{
if (!trader) {
return undefined;
}
@ -664,26 +602,25 @@ export class InsuranceController
* @param sessionID Session id
* @returns IItemEventRouterResponse object to send to client
*/
public insure(pmcData: IPmcData, body: IInsureRequestData, sessionID: string): IItemEventRouterResponse
{
public insure(pmcData: IPmcData, body: IInsureRequestData, sessionID: string): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionID);
const itemsToInsureCount = body.items.length;
const itemsToPay = [];
const inventoryItemsHash = {};
// Create hash of player inventory items (keyed by item id)
for (const item of pmcData.Inventory.items)
{
for (const item of pmcData.Inventory.items) {
inventoryItemsHash[item._id] = item;
}
// Get price of all items being insured
for (const key of body.items)
{
for (const key of body.items) {
itemsToPay.push({
id: Money.ROUBLES, // TODO: update to handle different currencies
count: this.insuranceService.getRoublePriceToInsureItemWithTrader(
pmcData, inventoryItemsHash[key],
body.tid),
pmcData,
inventoryItemsHash[key],
body.tid,
),
});
}
@ -699,14 +636,12 @@ export class InsuranceController
// pay for the item insurance
this.paymentService.payMoney(pmcData, options, sessionID, output);
if (output.warnings.length > 0)
{
if (output.warnings.length > 0) {
return output;
}
// add items to InsuredItems list once money has been paid
for (const key of body.items)
{
for (const key of body.items) {
pmcData.InsuredItems.push({ tid: body.tid, itemId: inventoryItemsHash[key]._id });
}
@ -723,33 +658,30 @@ export class InsuranceController
* @param sessionID session id
* @returns IGetInsuranceCostResponseData object to send to client
*/
public cost(request: IGetInsuranceCostRequestData, sessionID: string): IGetInsuranceCostResponseData
{
public cost(request: IGetInsuranceCostRequestData, sessionID: string): IGetInsuranceCostResponseData {
const response: IGetInsuranceCostResponseData = {};
const pmcData = this.profileHelper.getPmcProfile(sessionID);
const inventoryItemsHash: Record<string, Item> = {};
for (const item of pmcData.Inventory.items)
{
for (const item of pmcData.Inventory.items) {
inventoryItemsHash[item._id] = item;
}
// Loop over each trader in request
for (const trader of request.traders)
{
for (const trader of request.traders) {
const items: Record<string, number> = {};
for (const itemId of request.items)
{
for (const itemId of request.items) {
// Ensure hash has item in it
if (!inventoryItemsHash[itemId])
{
if (!inventoryItemsHash[itemId]) {
this.logger.debug(`Item with id: ${itemId} missing from player inventory, skipping`);
continue;
}
items[inventoryItemsHash[itemId]._tpl] = this.insuranceService.getRoublePriceToInsureItemWithTrader(
pmcData, inventoryItemsHash[itemId],
trader);
pmcData,
inventoryItemsHash[itemId],
trader,
);
}
response[trader] = items;
@ -760,8 +692,7 @@ export class InsuranceController
}
// Represents an insurance item that has had it's common locale-name and value added to it.
interface EnrichedItem extends Item
{
name: string
dynamicPrice: number
interface EnrichedItem extends Item {
name: string;
dynamicPrice: number;
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { LootGenerator } from "@spt/generators/LootGenerator";
import { HideoutHelper } from "@spt/helpers/HideoutHelper";
import { InventoryHelper } from "@spt/helpers/InventoryHelper";
@ -44,14 +43,14 @@ import { LocalisationService } from "@spt/services/LocalisationService";
import { MapMarkerService } from "@spt/services/MapMarkerService";
import { PlayerService } from "@spt/services/PlayerService";
import { RagfairOfferService } from "@spt/services/RagfairOfferService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HashUtil } from "@spt/utils/HashUtil";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class InventoryController
{
export class InventoryController {
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("HashUtil") protected hashUtil: HashUtil,
@ -74,8 +73,7 @@ export class InventoryController
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
@inject("HttpResponseUtil") protected httpResponseUtil: HttpResponseUtil,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{}
) {}
/**
* Move Item
@ -92,28 +90,23 @@ export class InventoryController
moveRequest: IInventoryMoveRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): void
{
if (output.warnings.length > 0)
{
): void {
if (output.warnings.length > 0) {
return;
}
// Changes made to result apply to character inventory
const ownerInventoryItems = this.inventoryHelper.getOwnerInventoryItems(moveRequest, sessionID);
if (ownerInventoryItems.sameInventory)
{
if (ownerInventoryItems.sameInventory) {
// Dont move items from trader to profile, this can happen when editing a traders preset weapons
if (moveRequest.fromOwner?.type === "Trader" && !ownerInventoryItems.isMail)
{
if (moveRequest.fromOwner?.type === "Trader" && !ownerInventoryItems.isMail) {
this.appendTraderExploitErrorResponse(output);
return;
}
// Check for item in inventory before allowing internal transfer
const originalItemLocation = ownerInventoryItems.from.find((item) => item._id === moveRequest.item);
if (!originalItemLocation)
{
if (!originalItemLocation) {
// Internal item move but item never existed, possible dupe glitch
this.appendTraderExploitErrorResponse(output);
return;
@ -122,20 +115,16 @@ export class InventoryController
const originalLocationSlotId = originalItemLocation?.slotId;
const moveResult = this.inventoryHelper.moveItemInternal(pmcData, ownerInventoryItems.from, moveRequest);
if (!moveResult.success)
{
if (!moveResult.success) {
this.httpResponseUtil.appendErrorToOutput(output, moveResult.errorMessage);
return;
}
// Item is moving into or out of place of fame dogtag slot
if (moveRequest.to.container.startsWith("dogtag") || originalLocationSlotId?.startsWith("dogtag"))
{
if (moveRequest.to.container.startsWith("dogtag") || originalLocationSlotId?.startsWith("dogtag")) {
this.hideoutHelper.applyPlaceOfFameDogtagBonus(pmcData);
}
}
else
{
} else {
this.inventoryHelper.moveItemToProfile(ownerInventoryItems.from, ownerInventoryItems.to, moveRequest);
}
}
@ -145,8 +134,7 @@ export class InventoryController
* @param output Item event router response
* @returns Item event router response
*/
protected appendTraderExploitErrorResponse(output: IItemEventRouterResponse): void
{
protected appendTraderExploitErrorResponse(output: IItemEventRouterResponse): void {
this.httpResponseUtil.appendErrorToOutput(
output,
this.localisationService.getText("inventory-edit_trader_item"),
@ -164,17 +152,15 @@ export class InventoryController
request: IInventoryRemoveRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): void
{
if (request.fromOwner?.type === "Mail")
{
): void {
if (request.fromOwner?.type === "Mail") {
this.inventoryHelper.removeItemAndChildrenFromMailRewards(sessionID, request, output);
return;
}
const profileToRemoveItemFrom
= !request.fromOwner || request.fromOwner.id === pmcData._id
const profileToRemoveItemFrom =
!request.fromOwner || request.fromOwner.id === pmcData._id
? pmcData
: this.profileHelper.getFullProfile(sessionID).characters.scav;
@ -195,22 +181,19 @@ export class InventoryController
request: IInventorySplitRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
// Changes made to result apply to character inventory
const inventoryItems = this.inventoryHelper.getOwnerInventoryItems(request, sessionID);
// Handle cartridge edge-case
if (!request.container.location && request.container.container === "cartridges")
{
if (!request.container.location && request.container.container === "cartridges") {
const matchingItems = inventoryItems.to.filter((x) => x.parentId === request.container.id);
request.container.location = matchingItems.length; // Wrong location for first cartridge
}
// The item being merged has three possible sources: pmc, scav or mail, getOwnerInventoryItems() handles getting correct one
const itemToSplit = inventoryItems.from.find((x) => x._id === request.splitItem);
if (!itemToSplit)
{
if (!itemToSplit) {
const errorMessage = `Unable to split stack as source item: ${request.splitItem} cannot be found`;
this.logger.error(errorMessage);
@ -258,15 +241,13 @@ export class InventoryController
body: IInventoryMergeRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
// Changes made to result apply to character inventory
const inventoryItems = this.inventoryHelper.getOwnerInventoryItems(body, sessionID);
// Get source item (can be from player or trader or mail)
const sourceItem = inventoryItems.from.find((x) => x._id === body.item);
if (!sourceItem)
{
if (!sourceItem) {
const errorMessage = `Unable to merge stacks as source item: ${body.with} cannot be found`;
this.logger.error(errorMessage);
@ -277,8 +258,7 @@ export class InventoryController
// Get item being merged into
const destinationItem = inventoryItems.to.find((x) => x._id === body.with);
if (!destinationItem)
{
if (!destinationItem) {
const errorMessage = `Unable to merge stacks as destination item: ${body.with} cannot be found`;
this.logger.error(errorMessage);
@ -287,25 +267,20 @@ export class InventoryController
return output;
}
if (!destinationItem.upd?.StackObjectsCount)
{
if (!destinationItem.upd?.StackObjectsCount) {
// No stackcount on destination, add one
destinationItem.upd = { StackObjectsCount: 1 };
}
if (!sourceItem.upd)
{
if (!sourceItem.upd) {
sourceItem.upd = { StackObjectsCount: 1 };
}
else if (!sourceItem.upd.StackObjectsCount)
{
} else if (!sourceItem.upd.StackObjectsCount) {
// Items pulled out of raid can have no stackcount if the stack should be 1
sourceItem.upd.StackObjectsCount = 1;
}
// Remove FiR status from destination stack when source stack has no FiR but destination does
if (!sourceItem.upd.SpawnedInSession && destinationItem.upd.SpawnedInSession)
{
if (!sourceItem.upd.SpawnedInSession && destinationItem.upd.SpawnedInSession) {
delete destinationItem.upd.SpawnedInSession;
}
@ -313,8 +288,7 @@ export class InventoryController
output.profileChanges[sessionID].items.del.push({ _id: sourceItem._id }); // Inform client source item being deleted
const indexOfItemToRemove = inventoryItems.from.findIndex((x) => x._id === sourceItem._id);
if (indexOfItemToRemove === -1)
{
if (indexOfItemToRemove === -1) {
const errorMessage = `Unable to find item: ${sourceItem._id} to remove from sender inventory`;
this.logger.error(errorMessage);
@ -342,14 +316,12 @@ export class InventoryController
body: IInventoryTransferRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const inventoryItems = this.inventoryHelper.getOwnerInventoryItems(body, sessionID);
const sourceItem = inventoryItems.from.find((item) => item._id === body.item);
const destinationItem = inventoryItems.to.find((item) => item._id === body.with);
if (!sourceItem)
{
if (!sourceItem) {
const errorMessage = `Unable to transfer stack, cannot find source: ${body.item}`;
this.logger.error(errorMessage);
@ -358,8 +330,7 @@ export class InventoryController
return output;
}
if (!destinationItem)
{
if (!destinationItem) {
const errorMessage = `Unable to transfer stack, cannot find destination: ${body.with} `;
this.logger.error(errorMessage);
@ -368,25 +339,20 @@ export class InventoryController
return output;
}
if (!sourceItem.upd)
{
if (!sourceItem.upd) {
sourceItem.upd = { StackObjectsCount: 1 };
}
const sourceStackCount = sourceItem.upd.StackObjectsCount;
if (sourceStackCount > body.count)
{
if (sourceStackCount > body.count) {
// Source items stack count greater than new desired count
sourceItem.upd.StackObjectsCount = sourceStackCount - body.count;
}
else
{
} else {
// Moving a full stack onto a smaller stack
sourceItem.upd.StackObjectsCount = sourceStackCount - 1;
}
if (!destinationItem.upd)
{
if (!destinationItem.upd) {
destinationItem.upd = { StackObjectsCount: 1 };
}
const destinationStackCount = destinationItem.upd.StackObjectsCount;
@ -404,25 +370,31 @@ export class InventoryController
pmcData: IPmcData,
request: IInventorySwapRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
// During post-raid scav transfer, the swap may be in the scav inventory
let playerData = pmcData;
if (request.fromOwner?.type === "Profile" && request.fromOwner.id !== playerData._id)
{
if (request.fromOwner?.type === "Profile" && request.fromOwner.id !== playerData._id) {
playerData = this.profileHelper.getScavProfile(sessionID);
}
const itemOne = playerData.Inventory.items.find((x) => x._id === request.item);
if (!itemOne)
{
this.logger.error(this.localisationService.getText("inventory-unable_to_find_item_to_swap", { item1Id: request.item, item2Id: request.item2 }));
if (!itemOne) {
this.logger.error(
this.localisationService.getText("inventory-unable_to_find_item_to_swap", {
item1Id: request.item,
item2Id: request.item2,
}),
);
}
const itemTwo = playerData.Inventory.items.find((x) => x._id === request.item2);
if (!itemTwo)
{
this.logger.error(this.localisationService.getText("inventory-unable_to_find_item_to_swap", { item1Id: request.item2, item2Id: request.item }));
if (!itemTwo) {
this.logger.error(
this.localisationService.getText("inventory-unable_to_find_item_to_swap", {
item1Id: request.item2,
item2Id: request.item,
}),
);
}
// to.id is the parentid
@ -432,23 +404,17 @@ export class InventoryController
itemOne.slotId = request.to.container;
// Request object has location data, add it in, otherwise remove existing location from object
if (request.to.location)
{
if (request.to.location) {
itemOne.location = request.to.location;
}
else
{
} else {
delete itemOne.location;
}
itemTwo.parentId = request.to2.id;
itemTwo.slotId = request.to2.container;
if (request.to2.location)
{
if (request.to2.location) {
itemTwo.location = request.to2.location;
}
else
{
} else {
delete itemTwo.location;
}
@ -463,22 +429,21 @@ export class InventoryController
pmcData: IPmcData,
request: IInventoryFoldRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
// May need to reassign to scav profile
let playerData = pmcData;
// We may be folding data on scav profile, get that profile instead
if (request.fromOwner?.type === "Profile" && request.fromOwner.id !== playerData._id)
{
if (request.fromOwner?.type === "Profile" && request.fromOwner.id !== playerData._id) {
playerData = this.profileHelper.getScavProfile(sessionID);
}
const itemToFold = playerData.Inventory.items.find((item) => item?._id === request.item);
if (!itemToFold)
{
if (!itemToFold) {
// Item not found
this.logger.warning(this.localisationService.getText("inventory-unable_to_fold_item_not_found_in_inventory", request.item));
this.logger.warning(
this.localisationService.getText("inventory-unable_to_fold_item_not_found_in_inventory", request.item),
);
return { warnings: [], profileChanges: {} };
}
@ -502,20 +467,17 @@ export class InventoryController
pmcData: IPmcData,
body: IInventoryToggleRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
// May need to reassign to scav profile
let playerData = pmcData;
// Fix for toggling items while on they're in the Scav inventory
if (body.fromOwner?.type === "Profile" && body.fromOwner.id !== playerData._id)
{
if (body.fromOwner?.type === "Profile" && body.fromOwner.id !== playerData._id) {
playerData = this.profileHelper.getScavProfile(sessionID);
}
const itemToToggle = playerData.Inventory.items.find((x) => x._id === body.item);
if (itemToToggle)
{
if (itemToToggle) {
this.itemHelper.addUpdObjectToItem(
itemToToggle,
this.localisationService.getText("inventory-item_to_toggle_missing_upd", itemToToggle._id),
@ -538,16 +500,13 @@ export class InventoryController
* @param sessionID session id
* @returns client response object
*/
public tagItem(pmcData: IPmcData, body: IInventoryTagRequestData, sessionID: string): IItemEventRouterResponse
{
public tagItem(pmcData: IPmcData, body: IInventoryTagRequestData, sessionID: string): IItemEventRouterResponse {
const itemToTag = pmcData.Inventory.items.find((item) => item._id === body.item);
if (!itemToTag)
{
if (!itemToTag) {
return { warnings: [], profileChanges: {} };
}
if (!itemToTag.upd)
{
if (!itemToTag.upd) {
itemToTag.upd = {};
}
@ -564,14 +523,11 @@ export class InventoryController
* @param sessionID Session id
* @returns IItemEventRouterResponse
*/
public bindItem(pmcData: IPmcData, bindRequest: IInventoryBindRequestData, sessionID: string): void
{
public bindItem(pmcData: IPmcData, bindRequest: IInventoryBindRequestData, sessionID: string): void {
// TODO - replace with single .find() call
for (const index in pmcData.Inventory.fastPanel)
{
for (const index in pmcData.Inventory.fastPanel) {
// Find item with existing item in it and remove existing binding, you cant have same item bound to more than 1 slot
if (pmcData.Inventory.fastPanel[index] === bindRequest.item)
{
if (pmcData.Inventory.fastPanel[index] === bindRequest.item) {
pmcData.Inventory.fastPanel[index] = "";
break;
@ -595,8 +551,7 @@ export class InventoryController
request: IInventoryBindRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): void
{
): void {
// Remove kvp from requested fast panel index
delete pmcData.Inventory.fastPanel[request.index];
}
@ -614,48 +569,37 @@ export class InventoryController
body: IInventoryExamineRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
let itemId = "";
if ("fromOwner" in body)
{
try
{
if ("fromOwner" in body) {
try {
itemId = this.getExaminedItemTpl(body);
}
catch
{
} catch {
this.logger.error(this.localisationService.getText("inventory-examine_item_does_not_exist", body.item));
}
// get hideout item
if (body.fromOwner.type === "HideoutProduction")
{
if (body.fromOwner.type === "HideoutProduction") {
itemId = body.item;
}
}
if (!itemId)
{
if (!itemId) {
// item template
if (body.item in this.databaseService.getItems())
{
if (body.item in this.databaseService.getItems()) {
itemId = body.item;
}
}
if (!itemId)
{
if (!itemId) {
// Player inventory
const target = pmcData.Inventory.items.find((item) => item._id === body.item);
if (target)
{
if (target) {
itemId = target._tpl;
}
}
if (itemId)
{
if (itemId) {
const fullProfile = this.profileHelper.getFullProfile(sessionID);
this.flagItemsAsInspectedAndRewardXp([itemId], fullProfile);
}
@ -668,14 +612,13 @@ export class InventoryController
* @param itemTpls Inspected item tpls
* @param fullProfile Profile to add xp to
*/
protected flagItemsAsInspectedAndRewardXp(itemTpls: string[], fullProfile: ISptProfile): void
{
for (const itemTpl of itemTpls)
{
protected flagItemsAsInspectedAndRewardXp(itemTpls: string[], fullProfile: ISptProfile): void {
for (const itemTpl of itemTpls) {
const item = this.itemHelper.getItem(itemTpl);
if (!item[0])
{
this.logger.warning(this.localisationService.getText("inventory-unable_to_inspect_item_not_in_db", itemTpl));
if (!item[0]) {
this.logger.warning(
this.localisationService.getText("inventory-unable_to_inspect_item_not_in_db", itemTpl),
);
return;
}
@ -700,47 +643,40 @@ export class InventoryController
* @param request Response request
* @returns tplId
*/
protected getExaminedItemTpl(request: IInventoryExamineRequestData): string
{
if (this.presetHelper.isPreset(request.item))
{
protected getExaminedItemTpl(request: IInventoryExamineRequestData): string {
if (this.presetHelper.isPreset(request.item)) {
return this.presetHelper.getBaseItemTpl(request.item);
}
if (request.fromOwner.id === Traders.FENCE)
{
if (request.fromOwner.id === Traders.FENCE) {
// Get tpl from fence assorts
return this.fenceService.getRawFenceAssorts().items.find((x) => x._id === request.item)?._tpl;
}
if (request.fromOwner.type === "Trader")
{
if (request.fromOwner.type === "Trader") {
// Not fence
// get tpl from trader assort
return this.databaseService.getTrader(request.fromOwner.id).assort.items
.find((item) => item._id === request.item)?._tpl;
return this.databaseService
.getTrader(request.fromOwner.id)
.assort.items.find((item) => item._id === request.item)?._tpl;
}
if (request.fromOwner.type === "RagFair")
{
if (request.fromOwner.type === "RagFair") {
// Try to get tplid from items.json first
const item = this.itemHelper.getItem(request.item);
if (item[0])
{
if (item[0]) {
return item[1]._id;
}
// Try alternate way of getting offer if first approach fails
let offer = this.ragfairOfferService.getOfferByOfferId(request.item);
if (!offer)
{
if (!offer) {
offer = this.ragfairOfferService.getOfferByOfferId(request.fromOwner.id);
}
// Try find examine item inside offer items array
const matchingItem = offer.items.find((offerItem) => offerItem._id === request.item);
if (matchingItem)
{
if (matchingItem) {
return matchingItem._tpl;
}
@ -753,10 +689,8 @@ export class InventoryController
pmcData: IPmcData,
body: IInventoryReadEncyclopediaRequestData,
sessionID: string,
): IItemEventRouterResponse
{
for (const id of body.ids)
{
): IItemEventRouterResponse {
for (const id of body.ids) {
pmcData.Encyclopedia[id] = true;
}
@ -770,26 +704,22 @@ export class InventoryController
* @param request sort request
* @param sessionID Session id
*/
public sortInventory(pmcData: IPmcData, request: IInventorySortRequestData, sessionID: string): void
{
for (const change of request.changedItems)
{
public sortInventory(pmcData: IPmcData, request: IInventorySortRequestData, sessionID: string): void {
for (const change of request.changedItems) {
const inventoryItem = pmcData.Inventory.items.find((item) => item._id === change._id);
if (!inventoryItem)
{
this.logger.error(this.localisationService.getText("inventory-unable_to_sort_inventory_restart_game", change._id));
if (!inventoryItem) {
this.logger.error(
this.localisationService.getText("inventory-unable_to_sort_inventory_restart_game", change._id),
);
continue;
}
inventoryItem.parentId = change.parentId;
inventoryItem.slotId = change.slotId;
if (change.location)
{
if (change.location) {
inventoryItem.location = change.location;
}
else
{
} else {
delete inventoryItem.location;
}
}
@ -808,8 +738,7 @@ export class InventoryController
request: IInventoryCreateMarkerRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): void
{
): void {
const adjustedMapItem = this.mapMarkerService.createMarkerOnMap(pmcData, request);
// Sync with client
@ -828,8 +757,7 @@ export class InventoryController
request: IInventoryDeleteMarkerRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): void
{
): void {
const mapItem = this.mapMarkerService.deleteMarkerFromMap(pmcData, request);
// sync with client
@ -848,8 +776,7 @@ export class InventoryController
request: IInventoryEditMarkerRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): void
{
): void {
const mapItem = this.mapMarkerService.editMarkerOnMap(pmcData, request);
// sync with client
@ -869,8 +796,7 @@ export class InventoryController
body: IOpenRandomLootContainerRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): void
{
): void {
/** Container player opened in their inventory */
const openedItem = pmcData.Inventory.items.find((item) => item._id === body.item);
const containerDetailsDb = this.itemHelper.getItem(openedItem._tpl);
@ -878,37 +804,32 @@ export class InventoryController
let foundInRaid = openedItem.upd?.SpawnedInSession;
const rewards: Item[][] = [];
const unlockedWeaponCrates = ["665829424de4820934746ce6", "665732e7ac60f009f270d1ef", "665888282c4a1b73af576b77"];// Temp fix for unlocked weapon crate hideout craft
if (isSealedWeaponBox || unlockedWeaponCrates.includes(containerDetailsDb[1]._id))
{
const unlockedWeaponCrates = [
"665829424de4820934746ce6",
"665732e7ac60f009f270d1ef",
"665888282c4a1b73af576b77",
]; // Temp fix for unlocked weapon crate hideout craft
if (isSealedWeaponBox || unlockedWeaponCrates.includes(containerDetailsDb[1]._id)) {
const containerSettings = this.inventoryHelper.getInventoryConfig().sealedAirdropContainer;
rewards.push(...this.lootGenerator.getSealedWeaponCaseLoot(containerSettings));
if (containerSettings.foundInRaid)
{
if (containerSettings.foundInRaid) {
foundInRaid = containerSettings.foundInRaid;
}
}
else
{
} else {
const rewardContainerDetails = this.inventoryHelper.getRandomLootContainerRewardDetails(openedItem._tpl);
if (!rewardContainerDetails || !rewardContainerDetails.rewardCount)
{
if (!rewardContainerDetails || !rewardContainerDetails.rewardCount) {
this.logger.error(`Unable to add loot to container: ${openedItem._tpl}, no rewards found`);
}
else
{
} else {
rewards.push(...this.lootGenerator.getRandomLootContainerLoot(rewardContainerDetails));
if (rewardContainerDetails.foundInRaid)
{
if (rewardContainerDetails.foundInRaid) {
foundInRaid = rewardContainerDetails.foundInRaid;
}
}
}
if (rewards.length > 0)
{
if (rewards.length > 0) {
const addItemsRequest: IAddItemsDirectRequest = {
itemsWithModsToAdd: rewards,
foundInRaid: foundInRaid,
@ -916,8 +837,7 @@ export class InventoryController
useSortingTable: true,
};
this.inventoryHelper.addItemsToStash(sessionID, addItemsRequest, pmcData, output);
if (output.warnings.length > 0)
{
if (output.warnings.length > 0) {
return;
}
}
@ -926,19 +846,16 @@ export class InventoryController
this.inventoryHelper.removeItem(pmcData, body.item, sessionID, output);
}
public redeemProfileReward(pmcData: IPmcData, request: IRedeemProfileRequestData, sessionId: string): void
{
public redeemProfileReward(pmcData: IPmcData, request: IRedeemProfileRequestData, sessionId: string): void {
const fullProfile = this.profileHelper.getFullProfile(sessionId);
for (const event of request.events)
{
for (const event of request.events) {
// Hard coded to `SYSTEM` for now
// TODO: make this dynamic
const dialog = fullProfile.dialogues["59e7125688a45068a6249071"];
const mail = dialog.messages.find((message) => message._id === event.MessageId);
const mailEvent = mail.profileChangeEvents.find((changeEvent) => changeEvent._id === event.EventId);
switch (mailEvent.Type)
{
switch (mailEvent.Type) {
case "TraderSalesSum":
pmcData.TradersInfo[mailEvent.entity].salesSum = mailEvent.value;
this.traderHelper.lvlUp(mailEvent.entity, pmcData);
@ -955,11 +872,9 @@ export class InventoryController
this.traderHelper.validateTraderStandingsAndPlayerLevelForProfile(sessionId);
this.logger.success(`Set profile xp to: ${mailEvent.value}`);
break;
case "SkillPoints":
{
case "SkillPoints": {
const profileSkill = pmcData.Skills.Common.find((x) => x.Id === mailEvent.entity);
if (!profileSkill)
{
if (!profileSkill) {
this.logger.warning(`Unable to find skill with name: ${mailEvent.entity}`);
continue;
}
@ -967,8 +882,7 @@ export class InventoryController
this.logger.success(`Set profile skill: ${mailEvent.entity} to: ${mailEvent.value}`);
break;
}
case "ExamineAllItems":
{
case "ExamineAllItems": {
const itemsToInspect = this.itemHelper.getItems().filter((x) => x._type !== "Node");
this.flagItemsAsInspectedAndRewardXp(
itemsToInspect.map((x) => x._id),
@ -984,8 +898,7 @@ export class InventoryController
break;
case "AssortmentUnlockRule":
if (!fullProfile.spt.blacklistedItemTpls)
{
if (!fullProfile.spt.blacklistedItemTpls) {
fullProfile.spt.blacklistedItemTpls = [];
}
fullProfile.spt.blacklistedItemTpls.push(mailEvent.entity);
@ -996,11 +909,9 @@ export class InventoryController
const areaName = mailEvent.entity;
const newValue = mailEvent.value;
const hideoutAreaCode = HideoutAreas[areaName.toUpperCase()];
if (hideoutAreaCode !== undefined)
{
if (hideoutAreaCode !== undefined) {
const desiredArea = pmcData.Hideout.Areas.find((area) => area.type === hideoutAreaCode);
if (desiredArea)
{
if (desiredArea) {
desiredArea.level = newValue;
}
}
@ -1014,23 +925,17 @@ export class InventoryController
}
}
public setFavoriteItem(pmcData: IPmcData, request: ISetFavoriteItems, sessionId: string): void
{
if (!pmcData.Inventory.favoriteItems)
{
public setFavoriteItem(pmcData: IPmcData, request: ISetFavoriteItems, sessionId: string): void {
if (!pmcData.Inventory.favoriteItems) {
pmcData.Inventory.favoriteItems = [];
}
for (const itemId of request.items)
{
for (const itemId of request.items) {
// If id already exists in array, we're removing it
const indexOfItemAlreadyFavorited = pmcData.Inventory.favoriteItems.findIndex((x) => x === itemId);
if (indexOfItemAlreadyFavorited > -1)
{
if (indexOfItemAlreadyFavorited > -1) {
pmcData.Inventory.favoriteItems.splice(indexOfItemAlreadyFavorited, 1);
}
else
{
} else {
pmcData.Inventory.favoriteItems.push(itemId);
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { HttpServerHelper } from "@spt/helpers/HttpServerHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { PreSptModLoader } from "@spt/loaders/PreSptModLoader";
@ -18,10 +17,10 @@ import { LocalisationService } from "@spt/services/LocalisationService";
import { HashUtil } from "@spt/utils/HashUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class LauncherController
{
export class LauncherController {
protected coreConfig: ICoreConfig;
constructor(
@ -36,16 +35,15 @@ export class LauncherController
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("PreSptModLoader") protected preSptModLoader: PreSptModLoader,
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
) {
this.coreConfig = this.configServer.getConfig(ConfigTypes.CORE);
}
public connect(): IConnectResponse
{
public connect(): IConnectResponse {
// Get all possible profile types + filter out any that are blacklisted
const profileKeys = Object.keys(this.databaseService.getProfiles())
.filter((key) => !this.coreConfig.features.createNewProfileTypesBlacklist.includes(key));
const profileKeys = Object.keys(this.databaseService.getProfiles()).filter(
(key) => !this.coreConfig.features.createNewProfileTypesBlacklist.includes(key),
);
return {
backendUrl: this.httpServerHelper.getBackendUrl(),
@ -59,15 +57,12 @@ export class LauncherController
* Get descriptive text for each of the profile edtions a player can choose, keyed by profile.json profile type e.g. "Edge Of Darkness"
* @returns Dictionary of profile types with related descriptive text
*/
protected getProfileDescriptions(): Record<string, string>
{
protected getProfileDescriptions(): Record<string, string> {
const result = {};
const dbProfiles = this.databaseService.getProfiles();
for (const profileKey in dbProfiles)
{
for (const profileKey in dbProfiles) {
const localeKey = dbProfiles[profileKey]?.descriptionLocaleKey;
if (!localeKey)
{
if (!localeKey) {
this.logger.warning(this.localisationService.getText("launcher-missing_property", profileKey));
continue;
}
@ -78,18 +73,14 @@ export class LauncherController
return result;
}
public find(sessionId: string): Info
{
public find(sessionId: string): Info {
return this.saveServer.getProfiles()[sessionId]?.info;
}
public login(info: ILoginRequestData): string
{
for (const sessionID in this.saveServer.getProfiles())
{
public login(info: ILoginRequestData): string {
for (const sessionID in this.saveServer.getProfiles()) {
const account = this.saveServer.getProfile(sessionID).info;
if (info.username === account.username)
{
if (info.username === account.username) {
return sessionID;
}
}
@ -97,12 +88,9 @@ export class LauncherController
return "";
}
public register(info: IRegisterData): string
{
for (const sessionID in this.saveServer.getProfiles())
{
if (info.username === this.saveServer.getProfile(sessionID).info.username)
{
public register(info: IRegisterData): string {
for (const sessionID in this.saveServer.getProfiles()) {
if (info.username === this.saveServer.getProfile(sessionID).info.username) {
return "";
}
}
@ -110,8 +98,7 @@ export class LauncherController
return this.createAccount(info);
}
protected createAccount(info: IRegisterData): string
{
protected createAccount(info: IRegisterData): string {
const profileId = this.generateProfileId();
const scavId = this.generateProfileId();
const newProfileDetails: Info = {
@ -131,39 +118,33 @@ export class LauncherController
return profileId;
}
protected generateProfileId(): string
{
protected generateProfileId(): string {
const timestamp = this.timeUtil.getTimestamp();
return this.formatID(timestamp, timestamp * this.randomUtil.getInt(1, 1000000));
}
protected formatID(timeStamp: number, counter: number): string
{
protected formatID(timeStamp: number, counter: number): string {
const timeStampStr = timeStamp.toString(16).padStart(8, "0");
const counterStr = counter.toString(16).padStart(16, "0");
return timeStampStr.toLowerCase() + counterStr.toLowerCase();
}
public changeUsername(info: IChangeRequestData): string
{
public changeUsername(info: IChangeRequestData): string {
const sessionID = this.login(info);
if (sessionID)
{
if (sessionID) {
this.saveServer.getProfile(sessionID).info.username = info.change;
}
return sessionID;
}
public changePassword(info: IChangeRequestData): string
{
public changePassword(info: IChangeRequestData): string {
const sessionID = this.login(info);
if (sessionID)
{
if (sessionID) {
this.saveServer.getProfile(sessionID).info.password = info.change;
}
@ -175,17 +156,14 @@ export class LauncherController
* @param info IRegisterData
* @returns Session id
*/
public wipe(info: IRegisterData): string
{
if (!this.coreConfig.allowProfileWipe)
{
public wipe(info: IRegisterData): string {
if (!this.coreConfig.allowProfileWipe) {
return;
}
const sessionID = this.login(info);
if (sessionID)
{
if (sessionID) {
const profile = this.saveServer.getProfile(sessionID);
profile.info.edition = info.edition;
profile.info.wipe = true;
@ -194,8 +172,7 @@ export class LauncherController
return sessionID;
}
public getCompatibleTarkovVersion(): string
{
public getCompatibleTarkovVersion(): string {
return this.coreConfig.compatibleTarkovVersion;
}
@ -203,8 +180,7 @@ export class LauncherController
* Get the mods the server has currently loaded
* @returns Dictionary of mod name and mod details
*/
public getLoadedServerMods(): Record<string, IPackageJsonData>
{
public getLoadedServerMods(): Record<string, IPackageJsonData> {
return this.preSptModLoader.getImportedModDetails();
}
@ -213,12 +189,10 @@ export class LauncherController
* @param sessionId Player id
* @returns Array of mod details
*/
public getServerModsProfileUsed(sessionId: string): ModDetails[]
{
public getServerModsProfileUsed(sessionId: string): ModDetails[] {
const profile = this.profileHelper.getFullProfile(sessionId);
if (profile?.spt?.mods)
{
if (profile?.spt?.mods) {
return this.preSptModLoader.getProfileModsGroupedByModName(profile?.spt?.mods);
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { ILocationsGenerateAllResponse } from "@spt/models/eft/common/ILocationsSourceDestinationBase";
import { IGetAirdropLootResponse } from "@spt/models/eft/location/IGetAirdropLootResponse";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
@ -9,10 +8,10 @@ import { ConfigServer } from "@spt/servers/ConfigServer";
import { AirdropService } from "@spt/services/AirdropService";
import { DatabaseService } from "@spt/services/DatabaseService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class LocationController
{
export class LocationController {
protected locationConfig: ILocationConfig;
constructor(
@ -21,8 +20,7 @@ export class LocationController
@inject("AirdropService") protected airdropService: AirdropService,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
) {
this.locationConfig = this.configServer.getConfig(ConfigTypes.LOCATION);
}
@ -32,15 +30,12 @@ export class LocationController
* @param sessionId Players Id
* @returns ILocationsGenerateAllResponse
*/
public generateAll(sessionId: string): ILocationsGenerateAllResponse
{
public generateAll(sessionId: string): ILocationsGenerateAllResponse {
const locationsFromDb = this.databaseService.getLocations();
const locations: ILocations = {};
for (const mapName in locationsFromDb)
{
for (const mapName in locationsFromDb) {
const mapBase = locationsFromDb[mapName]?.base;
if (!mapBase)
{
if (!mapBase) {
this.logger.debug(`Map: ${mapName} has no base json file, skipping generation`);
continue;
}
@ -55,8 +50,7 @@ export class LocationController
}
/** Handle client/airdrop/loot */
public getAirdropLoot(): IGetAirdropLootResponse
{
public getAirdropLoot(): IGetAirdropLootResponse {
return this.airdropService.generateAirdropLoot();
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { ApplicationContext } from "@spt/context/ApplicationContext";
import { ContextVariableType } from "@spt/context/ContextVariableType";
import { IEndLocalRaidRequestData } from "@spt/models/eft/match/IEndLocalRaidRequestData";
@ -19,10 +18,10 @@ import { LocationLifecycleService } from "@spt/services/LocationLifecycleService
import { MatchLocationService } from "@spt/services/MatchLocationService";
import { ProfileSnapshotService } from "@spt/services/ProfileSnapshotService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class MatchController
{
export class MatchController {
protected matchConfig: IMatchConfig;
protected pmcConfig: IPmcConfig;
@ -35,26 +34,22 @@ export class MatchController
@inject("ApplicationContext") protected applicationContext: ApplicationContext,
@inject("LocationLifecycleService") protected locationLifecycleService: LocationLifecycleService,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
) {
this.matchConfig = this.configServer.getConfig(ConfigTypes.MATCH);
this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
}
public getEnabled(): boolean
{
public getEnabled(): boolean {
return this.matchConfig.enabled;
}
/** Handle client/match/group/delete */
public deleteGroup(info: any): void
{
public deleteGroup(info: any): void {
this.matchLocationService.deleteGroup(info);
}
/** Handle match/group/start_game */
public joinMatch(info: IMatchGroupStartGameRequest, sessionId: string): IProfileStatusResponse
{
public joinMatch(info: IMatchGroupStartGameRequest, sessionId: string): IProfileStatusResponse {
const output: IProfileStatusResponse = { maxPveCountExceeded: false, profiles: [] };
// get list of players joining into the match
@ -77,8 +72,7 @@ export class MatchController
}
/** Handle client/match/group/status */
public getGroupStatus(info: IMatchGroupStatusRequest): IMatchGroupStatusResponse
{
public getGroupStatus(info: IMatchGroupStatusRequest): IMatchGroupStatusResponse {
return { players: [], maxPveCountExceeded: false };
}
@ -87,16 +81,14 @@ export class MatchController
* @param request Raid config request
* @param sessionID Session id
*/
public configureOfflineRaid(request: IGetRaidConfigurationRequestData, sessionID: string): void
{
public configureOfflineRaid(request: IGetRaidConfigurationRequestData, sessionID: string): void {
// Store request data for access during bot generation
this.applicationContext.addValue(ContextVariableType.RAID_CONFIGURATION, request);
// TODO: add code to strip PMC of equipment now they've started the raid
// Set pmcs to difficulty set in pre-raid screen if override in bot config isnt enabled
if (!this.pmcConfig.useDifficultyOverride)
{
if (!this.pmcConfig.useDifficultyOverride) {
this.pmcConfig.difficulty = this.convertDifficultyDropdownIntoBotDifficulty(
request.wavesSettings.botDifficulty,
);
@ -112,11 +104,9 @@ export class MatchController
* @param botDifficulty dropdown difficulty value
* @returns bot difficulty
*/
protected convertDifficultyDropdownIntoBotDifficulty(botDifficulty: string): string
{
protected convertDifficultyDropdownIntoBotDifficulty(botDifficulty: string): string {
// Edge case medium - must be altered
if (botDifficulty.toLowerCase() === "medium")
{
if (botDifficulty.toLowerCase() === "medium") {
return "normal";
}
@ -124,14 +114,12 @@ export class MatchController
}
/** Handle client/match/local/start */
public startLocalRaid(sessionId: string, request: IStartLocalRaidRequestData): IStartLocalRaidResponseData
{
public startLocalRaid(sessionId: string, request: IStartLocalRaidRequestData): IStartLocalRaidResponseData {
return this.locationLifecycleService.startLocalRaid(sessionId, request);
}
/** Handle client/match/local/end */
public endLocalRaid(sessionId: string, request: IEndLocalRaidRequestData): void
{
public endLocalRaid(sessionId: string, request: IEndLocalRaidRequestData): void {
return this.locationLifecycleService.endLocalRaid(sessionId, request);
}
}

View File

@ -1,26 +1,22 @@
import { inject, injectable } from "tsyringe";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { Note } from "@spt/models/eft/common/tables/IBotBase";
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { INoteActionData } from "@spt/models/eft/notes/INoteActionData";
import { EventOutputHolder } from "@spt/routers/EventOutputHolder";
import { inject, injectable } from "tsyringe";
@injectable()
export class NoteController
{
constructor(@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder)
{}
export class NoteController {
constructor(@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder) {}
public addNote(pmcData: IPmcData, body: INoteActionData, sessionID: string): IItemEventRouterResponse
{
public addNote(pmcData: IPmcData, body: INoteActionData, sessionID: string): IItemEventRouterResponse {
const newNote: Note = { Time: body.note.Time, Text: body.note.Text };
pmcData.Notes.Notes.push(newNote);
return this.eventOutputHolder.getOutput(sessionID);
}
public editNote(pmcData: IPmcData, body: INoteActionData, sessionID: string): IItemEventRouterResponse
{
public editNote(pmcData: IPmcData, body: INoteActionData, sessionID: string): IItemEventRouterResponse {
const noteToEdit: Note = pmcData.Notes.Notes[body.index];
noteToEdit.Time = body.note.Time;
noteToEdit.Text = body.note.Text;
@ -28,8 +24,7 @@ export class NoteController
return this.eventOutputHolder.getOutput(sessionID);
}
public deleteNote(pmcData: IPmcData, body: INoteActionData, sessionID: string): IItemEventRouterResponse
{
public deleteNote(pmcData: IPmcData, body: INoteActionData, sessionID: string): IItemEventRouterResponse {
pmcData.Notes.Notes.splice(body.index, 1);
return this.eventOutputHolder.getOutput(sessionID);
}

View File

@ -1,12 +1,11 @@
import { inject, injectable } from "tsyringe";
import { HttpServerHelper } from "@spt/helpers/HttpServerHelper";
import { NotifierHelper } from "@spt/helpers/NotifierHelper";
import { INotifierChannel } from "@spt/models/eft/notifier/INotifier";
import { NotificationService } from "@spt/services/NotificationService";
import { inject, injectable } from "tsyringe";
@injectable()
export class NotifierController
{
export class NotifierController {
protected pollInterval = 300;
protected timeout = 15000;
@ -14,8 +13,7 @@ export class NotifierController
@inject("NotifierHelper") protected notifierHelper: NotifierHelper,
@inject("HttpServerHelper") protected httpServerHelper: HttpServerHelper,
@inject("NotificationService") protected notificationService: NotificationService,
)
{}
) {}
/**
* Resolve an array of session notifications.
@ -24,10 +22,8 @@ export class NotifierController
* one or more appear or when a timeout expires.
* If no notifications are available after the timeout, use a default message.
*/
public async notifyAsync(sessionID: string): Promise<unknown>
{
return new Promise((resolve) =>
{
public async notifyAsync(sessionID: string): Promise<unknown> {
return new Promise((resolve) => {
// keep track of our timeout
let counter = 0;
@ -35,17 +31,14 @@ export class NotifierController
* Check for notifications, resolve if any, otherwise poll
* intermittently for a period of time.
*/
const checkNotifications = () =>
{
const checkNotifications = () => {
/**
* If there are no pending messages we should either check again later
* or timeout now with a default response.
*/
if (!this.notificationService.has(sessionID))
{
if (!this.notificationService.has(sessionID)) {
// have we exceeded timeout? if so reply with default ping message
if (counter > this.timeout)
{
if (counter > this.timeout) {
return resolve([this.notifierHelper.getDefaultNotification()]);
}
@ -71,14 +64,12 @@ export class NotifierController
});
}
public getServer(sessionID: string): string
{
public getServer(sessionID: string): string {
return `${this.httpServerHelper.getBackendUrl()}/notifierServer/get/${sessionID}`;
}
/** Handle client/notifier/channel/create */
public getChannel(sessionID: string): INotifierChannel
{
public getChannel(sessionID: string): INotifierChannel {
return {
server: this.httpServerHelper.buildUrl(),
channel_id: sessionID,

View File

@ -1,28 +1,23 @@
import { inject, injectable } from "tsyringe";
import { PresetHelper } from "@spt/helpers/PresetHelper";
import { IPreset } from "@spt/models/eft/common/IGlobals";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { DatabaseService } from "@spt/services/DatabaseService";
import { inject, injectable } from "tsyringe";
@injectable()
export class PresetController
{
export class PresetController {
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("PresetHelper") protected presetHelper: PresetHelper,
@inject("DatabaseService") protected databaseService: DatabaseService,
)
{}
) {}
public initialize(): void
{
public initialize(): void {
const presets: [string, IPreset][] = Object.entries(this.databaseService.getGlobals().ItemPresets);
const reverse: Record<string, string[]> = {};
for (const [id, preset] of presets)
{
if (id !== preset._id)
{
for (const [id, preset] of presets) {
if (id !== preset._id) {
this.logger.error(
`Preset for template tpl: '${preset._items[0]._tpl} ${preset._name}' has invalid key: (${id} != ${preset._id}). Skipping`,
);
@ -32,8 +27,7 @@ export class PresetController
const tpl = preset._items[0]._tpl;
if (!(tpl in reverse))
{
if (!(tpl in reverse)) {
reverse[tpl] = [];
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { PlayerScavGenerator } from "@spt/generators/PlayerScavGenerator";
import { DialogueHelper } from "@spt/helpers/DialogueHelper";
import { ItemHelper } from "@spt/helpers/ItemHelper";
@ -31,13 +30,13 @@ import { LocalisationService } from "@spt/services/LocalisationService";
import { MailSendService } from "@spt/services/MailSendService";
import { ProfileFixerService } from "@spt/services/ProfileFixerService";
import { SeasonalEventService } from "@spt/services/SeasonalEventService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HashUtil } from "@spt/utils/HashUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class ProfileController
{
export class ProfileController {
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("HashUtil") protected hashUtil: HashUtil,
@ -56,14 +55,12 @@ export class ProfileController
@inject("DialogueHelper") protected dialogueHelper: DialogueHelper,
@inject("QuestHelper") protected questHelper: QuestHelper,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
)
{}
) {}
/**
* Handle /launcher/profiles
*/
public getMiniProfiles(): IMiniProfile[]
{
public getMiniProfiles(): IMiniProfile[] {
const allProfiles = Object.keys(this.saveServer.getProfiles());
return allProfiles.map((sessionId) => this.getMiniProfile(sessionId));
@ -72,11 +69,9 @@ export class ProfileController
/**
* Handle launcher/profile/info
*/
public getMiniProfile(sessionID: string): IMiniProfile
{
public getMiniProfile(sessionID: string): IMiniProfile {
const profile = this.saveServer.getProfile(sessionID);
if (!profile || !profile.characters)
{
if (!profile || !profile.characters) {
throw new Error(`Unable to find character data for id: ${sessionID}. Profile may be corrupt`);
}
@ -84,8 +79,7 @@ export class ProfileController
const maxlvl = this.profileHelper.getMaxLevel();
// Player hasn't completed profile creation process, send defaults
if (!pmc?.Info?.Level)
{
if (!pmc?.Info?.Level) {
return {
username: profile.info?.username ?? "",
nickname: "unknown",
@ -109,9 +103,7 @@ export class ProfileController
side: pmc.Info.Side,
currlvl: pmc.Info.Level,
currexp: pmc.Info.Experience ?? 0,
prevexp: currlvl === 0
? 0
: this.profileHelper.getExperience(currlvl),
prevexp: currlvl === 0 ? 0 : this.profileHelper.getExperience(currlvl),
nextlvl: nextlvl,
maxlvl: maxlvl,
edition: profile.info?.edition ?? "",
@ -123,8 +115,7 @@ export class ProfileController
/**
* Handle client/game/profile/list
*/
public getCompleteProfile(sessionID: string): IPmcData[]
{
public getCompleteProfile(sessionID: string): IPmcData[] {
return this.profileHelper.getCompleteProfile(sessionID);
}
@ -134,11 +125,11 @@ export class ProfileController
* @param sessionID Player id
* @returns Profiles _id value
*/
public createProfile(info: IProfileCreateRequestData, sessionID: string): string
{
public createProfile(info: IProfileCreateRequestData, sessionID: string): string {
const account = this.saveServer.getProfile(sessionID).info;
const profileTemplate: ITemplateSide
= this.cloner.clone(this.databaseService.getProfiles()[account.edition][info.side.toLowerCase()]);
const profileTemplate: ITemplateSide = this.cloner.clone(
this.databaseService.getProfiles()[account.edition][info.side.toLowerCase()],
);
const pmcData = profileTemplate.character;
// Delete existing profile
@ -166,8 +157,7 @@ export class ProfileController
this.updateInventoryEquipmentId(pmcData);
if (!pmcData.UnlockedInfo)
{
if (!pmcData.UnlockedInfo) {
pmcData.UnlockedInfo = { unlockedProductionRecipe: [] };
}
@ -200,14 +190,12 @@ export class ProfileController
this.saveServer.addProfile(profileDetails);
if (profileTemplate.trader.setQuestsAvailableForStart)
{
if (profileTemplate.trader.setQuestsAvailableForStart) {
this.questHelper.addAllQuestsToProfile(profileDetails.characters.pmc, [QuestStatus.AvailableForStart]);
}
// Profile is flagged as wanting quests set to ready to hand in and collect rewards
if (profileTemplate.trader.setQuestsAvailableForFinish)
{
if (profileTemplate.trader.setQuestsAvailableForFinish) {
this.questHelper.addAllQuestsToProfile(profileDetails.characters.pmc, [
QuestStatus.AvailableForStart,
QuestStatus.Started,
@ -234,8 +222,7 @@ export class ProfileController
this.saveServer.saveProfile(sessionID);
// Requires to enable seasonal changes after creating fresh profile
if (this.seasonalEventService.isAutomaticEventDetectionEnabled())
{
if (this.seasonalEventService.isAutomaticEventDetectionEnabled()) {
this.seasonalEventService.enableSeasonalEvents(sessionID);
}
@ -246,22 +233,18 @@ export class ProfileController
* make profiles pmcData.Inventory.equipment unique
* @param pmcData Profile to update
*/
protected updateInventoryEquipmentId(pmcData: IPmcData): void
{
protected updateInventoryEquipmentId(pmcData: IPmcData): void {
const oldEquipmentId = pmcData.Inventory.equipment;
pmcData.Inventory.equipment = this.hashUtil.generate();
for (const item of pmcData.Inventory.items)
{
if (item.parentId === oldEquipmentId)
{
for (const item of pmcData.Inventory.items) {
if (item.parentId === oldEquipmentId) {
item.parentId = pmcData.Inventory.equipment;
continue;
}
if (item._id === oldEquipmentId)
{
if (item._id === oldEquipmentId) {
item._id = pmcData.Inventory.equipment;
}
}
@ -271,14 +254,10 @@ export class ProfileController
* Delete a profile
* @param sessionID Id of profile to delete
*/
protected deleteProfileBySessionId(sessionID: string): void
{
if (sessionID in this.saveServer.getProfiles())
{
protected deleteProfileBySessionId(sessionID: string): void {
if (sessionID in this.saveServer.getProfiles()) {
this.saveServer.deleteProfileById(sessionID);
}
else
{
} else {
this.logger.warning(
this.localisationService.getText("profile-unable_to_find_profile_by_id_cannot_delete", sessionID),
);
@ -296,10 +275,8 @@ export class ProfileController
profileDetails: ISptProfile,
sessionID: string,
response: IItemEventRouterResponse,
): void
{
for (const quest of profileDetails.characters.pmc.Quests)
{
): void {
for (const quest of profileDetails.characters.pmc.Quests) {
const questFromDb = this.questHelper.getQuestFromDb(quest.qid, profileDetails.characters.pmc);
// Get messageId of text to send to player as text message in game
@ -331,10 +308,8 @@ export class ProfileController
* For each trader reset their state to what a level 1 player would see
* @param sessionId Session id of profile to reset
*/
protected resetAllTradersInProfile(sessionId: string): void
{
for (const traderId in this.databaseService.getTraders())
{
protected resetAllTradersInProfile(sessionId: string): void {
for (const traderId in this.databaseService.getTraders()) {
this.traderHelper.resetTrader(sessionId, traderId);
}
}
@ -345,23 +320,19 @@ export class ProfileController
* @param sessionID
* @returns IPmcData object
*/
public generatePlayerScav(sessionID: string): IPmcData
{
public generatePlayerScav(sessionID: string): IPmcData {
return this.playerScavGenerator.generate(sessionID);
}
/**
* Handle client/game/profile/nickname/validate
*/
public validateNickname(info: IValidateNicknameRequestData, sessionID: string): string
{
if (info.nickname.length < 3)
{
public validateNickname(info: IValidateNicknameRequestData, sessionID: string): string {
if (info.nickname.length < 3) {
return "tooshort";
}
if (this.profileHelper.isNicknameTaken(info, sessionID))
{
if (this.profileHelper.isNicknameTaken(info, sessionID)) {
return "taken";
}
@ -372,12 +343,10 @@ export class ProfileController
* Handle client/game/profile/nickname/change event
* Client allows player to adjust their profile name
*/
public changeNickname(info: IProfileChangeNicknameRequestData, sessionID: string): string
{
public changeNickname(info: IProfileChangeNicknameRequestData, sessionID: string): string {
const output = this.validateNickname(info, sessionID);
if (output === "OK")
{
if (output === "OK") {
const pmcData = this.profileHelper.getPmcProfile(sessionID);
pmcData.Info.Nickname = info.nickname;
@ -390,8 +359,7 @@ export class ProfileController
/**
* Handle client/game/profile/voice/change event
*/
public changeVoice(info: IProfileChangeVoiceRequestData, sessionID: string): void
{
public changeVoice(info: IProfileChangeVoiceRequestData, sessionID: string): void {
const pmcData = this.profileHelper.getPmcProfile(sessionID);
pmcData.Info.Voice = info.voice;
}
@ -399,8 +367,7 @@ export class ProfileController
/**
* Handle client/game/profile/search
*/
public getFriends(info: ISearchFriendRequestData, sessionID: string): ISearchFriendResponse[]
{
public getFriends(info: ISearchFriendRequestData, sessionID: string): ISearchFriendResponse[] {
const profile = this.saveServer.getProfile(sessionID);
// return some of the current player info for now
@ -421,8 +388,7 @@ export class ProfileController
/**
* Handle client/profile/status
*/
public getProfileStatus(sessionId: string): GetProfileStatusResponseData
{
public getProfileStatus(sessionId: string): GetProfileStatusResponseData {
const account = this.saveServer.getProfile(sessionId).info;
const response: GetProfileStatusResponseData = {
maxPveCountExceeded: false,
@ -442,8 +408,7 @@ export class ProfileController
return response;
}
public getOtherProfile(sessionId: string, request: IGetOtherProfileRequest): IGetOtherProfileResponse
{
public getOtherProfile(sessionId: string, request: IGetOtherProfileRequest): IGetOtherProfileResponse {
const player = this.profileHelper.getFullProfile(sessionId);
const playerPmc = player.characters.pmc;
const playerScav = player.characters.scav;
@ -493,21 +458,17 @@ export class ProfileController
/**
* Handle client/profile/settings
*/
public setChosenProfileIcon(sessionId: string, request: IGetProfileSettingsRequest): boolean
{
public setChosenProfileIcon(sessionId: string, request: IGetProfileSettingsRequest): boolean {
const profileToUpdate = this.profileHelper.getPmcProfile(sessionId);
if (!profileToUpdate)
{
if (!profileToUpdate) {
return false;
}
if (request.memberCategory !== null)
{
if (request.memberCategory !== null) {
profileToUpdate.Info.SelectedMemberCategory = request.memberCategory;
}
if (request.squadInviteRestriction !== null)
{
if (request.squadInviteRestriction !== null) {
profileToUpdate.Info.SquadInviteRestriction = request.squadInviteRestriction;
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { DialogueHelper } from "@spt/helpers/DialogueHelper";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
@ -27,13 +26,13 @@ import { LocaleService } from "@spt/services/LocaleService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { MailSendService } from "@spt/services/MailSendService";
import { PlayerService } from "@spt/services/PlayerService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class QuestController
{
export class QuestController {
protected questConfig: IQuestConfig;
constructor(
@ -54,8 +53,7 @@ export class QuestController
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
) {
this.questConfig = this.configServer.getConfig(ConfigTypes.QUEST);
}
@ -66,44 +64,37 @@ export class QuestController
* @param sessionID session id
* @returns array of IQuest
*/
public getClientQuests(sessionID: string): IQuest[]
{
public getClientQuests(sessionID: string): IQuest[] {
const questsToShowPlayer: IQuest[] = [];
const allQuests = this.questHelper.getQuestsFromDb();
const profile: IPmcData = this.profileHelper.getPmcProfile(sessionID);
for (const quest of allQuests)
{
for (const quest of allQuests) {
// Player already accepted the quest, show it regardless of status
const questInProfile = profile.Quests.find((x) => x.qid === quest._id);
if (questInProfile)
{
if (questInProfile) {
quest.sptStatus = questInProfile.status;
questsToShowPlayer.push(quest);
continue;
}
// Filter out bear quests for usec and vice versa
if (this.questHelper.questIsForOtherSide(profile.Info.Side, quest._id))
{
if (this.questHelper.questIsForOtherSide(profile.Info.Side, quest._id)) {
continue;
}
if (!this.questHelper.showEventQuestToPlayer(quest._id))
{
if (!this.questHelper.showEventQuestToPlayer(quest._id)) {
continue;
}
// Don't add quests that have a level higher than the user's
if (!this.playerLevelFulfillsQuestRequirement(quest, profile.Info.Level))
{
if (!this.playerLevelFulfillsQuestRequirement(quest, profile.Info.Level)) {
continue;
}
// Player can use trader mods then remove them, leaving quests behind
const trader = profile.TradersInfo[quest.traderId];
if (!trader)
{
if (!trader) {
this.logger.debug(
`Unable to show quest: ${quest.QuestName} as its for a trader: ${quest.traderId} that no longer exists.`,
);
@ -121,11 +112,10 @@ export class QuestController
// Quest has no conditions, standing or loyalty conditions, add to visible quest list
if (
questRequirements.length === 0
&& loyaltyRequirements.length === 0
&& standingRequirements.length === 0
)
{
questRequirements.length === 0 &&
loyaltyRequirements.length === 0 &&
standingRequirements.length === 0
) {
quest.sptStatus = QuestStatus.AvailableForStart;
questsToShowPlayer.push(quest);
continue;
@ -134,34 +124,29 @@ export class QuestController
// Check the status of each quest condition, if any are not completed
// then this quest should not be visible
let haveCompletedPreviousQuest = true;
for (const conditionToFulfil of questRequirements)
{
for (const conditionToFulfil of questRequirements) {
// If the previous quest isn't in the user profile, it hasn't been completed or started
const prerequisiteQuest = profile.Quests.find((profileQuest) =>
conditionToFulfil.target.includes(profileQuest.qid),
);
if (!prerequisiteQuest)
{
if (!prerequisiteQuest) {
haveCompletedPreviousQuest = false;
break;
}
// Prereq does not have its status requirement fulfilled
// Some bsg status ids are strings, MUST convert to number before doing includes check
if (!conditionToFulfil.status.map((status) => Number(status)).includes(prerequisiteQuest.status))
{
if (!conditionToFulfil.status.map((status) => Number(status)).includes(prerequisiteQuest.status)) {
haveCompletedPreviousQuest = false;
break;
}
// Has a wait timer
if (conditionToFulfil.availableAfter > 0)
{
if (conditionToFulfil.availableAfter > 0) {
// Compare current time to unlock time for previous quest
const previousQuestCompleteTime = prerequisiteQuest.statusTimers[prerequisiteQuest.status];
const unlockTime = previousQuestCompleteTime + conditionToFulfil.availableAfter;
if (unlockTime > this.timeUtil.getTimestamp())
{
if (unlockTime > this.timeUtil.getTimestamp()) {
this.logger.debug(
`Quest ${quest.QuestName} is locked for another ${
unlockTime - this.timeUtil.getTimestamp()
@ -172,33 +157,27 @@ export class QuestController
}
// Previous quest not completed, skip
if (!haveCompletedPreviousQuest)
{
if (!haveCompletedPreviousQuest) {
continue;
}
let passesLoyaltyRequirements = true;
for (const condition of loyaltyRequirements)
{
if (!this.questHelper.traderLoyaltyLevelRequirementCheck(condition, profile))
{
for (const condition of loyaltyRequirements) {
if (!this.questHelper.traderLoyaltyLevelRequirementCheck(condition, profile)) {
passesLoyaltyRequirements = false;
break;
}
}
let passesStandingRequirements = true;
for (const condition of standingRequirements)
{
if (!this.questHelper.traderStandingRequirementCheck(condition, profile))
{
for (const condition of standingRequirements) {
if (!this.questHelper.traderStandingRequirementCheck(condition, profile)) {
passesStandingRequirements = false;
break;
}
}
if (haveCompletedPreviousQuest && passesLoyaltyRequirements && passesStandingRequirements)
{
if (haveCompletedPreviousQuest && passesLoyaltyRequirements && passesStandingRequirements) {
quest.sptStatus = QuestStatus.AvailableForStart;
questsToShowPlayer.push(quest);
}
@ -213,21 +192,16 @@ export class QuestController
* @param playerLevel level of player to test against quest
* @returns true if quest can be seen/accepted by player of defined level
*/
protected playerLevelFulfillsQuestRequirement(quest: IQuest, playerLevel: number): boolean
{
if (!quest.conditions)
{
protected playerLevelFulfillsQuestRequirement(quest: IQuest, playerLevel: number): boolean {
if (!quest.conditions) {
// No conditions
return true;
}
const levelConditions = this.questConditionHelper.getLevelConditions(quest.conditions.AvailableForStart);
if (levelConditions.length)
{
for (const levelCondition of levelConditions)
{
if (!this.questHelper.doesPlayerLevelFulfilCondition(playerLevel, levelCondition))
{
if (levelConditions.length) {
for (const levelCondition of levelConditions) {
if (!this.questHelper.doesPlayerLevelFulfilCondition(playerLevel, levelCondition)) {
// Not valid, exit out
return false;
}
@ -252,23 +226,19 @@ export class QuestController
pmcData: IPmcData,
acceptedQuest: IAcceptQuestRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const acceptQuestResponse = this.eventOutputHolder.getOutput(sessionID);
// Does quest exist in profile
// Restarting a failed quest can mean quest exists in profile
const existingQuestStatus = pmcData.Quests.find((x) => x.qid === acceptedQuest.qid);
if (existingQuestStatus)
{
if (existingQuestStatus) {
// Update existing
this.questHelper.resetQuestState(pmcData, QuestStatus.Started, acceptedQuest.qid);
// Need to send client an empty list of completedConditions (Unsure if this does anything)
acceptQuestResponse.profileChanges[sessionID].questsStatus.push(existingQuestStatus);
}
else
{
} else {
// Add new quest to server profile
const newQuest = this.questHelper.getQuestReadyForProfile(pmcData, QuestStatus.Started, acceptedQuest);
pmcData.Quests.push(newQuest);
@ -324,8 +294,7 @@ export class QuestController
pmcData: IPmcData,
acceptedQuest: IAcceptQuestRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
// Create and store quest status object inside player profile
const newRepeatableQuest = this.questHelper.getQuestReadyForProfile(
pmcData,
@ -336,8 +305,7 @@ export class QuestController
// Look for the generated quest cache in profile.RepeatableQuests
const repeatableQuestProfile = this.getRepeatableQuestFromProfile(pmcData, acceptedQuest);
if (!repeatableQuestProfile)
{
if (!repeatableQuestProfile) {
this.logger.error(
this.localisationService.getText(
"repeatable-accepted_repeatable_quest_not_found_in_active_quests",
@ -350,13 +318,11 @@ export class QuestController
// Some scav quests need to be added to scav profile for them to show up in-raid
if (
repeatableQuestProfile.side === "Scav"
&& ["PickUp", "Exploration", "Elimination"].includes(repeatableQuestProfile.type)
)
{
repeatableQuestProfile.side === "Scav" &&
["PickUp", "Exploration", "Elimination"].includes(repeatableQuestProfile.type)
) {
const fullProfile = this.profileHelper.getFullProfile(sessionID);
if (!fullProfile.characters.scav.Quests)
{
if (!fullProfile.characters.scav.Quests) {
fullProfile.characters.scav.Quests = [];
}
@ -371,20 +337,21 @@ export class QuestController
protected createAcceptedQuestClientResponse(
sessionID: string,
pmcData: IPmcData,
repeatableQuestProfile: IRepeatableQuest): IItemEventRouterResponse
{
const repeatableSettings = pmcData.RepeatableQuests
.find((quest) => quest.name === repeatableQuestProfile.sptRepatableGroupName);
repeatableQuestProfile: IRepeatableQuest,
): IItemEventRouterResponse {
const repeatableSettings = pmcData.RepeatableQuests.find(
(quest) => quest.name === repeatableQuestProfile.sptRepatableGroupName,
);
const change = {};
change[repeatableQuestProfile._id] = repeatableSettings!.changeRequirement[repeatableQuestProfile._id];
const repeatableData: IPmcDataRepeatableQuest = {
id:
repeatableSettings.id
?? this.questConfig.repeatableQuests
.find((repeatableQuest) => repeatableQuest.name === repeatableQuestProfile.sptRepatableGroupName)
.id,
repeatableSettings.id ??
this.questConfig.repeatableQuests.find(
(repeatableQuest) => repeatableQuest.name === repeatableQuestProfile.sptRepatableGroupName,
).id,
name: repeatableSettings.name,
endTime: repeatableSettings.endTime,
changeRequirement: change,
@ -396,8 +363,7 @@ export class QuestController
// Nullguard
const acceptQuestResponse = this.eventOutputHolder.getOutput(sessionID);
if (!acceptQuestResponse.profileChanges[sessionID].repeatableQuests)
{
if (!acceptQuestResponse.profileChanges[sessionID].repeatableQuests) {
acceptQuestResponse.profileChanges[sessionID].repeatableQuests = [];
}
@ -416,13 +382,10 @@ export class QuestController
protected getRepeatableQuestFromProfile(
pmcData: IPmcData,
acceptedQuest: IAcceptQuestRequestData,
): IRepeatableQuest
{
for (const repeatableQuest of pmcData.RepeatableQuests)
{
): IRepeatableQuest {
for (const repeatableQuest of pmcData.RepeatableQuests) {
const matchingQuest = repeatableQuest.activeQuests.find((x) => x._id === acceptedQuest.qid);
if (matchingQuest)
{
if (matchingQuest) {
this.logger.debug(`Accepted repeatable quest ${acceptedQuest.qid} from ${repeatableQuest.name}`);
matchingQuest.sptRepatableGroupName = repeatableQuest.name;
@ -447,8 +410,7 @@ export class QuestController
pmcData: IPmcData,
body: ICompleteQuestRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const completeQuestResponse = this.eventOutputHolder.getOutput(sessionID);
const completedQuest = this.questHelper.getQuestFromDb(body.qid, pmcData);
@ -469,8 +431,7 @@ export class QuestController
// Check for linked failed + unrestartable quests (only get quests not already failed
const questsToFail = this.getQuestsFailedByCompletingQuest(completedQuestId, pmcData);
if (questsToFail?.length > 0)
{
if (questsToFail?.length > 0) {
this.failQuests(sessionID, pmcData, questsToFail, completeQuestResponse);
}
@ -487,16 +448,13 @@ export class QuestController
completeQuestResponse.profileChanges[sessionID].quests.push(...questDelta);
// Check if it's a repeatable quest. If so, remove from Quests
for (const currentRepeatable of pmcData.RepeatableQuests)
{
for (const currentRepeatable of pmcData.RepeatableQuests) {
const repeatableQuest = currentRepeatable.activeQuests.find(
(activeRepeatable) => activeRepeatable._id === completedQuestId,
);
if (repeatableQuest)
{
if (repeatableQuest) {
// Need to remove redundant scav quest object as its no longer necessary, is tracked in pmc profile
if (repeatableQuest.side === "Scav")
{
if (repeatableQuest.side === "Scav") {
this.removeQuestFromScavProfile(sessionID, repeatableQuest._id);
}
}
@ -504,8 +462,7 @@ export class QuestController
// Hydrate client response questsStatus array with data
const questStatusChanges = this.getQuestsWithDifferentStatuses(preCompleteProfileQuests, pmcData.Quests);
if (questStatusChanges)
{
if (questStatusChanges) {
completeQuestResponse.profileChanges[sessionID].questsStatus.push(...questStatusChanges);
}
@ -520,14 +477,11 @@ export class QuestController
* @param completedQuestId quest completed id
* @returns array of IQuest objects
*/
protected getQuestsFailedByCompletingQuest(completedQuestId: string, pmcProfile: IPmcData): IQuest[]
{
protected getQuestsFailedByCompletingQuest(completedQuestId: string, pmcProfile: IPmcData): IQuest[] {
const questsInDb = this.questHelper.getQuestsFromDb();
return questsInDb.filter((quest) =>
{
return questsInDb.filter((quest) => {
// No fail conditions, skip
if (!quest.conditions.Fail || quest.conditions.Fail.length === 0)
{
if (!quest.conditions.Fail || quest.conditions.Fail.length === 0) {
return false;
}
@ -536,8 +490,7 @@ export class QuestController
pmcProfile.Quests.some(
(profileQuest) => profileQuest.qid === quest._id && profileQuest.status === QuestStatus.Fail,
)
)
{
) {
return false;
}
@ -550,17 +503,16 @@ export class QuestController
* @param sessionId Player id
* @param questIdToRemove Qid of quest to remove
*/
protected removeQuestFromScavProfile(sessionId: string, questIdToRemove: string): void
{
protected removeQuestFromScavProfile(sessionId: string, questIdToRemove: string): void {
const fullProfile = this.profileHelper.getFullProfile(sessionId);
const repeatableInScavProfile = fullProfile.characters.scav.Quests?.find((x) => x.qid === questIdToRemove);
if (!repeatableInScavProfile)
{
this.logger.warning(this.localisationService.getText("quest-unable_to_remove_scav_quest_from_profile",
{
if (!repeatableInScavProfile) {
this.logger.warning(
this.localisationService.getText("quest-unable_to_remove_scav_quest_from_profile", {
scavQuestId: questIdToRemove,
profileId: sessionId,
}));
}),
);
return;
}
@ -580,22 +532,18 @@ export class QuestController
protected getQuestsWithDifferentStatuses(
preQuestStatusus: IQuestStatus[],
postQuestStatuses: IQuestStatus[],
): IQuestStatus[] | undefined
{
): IQuestStatus[] | undefined {
const result: IQuestStatus[] = [];
for (const quest of postQuestStatuses)
{
for (const quest of postQuestStatuses) {
// Add quest if status differs or quest not found
const preQuest = preQuestStatusus.find((x) => x.qid === quest.qid);
if (!preQuest || preQuest.status !== quest.status)
{
if (!preQuest || preQuest.status !== quest.status) {
result.push(quest);
}
}
if (result.length === 0)
{
if (result.length === 0) {
return undefined;
}
@ -614,8 +562,7 @@ export class QuestController
pmcData: IPmcData,
completedQuestId: string,
questRewards: Item[],
): void
{
): void {
const quest = this.questHelper.getQuestFromDb(completedQuestId, pmcData);
this.mailSendService.sendLocalisedNpcMessageToPlayer(
@ -634,24 +581,20 @@ export class QuestController
* @param quests Quests to look for wait conditions in
* @param completedQuestId Quest just completed
*/
protected addTimeLockedQuestsToProfile(pmcData: IPmcData, quests: IQuest[], completedQuestId: string): void
{
protected addTimeLockedQuestsToProfile(pmcData: IPmcData, quests: IQuest[], completedQuestId: string): void {
// Iterate over quests, look for quests with right criteria
for (const quest of quests)
{
for (const quest of quests) {
// If quest has prereq of completed quest + availableAfter value > 0 (quest has wait time)
const nextQuestWaitCondition = quest.conditions.AvailableForStart.find(
(x) => x.target?.includes(completedQuestId) && x.availableAfter > 0,
);
if (nextQuestWaitCondition)
{
if (nextQuestWaitCondition) {
// Now + wait time
const availableAfterTimestamp = this.timeUtil.getTimestamp() + nextQuestWaitCondition.availableAfter;
// Update quest in profile with status of AvailableAfter
const existingQuestInProfile = pmcData.Quests.find((x) => x.qid === quest._id);
if (existingQuestInProfile)
{
if (existingQuestInProfile) {
existingQuestInProfile.availableAfter = availableAfterTimestamp;
existingQuestInProfile.status = QuestStatus.AvailableAfter;
existingQuestInProfile.startTime = 0;
@ -686,21 +629,16 @@ export class QuestController
pmcData: IPmcData,
questsToFail: IQuest[],
output: IItemEventRouterResponse,
): void
{
for (const questToFail of questsToFail)
{
): void {
for (const questToFail of questsToFail) {
// Skip failing a quest that has a fail status of something other than success
if (questToFail.conditions.Fail?.some((x) => x.status?.some((status) => status !== QuestStatus.Success)))
{
if (questToFail.conditions.Fail?.some((x) => x.status?.some((status) => status !== QuestStatus.Success))) {
continue;
}
const isActiveQuestInPlayerProfile = pmcData.Quests.find((quest) => quest.qid === questToFail._id);
if (isActiveQuestInPlayerProfile)
{
if (isActiveQuestInPlayerProfile.status !== QuestStatus.Fail)
{
if (isActiveQuestInPlayerProfile) {
if (isActiveQuestInPlayerProfile.status !== QuestStatus.Fail) {
const failBody: IFailQuestRequestData = {
Action: "QuestFail",
qid: questToFail._id,
@ -708,9 +646,7 @@ export class QuestController
};
this.questHelper.failQuest(pmcData, failBody, sessionID, output);
}
}
else
{
} else {
// Failing an entirely new quest that doesnt exist in profile
const statusTimers = {};
statusTimers[QuestStatus.Fail] = this.timeUtil.getTimestamp();
@ -736,8 +672,7 @@ export class QuestController
pmcData: IPmcData,
handoverQuestRequest: IHandoverQuestRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const quest = this.questHelper.getQuestFromDb(handoverQuestRequest.qid, pmcData);
const handoverQuestTypes = ["HandoverItem", "WeaponAssembly"];
const output = this.eventOutputHolder.getOutput(sessionID);
@ -747,25 +682,22 @@ export class QuestController
// Decrement number of items handed in
let handoverRequirements: IQuestCondition;
for (const condition of quest.conditions.AvailableForFinish)
{
for (const condition of quest.conditions.AvailableForFinish) {
if (
condition.id === handoverQuestRequest.conditionId
&& handoverQuestTypes.includes(condition.conditionType)
)
{
condition.id === handoverQuestRequest.conditionId &&
handoverQuestTypes.includes(condition.conditionType)
) {
handedInCount = Number.parseInt(<string>condition.value);
isItemHandoverQuest = condition.conditionType === handoverQuestTypes[0];
handoverRequirements = condition;
const profileCounter
= handoverQuestRequest.conditionId in pmcData.TaskConditionCounters
const profileCounter =
handoverQuestRequest.conditionId in pmcData.TaskConditionCounters
? pmcData.TaskConditionCounters[handoverQuestRequest.conditionId].value
: 0;
handedInCount -= profileCounter;
if (handedInCount <= 0)
{
if (handedInCount <= 0) {
this.logger.error(
this.localisationService.getText(
"repeatable-quest_handover_failed_condition_already_satisfied",
@ -785,17 +717,14 @@ export class QuestController
}
}
if (isItemHandoverQuest && handedInCount === 0)
{
if (isItemHandoverQuest && handedInCount === 0) {
return this.showRepeatableQuestInvalidConditionError(handoverQuestRequest, output);
}
let totalItemCountToRemove = 0;
for (const itemHandover of handoverQuestRequest.items)
{
for (const itemHandover of handoverQuestRequest.items) {
const matchingItemInProfile = pmcData.Inventory.items.find((item) => item._id === itemHandover.id);
if (!(matchingItemInProfile && handoverRequirements.target.includes(matchingItemInProfile._tpl)))
{
if (!(matchingItemInProfile && handoverRequirements.target.includes(matchingItemInProfile._tpl))) {
// Item handed in by player doesnt match what was requested
return this.showQuestItemHandoverMatchError(
handoverQuestRequest,
@ -808,8 +737,7 @@ export class QuestController
// Remove the right quantity of given items
const itemCountToRemove = Math.min(itemHandover.count, handedInCount - totalItemCountToRemove);
totalItemCountToRemove += itemCountToRemove;
if (itemHandover.count - itemCountToRemove > 0)
{
if (itemHandover.count - itemCountToRemove > 0) {
// Remove single item with no children
this.questHelper.changeItemStack(
pmcData,
@ -818,13 +746,10 @@ export class QuestController
sessionID,
output,
);
if (totalItemCountToRemove === handedInCount)
{
if (totalItemCountToRemove === handedInCount) {
break;
}
}
else
{
} else {
// Remove item with children
const toRemove = this.itemHelper.findAndReturnChildrenByItems(pmcData.Inventory.items, itemHandover.id);
let index = pmcData.Inventory.items.length;
@ -833,17 +758,14 @@ export class QuestController
output.profileChanges[sessionID].items.del.push({ _id: itemHandover.id });
// Important: loop backward when removing items from the array we're looping on
while (index-- > 0)
{
if (toRemove.includes(pmcData.Inventory.items[index]._id))
{
while (index-- > 0) {
if (toRemove.includes(pmcData.Inventory.items[index]._id)) {
// Remove the item
const removedItem = pmcData.Inventory.items.splice(index, 1)[0];
// If the removed item has a numeric `location` property, re-calculate all the child
// element `location` properties of the parent so they are sequential, while retaining order
if (typeof removedItem.location === "number")
{
if (typeof removedItem.location === "number") {
const childItems = this.itemHelper.findAndReturnChildrenAsItems(
pmcData.Inventory.items,
removedItem.parentId,
@ -852,8 +774,7 @@ export class QuestController
// Sort by the current `location` and update
childItems.sort((a, b) => (a.location > b.location ? 1 : -1));
for (const [index, item] of childItems.entries())
{
for (const [index, item] of childItems.entries()) {
item.location = index;
}
}
@ -881,8 +802,7 @@ export class QuestController
protected showRepeatableQuestInvalidConditionError(
handoverQuestRequest: IHandoverQuestRequestData,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const errorMessage = this.localisationService.getText("repeatable-quest_handover_failed_condition_invalid", {
questId: handoverQuestRequest.qid,
conditionId: handoverQuestRequest.conditionId,
@ -905,8 +825,7 @@ export class QuestController
itemHandedOver: Item,
handoverRequirements: IQuestCondition,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const errorMessage = this.localisationService.getText("quest-handover_wrong_item", {
questId: handoverQuestRequest.qid,
handedInTpl: itemHandedOver?._tpl ?? "UNKNOWN",
@ -930,10 +849,8 @@ export class QuestController
conditionId: string,
questId: string,
counterValue: number,
): void
{
if (pmcData.TaskConditionCounters[conditionId] !== undefined)
{
): void {
if (pmcData.TaskConditionCounters[conditionId] !== undefined) {
pmcData.TaskConditionCounters[conditionId].value += counterValue;
return;
@ -959,8 +876,7 @@ export class QuestController
request: IFailQuestRequestData,
sessionID: string,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
this.questHelper.failQuest(pmcData, request, sessionID, output);
return output;

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { RagfairOfferGenerator } from "@spt/generators/RagfairOfferGenerator";
import { HandbookHelper } from "@spt/helpers/HandbookHelper";
import { InventoryHelper } from "@spt/helpers/InventoryHelper";
@ -43,13 +42,13 @@ import { RagfairRequiredItemsService } from "@spt/services/RagfairRequiredItemsS
import { RagfairTaxService } from "@spt/services/RagfairTaxService";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { inject, injectable } from "tsyringe";
/**
* Handle RagfairCallback events
*/
@injectable()
export class RagfairController
{
export class RagfairController {
protected ragfairConfig: IRagfairConfig;
constructor(
@ -78,8 +77,7 @@ export class RagfairController
@inject("RagfairOfferGenerator") protected ragfairOfferGenerator: RagfairOfferGenerator,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
) {
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
}
@ -90,8 +88,7 @@ export class RagfairController
* @param searchRequest Search request data
* @returns IGetOffersResult
*/
public getOffers(sessionID: string, searchRequest: ISearchRequestData): IGetOffersResult
{
public getOffers(sessionID: string, searchRequest: ISearchRequestData): IGetOffersResult {
const profile = this.profileHelper.getFullProfile(sessionID);
const itemsToAdd = this.ragfairHelper.filterCategories(sessionID, searchRequest);
@ -105,8 +102,7 @@ export class RagfairController
result.offers = this.getOffersForSearchType(searchRequest, itemsToAdd, traderAssorts, profile.characters.pmc);
// Client requested a category refresh
if (searchRequest.updateOfferCount)
{
if (searchRequest.updateOfferCount) {
result.categories = this.getSpecificCategories(profile.characters.pmc, searchRequest, result.offers);
}
@ -120,14 +116,11 @@ export class RagfairController
);
// Match offers with quests and lock unfinished quests
for (const offer of result.offers)
{
if (offer.user.memberType === MemberCategory.TRADER)
{
for (const offer of result.offers) {
if (offer.user.memberType === MemberCategory.TRADER) {
// for the items, check the barter schemes. The method getDisplayableAssorts sets a flag sptQuestLocked
// to true if the quest is not completed yet
if (this.ragfairOfferHelper.traderOfferItemQuestLocked(offer, traderAssorts))
{
if (this.ragfairOfferHelper.traderOfferItemQuestLocked(offer, traderAssorts)) {
offer.locked = true;
}
@ -140,8 +133,7 @@ export class RagfairController
result.offersCount = result.offers.length;
// Handle paging before returning results only if searching for general items, not preset items
if (searchRequest.buildCount === 0)
{
if (searchRequest.buildCount === 0) {
const start = searchRequest.page * searchRequest.limit;
const end = Math.min((searchRequest.page + 1) * searchRequest.limit, result.offers.length);
result.offers = result.offers.slice(start, end);
@ -156,8 +148,7 @@ export class RagfairController
* @param request Request data
* @returns IRagfairOffer
*/
public getOfferById(sessionId: string, request: IGetRagfairOfferByIdRequest): IRagfairOffer
{
public getOfferById(sessionId: string, request: IGetRagfairOfferByIdRequest): IRagfairOffer {
const offers = this.ragfairOfferService.getOffers();
const offerToReturn = offers.find((offer) => offer.intId === request.id);
@ -177,16 +168,13 @@ export class RagfairController
itemsToAdd: string[],
traderAssorts: Record<string, ITraderAssort>,
pmcProfile: IPmcData,
): IRagfairOffer[]
{
): IRagfairOffer[] {
// Searching for items in preset menu
if (searchRequest.buildCount)
{
if (searchRequest.buildCount) {
return this.ragfairOfferHelper.getOffersForBuild(searchRequest, itemsToAdd, traderAssorts, pmcProfile);
}
if (searchRequest.neededSearchId?.length > 0)
{
if (searchRequest.neededSearchId?.length > 0) {
return this.ragfairOfferHelper.getOffersThatRequireItem(searchRequest, pmcProfile);
}
@ -204,23 +192,17 @@ export class RagfairController
pmcProfile: IPmcData,
searchRequest: ISearchRequestData,
offers: IRagfairOffer[],
): Record<string, number>
{
): Record<string, number> {
// Linked/required search categories
const playerHasFleaUnlocked
= pmcProfile.Info.Level >= this.databaseService.getGlobals().config.RagFair.minUserLevel;
const playerHasFleaUnlocked =
pmcProfile.Info.Level >= this.databaseService.getGlobals().config.RagFair.minUserLevel;
let offerPool = [];
if (this.isLinkedSearch(searchRequest) || this.isRequiredSearch(searchRequest))
{
if (this.isLinkedSearch(searchRequest) || this.isRequiredSearch(searchRequest)) {
offerPool = offers;
}
else if (!(this.isLinkedSearch(searchRequest) || this.isRequiredSearch(searchRequest)))
{
} else if (!(this.isLinkedSearch(searchRequest) || this.isRequiredSearch(searchRequest))) {
// Get all categories
offerPool = this.ragfairOfferService.getOffers();
}
else
{
} else {
this.logger.error(this.localisationService.getText("ragfair-unable_to_get_categories"));
this.logger.debug(JSON.stringify(searchRequest));
return {};
@ -233,12 +215,10 @@ export class RagfairController
* Add index to all offers passed in (0-indexed)
* @param offers Offers to add index value to
*/
protected addIndexValueToOffers(offers: IRagfairOffer[]): void
{
protected addIndexValueToOffers(offers: IRagfairOffer[]): void {
let counter = 0;
for (const offer of offers)
{
for (const offer of offers) {
offer.intId = ++counter;
offer.items[0].parentId = ""; // Without this it causes error: "Item deserialization error: No parent with id hideout found for item x"
}
@ -249,8 +229,7 @@ export class RagfairController
* @param offer Flea offer to update
* @param fullProfile Players full profile
*/
protected setTraderOfferPurchaseLimits(offer: IRagfairOffer, fullProfile: ISptProfile): void
{
protected setTraderOfferPurchaseLimits(offer: IRagfairOffer, fullProfile: ISptProfile): void {
// No trader found, create a blank record for them
fullProfile.traderPurchases[offer.user.id] ||= {};
@ -270,14 +249,12 @@ export class RagfairController
* Adjust ragfair offer stack count to match same value as traders assort stack count
* @param offer Flea offer to adjust stack size of
*/
protected setTraderOfferStackSize(offer: IRagfairOffer): void
{
protected setTraderOfferStackSize(offer: IRagfairOffer): void {
const firstItem = offer.items[0];
const traderAssorts = this.traderHelper.getTraderAssortsByTraderId(offer.user.id).items;
const assortPurchased = traderAssorts.find((x) => x._id === offer.items[0]._id);
if (!assortPurchased)
{
if (!assortPurchased) {
this.logger.warning(
this.localisationService.getText("ragfair-unable_to_adjust_stack_count_assort_not_found", {
offerId: offer.items[0]._id,
@ -296,8 +273,7 @@ export class RagfairController
* @param info Search request
* @returns True if it is a 'linked' search type
*/
protected isLinkedSearch(info: ISearchRequestData): boolean
{
protected isLinkedSearch(info: ISearchRequestData): boolean {
return info.linkedSearchId !== "";
}
@ -306,26 +282,22 @@ export class RagfairController
* @param info Search request
* @returns True if it is a 'required' search type
*/
protected isRequiredSearch(info: ISearchRequestData): boolean
{
protected isRequiredSearch(info: ISearchRequestData): boolean {
return info.neededSearchId !== "";
}
/**
* Check all profiles and sell player offers / send player money for listing if it sold
*/
public update(): void
{
public update(): void {
const profilesDict = this.saveServer.getProfiles();
for (const sessionID in this.saveServer.getProfiles())
{
for (const sessionID in this.saveServer.getProfiles()) {
// Check profile is capable of creating offers
const pmcProfile = profilesDict[sessionID].characters.pmc;
if (
pmcProfile.RagfairInfo !== undefined
&& pmcProfile.Info.Level >= this.databaseService.getGlobals().config.RagFair.minUserLevel
)
{
pmcProfile.RagfairInfo !== undefined &&
pmcProfile.Info.Level >= this.databaseService.getGlobals().config.RagFair.minUserLevel
) {
this.ragfairOfferHelper.processOffersOnProfile(sessionID);
}
}
@ -336,26 +308,22 @@ export class RagfairController
* @param getPriceRequest
* @returns min/avg/max values for an item based on flea offers available
*/
public getItemMinAvgMaxFleaPriceValues(getPriceRequest: IGetMarketPriceRequestData): IGetItemPriceResult
{
public getItemMinAvgMaxFleaPriceValues(getPriceRequest: IGetMarketPriceRequestData): IGetItemPriceResult {
// Get all items of tpl
const offers = this.ragfairOfferService.getOffersOfType(getPriceRequest.templateId);
// Offers exist for item, get averages of what's listed
if (typeof offers === "object" && offers.length > 0)
{
if (typeof offers === "object" && offers.length > 0) {
// These get calculated while iterating through the list below
let min = Number.MAX_VALUE;
let max = 0;
// Get the average offer price, excluding barter offers
let avgOfferCount = 0;
const avg
= offers.reduce((sum, offer) =>
{
const avg =
offers.reduce((sum, offer) => {
// Exclude barter items, they tend to have outrageous equivalent prices
if (offer.requirements.some((req) => !this.paymentHelper.isMoneyTpl(req._tpl)))
{
if (offer.requirements.some((req) => !this.paymentHelper.isMoneyTpl(req._tpl))) {
return sum;
}
@ -366,12 +334,9 @@ export class RagfairController
const perItemPrice = offer.requirementsCost / offerItemCount;
// Handle min/max calculations based on the per-item price
if (perItemPrice < min)
{
if (perItemPrice < min) {
min = perItemPrice;
}
else if (perItemPrice > max)
{
} else if (perItemPrice > max) {
max = perItemPrice;
}
@ -380,8 +345,7 @@ export class RagfairController
}, 0) / Math.max(avgOfferCount, 1);
// If no items were actually counted, min will still be MAX_VALUE, so set it to 0
if (min === Number.MAX_VALUE)
{
if (min === Number.MAX_VALUE) {
min = 0;
}
@ -390,8 +354,7 @@ export class RagfairController
// No offers listed, get price from live ragfair price list prices.json
let tplPrice = this.databaseService.getPrices()[getPriceRequest.templateId];
if (!tplPrice)
{
if (!tplPrice) {
// No flea price, get handbook price
tplPrice = this.handbookHelper.getTemplatePrice(getPriceRequest.templateId);
}
@ -410,25 +373,21 @@ export class RagfairController
pmcData: IPmcData,
offerRequest: IAddOfferRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionID);
const fullProfile = this.saveServer.getProfile(sessionID);
const validationMessage = "";
if (!this.isValidPlayerOfferRequest(offerRequest, validationMessage))
{
if (!this.isValidPlayerOfferRequest(offerRequest, validationMessage)) {
return this.httpResponse.appendErrorToOutput(output, validationMessage);
}
const typeOfOffer = this.getOfferType(offerRequest);
if (typeOfOffer === FleaOfferType.UNKNOWN)
{
if (typeOfOffer === FleaOfferType.UNKNOWN) {
return this.httpResponse.appendErrorToOutput(output, "Unknown offer type, cannot list item on flea");
}
switch (typeOfOffer)
{
switch (typeOfOffer) {
case FleaOfferType.SINGLE:
return this.createSingleOffer(sessionID, offerRequest, fullProfile, output);
case FleaOfferType.MULTI:
@ -451,16 +410,15 @@ export class RagfairController
sessionID: string,
offerRequest: IAddOfferRequestData,
fullProfile: ISptProfile,
output: IItemEventRouterResponse): IItemEventRouterResponse
{
output: IItemEventRouterResponse,
): IItemEventRouterResponse {
const pmcData = fullProfile.characters.pmc;
const itemsToListCount = offerRequest.items.length; // Does not count stack size, only items
// Find items to be listed on flea from player inventory
const { items: itemsAndChildrenInInventoryToList, errorMessage: itemsInInventoryError }
= this.getItemsToListOnFleaFromInventory(pmcData, offerRequest.items);
if (!itemsAndChildrenInInventoryToList || itemsInInventoryError)
{
const { items: itemsAndChildrenInInventoryToList, errorMessage: itemsInInventoryError } =
this.getItemsToListOnFleaFromInventory(pmcData, offerRequest.items);
if (!itemsAndChildrenInInventoryToList || itemsInInventoryError) {
this.httpResponse.appendErrorToOutput(output, itemsInInventoryError);
}
@ -485,8 +443,7 @@ export class RagfairController
// Check for and apply item price modifer if it exists in config
const itemPriceModifer = this.ragfairConfig.dynamic.itemPriceMultiplier[rootItem._tpl];
if (itemPriceModifer)
{
if (itemPriceModifer) {
averageOfferPriceSingleItem *= itemPriceModifer;
}
@ -502,8 +459,7 @@ export class RagfairController
offer.sellResult = this.ragfairSellHelper.rollForSale(sellChancePercent, stackCountTotal);
// Subtract flea market fee from stash
if (this.ragfairConfig.sell.fees)
{
if (this.ragfairConfig.sell.fees) {
const taxFeeChargeFailed = this.chargePlayerTaxFee(
sessionID,
rootItem,
@ -513,8 +469,7 @@ export class RagfairController
offerRequest,
output,
);
if (taxFeeChargeFailed)
{
if (taxFeeChargeFailed) {
return output;
}
}
@ -524,8 +479,7 @@ export class RagfairController
output.profileChanges[sessionID].ragFairOffers.push(offer);
// Remove items from inventory after creating offer
for (const itemToRemove of offerRequest.items)
{
for (const itemToRemove of offerRequest.items) {
this.inventoryHelper.removeItem(pmcData, itemToRemove, sessionID, output);
}
@ -546,8 +500,8 @@ export class RagfairController
sessionID: string,
offerRequest: IAddOfferRequestData,
fullProfile: ISptProfile,
output: IItemEventRouterResponse): IItemEventRouterResponse
{
output: IItemEventRouterResponse,
): IItemEventRouterResponse {
const pmcData = fullProfile.characters.pmc;
const itemsToListCount = offerRequest.items.length; // Does not count stack size, only items
@ -555,13 +509,13 @@ export class RagfairController
// Get first item and its children and use as template
const firstListingAndChidren = this.itemHelper.findAndReturnChildrenAsItems(
pmcData.Inventory.items,
offerRequest.items[0]);
offerRequest.items[0],
);
// Find items to be listed on flea (+ children) from player inventory
const { items: itemsAndChildrenInInventoryToList, errorMessage: itemsInInventoryError }
= this.getItemsToListOnFleaFromInventory(pmcData, offerRequest.items);
if (!itemsAndChildrenInInventoryToList || itemsInInventoryError)
{
const { items: itemsAndChildrenInInventoryToList, errorMessage: itemsInInventoryError } =
this.getItemsToListOnFleaFromInventory(pmcData, offerRequest.items);
if (!itemsAndChildrenInInventoryToList || itemsInInventoryError) {
this.httpResponse.appendErrorToOutput(output, itemsInInventoryError);
}
@ -570,19 +524,13 @@ export class RagfairController
// When listing identical items on flea, condense separate items into one stack with a merged stack count
// e.g. 2 ammo items, stackObjectCount = 3 for each, will result in 1 stack of 6
if (!firstListingAndChidren[0].upd)
{
if (!firstListingAndChidren[0].upd) {
firstListingAndChidren[0].upd = {};
}
firstListingAndChidren[0].upd.StackObjectsCount = stackCountTotal;
// Create flea object
const offer = this.createPlayerOffer(
sessionID,
offerRequest.requirements,
firstListingAndChidren,
false,
);
const offer = this.createPlayerOffer(sessionID, offerRequest.requirements, firstListingAndChidren, false);
// This is the item that will be listed on flea, has merged stackObjectCount
const newRootOfferItem = offer.items[0];
@ -592,8 +540,7 @@ export class RagfairController
// Check for and apply item price modifer if it exists in config
const itemPriceModifer = this.ragfairConfig.dynamic.itemPriceMultiplier[newRootOfferItem._tpl];
if (itemPriceModifer)
{
if (itemPriceModifer) {
averageOfferPrice *= itemPriceModifer;
}
@ -617,8 +564,7 @@ export class RagfairController
offer.sellResult = this.ragfairSellHelper.rollForSale(sellChancePercent, stackCountTotal);
// Subtract flea market fee from stash
if (this.ragfairConfig.sell.fees)
{
if (this.ragfairConfig.sell.fees) {
const taxFeeChargeFailed = this.chargePlayerTaxFee(
sessionID,
newRootOfferItem,
@ -628,8 +574,7 @@ export class RagfairController
offerRequest,
output,
);
if (taxFeeChargeFailed)
{
if (taxFeeChargeFailed) {
return output;
}
}
@ -639,8 +584,7 @@ export class RagfairController
output.profileChanges[sessionID].ragFairOffers.push(offer);
// Remove items from inventory after creating offer
for (const itemToRemove of offerRequest.items)
{
for (const itemToRemove of offerRequest.items) {
this.inventoryHelper.removeItem(pmcData, itemToRemove, sessionID, output);
}
@ -661,8 +605,8 @@ export class RagfairController
sessionID: string,
offerRequest: IAddOfferRequestData,
fullProfile: ISptProfile,
output: IItemEventRouterResponse): IItemEventRouterResponse
{
output: IItemEventRouterResponse,
): IItemEventRouterResponse {
const pmcData = fullProfile.characters.pmc;
const itemsToListCount = offerRequest.items.length; // Does not count stack size, only items
@ -670,13 +614,13 @@ export class RagfairController
// Get first item and its children and use as template
const firstListingAndChidren = this.itemHelper.findAndReturnChildrenAsItems(
pmcData.Inventory.items,
offerRequest.items[0]);
offerRequest.items[0],
);
// Find items to be listed on flea (+ children) from player inventory
const { items: itemsAndChildrenInInventoryToList, errorMessage: itemsInInventoryError }
= this.getItemsToListOnFleaFromInventory(pmcData, offerRequest.items);
if (!itemsAndChildrenInInventoryToList || itemsInInventoryError)
{
const { items: itemsAndChildrenInInventoryToList, errorMessage: itemsInInventoryError } =
this.getItemsToListOnFleaFromInventory(pmcData, offerRequest.items);
if (!itemsAndChildrenInInventoryToList || itemsInInventoryError) {
this.httpResponse.appendErrorToOutput(output, itemsInInventoryError);
}
@ -685,19 +629,13 @@ export class RagfairController
// When listing identical items on flea, condense separate items into one stack with a merged stack count
// e.g. 2 ammo items, stackObjectCount = 3 for each, will result in 1 stack of 6
if (!firstListingAndChidren[0].upd)
{
if (!firstListingAndChidren[0].upd) {
firstListingAndChidren[0].upd = {};
}
firstListingAndChidren[0].upd.StackObjectsCount = stackCountTotal;
// Create flea object
const offer = this.createPlayerOffer(
sessionID,
offerRequest.requirements,
firstListingAndChidren,
true,
);
const offer = this.createPlayerOffer(sessionID, offerRequest.requirements, firstListingAndChidren, true);
// This is the item that will be listed on flea, has merged stackObjectCount
const newRootOfferItem = offer.items[0];
@ -707,8 +645,7 @@ export class RagfairController
// Check for and apply item price modifer if it exists in config
const itemPriceModifer = this.ragfairConfig.dynamic.itemPriceMultiplier[newRootOfferItem._tpl];
if (itemPriceModifer)
{
if (itemPriceModifer) {
singleItemPrice *= itemPriceModifer;
}
@ -732,8 +669,7 @@ export class RagfairController
offer.sellResult = this.ragfairSellHelper.rollForSale(sellChancePercent, stackCountTotal, true);
// Subtract flea market fee from stash
if (this.ragfairConfig.sell.fees)
{
if (this.ragfairConfig.sell.fees) {
const taxFeeChargeFailed = this.chargePlayerTaxFee(
sessionID,
newRootOfferItem,
@ -743,8 +679,7 @@ export class RagfairController
offerRequest,
output,
);
if (taxFeeChargeFailed)
{
if (taxFeeChargeFailed) {
return output;
}
}
@ -754,8 +689,7 @@ export class RagfairController
output.profileChanges[sessionID].ragFairOffers.push(offer);
// Remove items from inventory after creating offer
for (const itemToRemove of offerRequest.items)
{
for (const itemToRemove of offerRequest.items) {
this.inventoryHelper.removeItem(pmcData, itemToRemove, sessionID, output);
}
@ -768,18 +702,12 @@ export class RagfairController
* @param offerRequest Client request
* @returns FleaOfferType
*/
protected getOfferType(offerRequest: IAddOfferRequestData): FleaOfferType
{
if (offerRequest.items.length == 1 && !offerRequest.sellInOnePiece)
{
protected getOfferType(offerRequest: IAddOfferRequestData): FleaOfferType {
if (offerRequest.items.length == 1 && !offerRequest.sellInOnePiece) {
return FleaOfferType.SINGLE;
}
else if (offerRequest.items.length > 1 && !offerRequest.sellInOnePiece)
{
} else if (offerRequest.items.length > 1 && !offerRequest.sellInOnePiece) {
return FleaOfferType.MULTI;
}
else if (offerRequest.sellInOnePiece)
{
} else if (offerRequest.sellInOnePiece) {
return FleaOfferType.PACK;
}
@ -805,8 +733,7 @@ export class RagfairController
itemStackCount: number,
offerRequest: IAddOfferRequestData,
output: IItemEventRouterResponse,
): boolean
{
): boolean {
// Get tax from cache hydrated earlier by client, if that's missing fall back to server calculation (inaccurate)
const storedClientTaxValue = this.ragfairTaxService.getStoredClientOfferTaxValueById(offerRequest.items[0]);
const tax = storedClientTaxValue
@ -826,8 +753,7 @@ export class RagfairController
const buyTradeRequest = this.createBuyTradeRequestObject("RUB", tax);
this.paymentService.payMoney(pmcData, buyTradeRequest, sessionID, output);
if (output.warnings.length > 0)
{
if (output.warnings.length > 0) {
this.httpResponse.appendErrorToOutput(
output,
this.localisationService.getText("ragfair-unable_to_pay_commission_fee", tax),
@ -844,17 +770,14 @@ export class RagfairController
* @param errorMessage message to show to player when offer is invalid
* @returns Is offer valid
*/
protected isValidPlayerOfferRequest(offerRequest: IAddOfferRequestData, errorMessage: string): boolean
{
if (!offerRequest?.items || offerRequest.items.length === 0)
{
protected isValidPlayerOfferRequest(offerRequest: IAddOfferRequestData, errorMessage: string): boolean {
if (!offerRequest?.items || offerRequest.items.length === 0) {
this.logger.error(this.localisationService.getText("ragfair-invalid_player_offer_request"));
return false;
}
if (!offerRequest.requirements)
{
if (!offerRequest.requirements) {
this.logger.error(this.localisationService.getText("ragfair-unable_to_place_offer_with_no_requirements"));
return false;
@ -868,21 +791,16 @@ export class RagfairController
* @param requirements
* @returns Rouble price
*/
protected calculateRequirementsPriceInRub(requirements: Requirement[]): number
{
protected calculateRequirementsPriceInRub(requirements: Requirement[]): number {
let requirementsPriceInRub = 0;
for (const item of requirements)
{
for (const item of requirements) {
const requestedItemTpl = item._tpl;
if (this.paymentHelper.isMoneyTpl(requestedItemTpl))
{
if (this.paymentHelper.isMoneyTpl(requestedItemTpl)) {
requirementsPriceInRub += this.handbookHelper.inRUB(item.count, requestedItemTpl);
}
else
{
requirementsPriceInRub
+= this.ragfairPriceService.getDynamicPriceForItem(requestedItemTpl) * item.count;
} else {
requirementsPriceInRub +=
this.ragfairPriceService.getDynamicPriceForItem(requestedItemTpl) * item.count;
}
}
@ -898,17 +816,14 @@ export class RagfairController
protected getItemsToListOnFleaFromInventory(
pmcData: IPmcData,
itemIdsFromFleaOfferRequest: string[],
): { items: Item[][] | undefined, errorMessage: string | undefined }
{
): { items: Item[][] | undefined; errorMessage: string | undefined } {
const itemsToReturn: Item[][] = [];
let errorMessage: string | undefined = undefined;
// Count how many items are being sold and multiply the requested amount accordingly
for (const itemId of itemIdsFromFleaOfferRequest)
{
for (const itemId of itemIdsFromFleaOfferRequest) {
let item = pmcData.Inventory.items.find((i) => i._id === itemId);
if (!item)
{
if (!item) {
errorMessage = this.localisationService.getText("ragfair-unable_to_find_item_in_inventory", {
id: itemId,
});
@ -921,8 +836,7 @@ export class RagfairController
itemsToReturn.push(this.itemHelper.findAndReturnChildrenAsItems(pmcData.Inventory.items, itemId));
}
if (!itemsToReturn?.length)
{
if (!itemsToReturn?.length) {
errorMessage = this.localisationService.getText("ragfair-unable_to_find_requested_items_in_inventory");
this.logger.error(errorMessage);
@ -937,11 +851,9 @@ export class RagfairController
requirements: Requirement[],
items: Item[],
sellInOnePiece: boolean,
): IRagfairOffer
{
): IRagfairOffer {
const loyalLevel = 1;
const formattedItems: Item[] = items.map((item) =>
{
const formattedItems: Item[] = items.map((item) => {
const isChild = items.some((subItem) => subItem._id === item.parentId);
return {
@ -953,12 +865,12 @@ export class RagfairController
};
});
const formattedRequirements: IBarterScheme[] = requirements.map((item) =>
{
const formattedRequirements: IBarterScheme[] = requirements.map((item) => {
return {
_tpl: item._tpl,
count: item.count,
onlyFunctional: item.onlyFunctional };
onlyFunctional: item.onlyFunctional,
};
});
return this.ragfairOfferGenerator.createAndAddFleaOffer(
@ -971,13 +883,11 @@ export class RagfairController
);
}
public getAllFleaPrices(): Record<string, number>
{
public getAllFleaPrices(): Record<string, number> {
return this.ragfairPriceService.getAllFleaPrices();
}
public getStaticPrices(): Record<string, number>
{
public getStaticPrices(): Record<string, number> {
return this.ragfairPriceService.getAllStaticPrices();
}
@ -988,14 +898,12 @@ export class RagfairController
* @param sessionId Players id
* @returns IItemEventRouterResponse
*/
public removeOffer(removeRequest: IRemoveOfferRequestData, sessionId: string): IItemEventRouterResponse
{
public removeOffer(removeRequest: IRemoveOfferRequestData, sessionId: string): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionId);
const pmcData = this.saveServer.getProfile(sessionId).characters.pmc;
const playerProfileOffers = pmcData.RagfairInfo.offers;
if (!playerProfileOffers)
{
if (!playerProfileOffers) {
this.logger.warning(
this.localisationService.getText("ragfair-unable_to_remove_offer_not_found_in_profile", {
profileId: sessionId,
@ -1007,8 +915,7 @@ export class RagfairController
}
const playerOfferIndex = playerProfileOffers.findIndex((offer) => offer._id === removeRequest.offerId);
if (playerOfferIndex === -1)
{
if (playerOfferIndex === -1) {
this.logger.error(
this.localisationService.getText("ragfair-offer_not_found_in_profile", {
offerId: removeRequest.offerId,
@ -1021,8 +928,7 @@ export class RagfairController
}
const differenceInSeconds = playerProfileOffers[playerOfferIndex].endTime - this.timeUtil.getTimestamp();
if (differenceInSeconds > this.ragfairConfig.sell.expireSeconds)
{
if (differenceInSeconds > this.ragfairConfig.sell.expireSeconds) {
// `expireSeconds` Default is 71 seconds
const newEndTime = this.ragfairConfig.sell.expireSeconds + this.timeUtil.getTimestamp();
playerProfileOffers[playerOfferIndex].endTime = Math.round(newEndTime);
@ -1037,8 +943,7 @@ export class RagfairController
* @param sessionId Players id
* @returns IItemEventRouterResponse
*/
public extendOffer(extendRequest: IExtendOfferRequestData, sessionId: string): IItemEventRouterResponse
{
public extendOffer(extendRequest: IExtendOfferRequestData, sessionId: string): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionId);
const pmcData = this.saveServer.getProfile(sessionId).characters.pmc;
@ -1046,8 +951,7 @@ export class RagfairController
const playerOfferIndex = playerOffers.findIndex((offer) => offer._id === extendRequest.offerId);
const secondsToAdd = extendRequest.renewalTime * TimeUtil.ONE_HOUR_AS_SECONDS;
if (playerOfferIndex === -1)
{
if (playerOfferIndex === -1) {
this.logger.warning(
this.localisationService.getText("ragfair-offer_not_found_in_profile", {
offerId: extendRequest.offerId,
@ -1060,12 +964,10 @@ export class RagfairController
}
// MOD: Pay flea market fee
if (this.ragfairConfig.sell.fees)
{
if (this.ragfairConfig.sell.fees) {
const count = playerOffers[playerOfferIndex].sellInOnePiece
? 1
: playerOffers[playerOfferIndex].items.reduce((sum, item) =>
{
: playerOffers[playerOfferIndex].items.reduce((sum, item) => {
return sum + item.upd.StackObjectsCount;
}, 0);
@ -1079,8 +981,7 @@ export class RagfairController
const request = this.createBuyTradeRequestObject("RUB", tax);
this.paymentService.payMoney(pmcData, request, sessionId, output);
if (output.warnings.length > 0)
{
if (output.warnings.length > 0) {
return this.httpResponse.appendErrorToOutput(
output,
this.localisationService.getText("ragfair-unable_to_pay_commission_fee"),
@ -1100,8 +1001,7 @@ export class RagfairController
* @param value Amount of currency
* @returns IProcessBuyTradeRequestData
*/
protected createBuyTradeRequestObject(currency: string, value: number): IProcessBuyTradeRequestData
{
protected createBuyTradeRequestObject(currency: string, value: number): IProcessBuyTradeRequestData {
return {
tid: "ragfair",
Action: "TradingConfirm",

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { QuestHelper } from "@spt/helpers/QuestHelper";
import { RepairHelper } from "@spt/helpers/RepairHelper";
@ -13,10 +12,10 @@ import { EventOutputHolder } from "@spt/routers/EventOutputHolder";
import { DatabaseService } from "@spt/services/DatabaseService";
import { PaymentService } from "@spt/services/PaymentService";
import { RepairService } from "@spt/services/RepairService";
import { inject, injectable } from "tsyringe";
@injectable()
export class RepairController
{
export class RepairController {
protected repairConfig: IRepairConfig;
constructor(
@ -29,8 +28,7 @@ export class RepairController
@inject("RepairHelper") protected repairHelper: RepairHelper,
@inject("RepairService") protected repairService: RepairService,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
)
{}
) {}
/**
* Handle TraderRepair event
@ -44,13 +42,11 @@ export class RepairController
sessionID: string,
body: ITraderRepairActionDataRequest,
pmcData: IPmcData,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionID);
// find the item to repair
for (const repairItem of body.repairItems)
{
for (const repairItem of body.repairItems) {
const repairDetails = this.repairService.repairItemByTrader(sessionID, pmcData, repairItem, body.tid);
this.repairService.payForRepair(
@ -62,8 +58,7 @@ export class RepairController
output,
);
if (output.warnings.length > 0)
{
if (output.warnings.length > 0) {
return output;
}
@ -89,8 +84,7 @@ export class RepairController
sessionID: string,
body: IRepairActionDataRequest,
pmcData: IPmcData,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionID);
// repair item

View File

@ -1,13 +1,9 @@
import { inject, injectable } from "tsyringe";
import { RepeatableQuestGenerator } from "@spt/generators/RepeatableQuestGenerator";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { QuestHelper } from "@spt/helpers/QuestHelper";
import { RepeatableQuestHelper } from "@spt/helpers/RepeatableQuestHelper";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import {
IPmcDataRepeatableQuest,
IRepeatableQuest,
} from "@spt/models/eft/common/tables/IRepeatableQuests";
import { IPmcDataRepeatableQuest, IRepeatableQuest } from "@spt/models/eft/common/tables/IRepeatableQuests";
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { ISptProfile } from "@spt/models/eft/profile/ISptProfile";
import { IRepeatableQuestChangeRequest } from "@spt/models/eft/quests/IRepeatableQuestChangeRequest";
@ -26,15 +22,15 @@ import { DatabaseService } from "@spt/services/DatabaseService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { PaymentService } from "@spt/services/PaymentService";
import { ProfileFixerService } from "@spt/services/ProfileFixerService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { ObjectId } from "@spt/utils/ObjectId";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class RepeatableQuestController
{
export class RepeatableQuestController {
protected questConfig: IQuestConfig;
constructor(
@ -54,8 +50,7 @@ export class RepeatableQuestController
@inject("QuestHelper") protected questHelper: QuestHelper,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
) {
this.questConfig = this.configServer.getConfig(ConfigTypes.QUEST);
}
@ -84,30 +79,26 @@ export class RepeatableQuestController
*
* @returns {array} Array of "repeatableQuestObjects" as described above
*/
public getClientRepeatableQuests(sessionID: string): IPmcDataRepeatableQuest[]
{
public getClientRepeatableQuests(sessionID: string): IPmcDataRepeatableQuest[] {
const returnData: Array<IPmcDataRepeatableQuest> = [];
const fullProfile = this.profileHelper.getFullProfile(sessionID)!;
const pmcData = fullProfile.characters.pmc;
const currentTime = this.timeUtil.getTimestamp();
// Daily / weekly / Daily_Savage
for (const repeatableConfig of this.questConfig.repeatableQuests)
{
for (const repeatableConfig of this.questConfig.repeatableQuests) {
// Get daily/weekly data from profile, add empty object if missing
const generatedRepeatables = this.getRepeatableQuestSubTypeFromProfile(repeatableConfig, pmcData);
const repeatableTypeLower = repeatableConfig.name.toLowerCase();
const canAccessRepeatables = this.canProfileAccessRepeatableQuests(repeatableConfig, pmcData);
if (!canAccessRepeatables)
{
if (!canAccessRepeatables) {
// Dont send any repeatables, even existing ones
continue;
}
// Existing repeatables are still valid, add to return data and move to next sub-type
if (currentTime < generatedRepeatables.endTime - 1)
{
if (currentTime < generatedRepeatables.endTime - 1) {
returnData.push(generatedRepeatables);
this.logger.debug(`[Quest Check] ${repeatableTypeLower} quests are still valid.`);
@ -132,12 +123,10 @@ export class RepeatableQuestController
const questTypePool = this.generateQuestPool(repeatableConfig, pmcData.Info.Level);
// Add repeatable quests of this loops sub-type (daily/weekly)
for (let i = 0; i < this.getQuestCount(repeatableConfig, pmcData); i++)
{
for (let i = 0; i < this.getQuestCount(repeatableConfig, pmcData); i++) {
let quest: IRepeatableQuest | undefined = undefined;
let lifeline = 0;
while (!quest && questTypePool.types.length > 0)
{
while (!quest && questTypePool.types.length > 0) {
quest = this.repeatableQuestGenerator.generateRepeatableQuest(
pmcData.Info.Level,
pmcData.TradersInfo,
@ -145,8 +134,7 @@ export class RepeatableQuestController
repeatableConfig,
);
lifeline++;
if (lifeline > 10)
{
if (lifeline > 10) {
this.logger.debug(
"We were stuck in repeatable quest generation. This should never happen. Please report",
);
@ -155,8 +143,7 @@ export class RepeatableQuestController
}
// check if there are no more quest types available
if (questTypePool.types.length === 0)
{
if (questTypePool.types.length === 0) {
break;
}
quest.side = repeatableConfig.side;
@ -170,8 +157,7 @@ export class RepeatableQuestController
fullProfile.spt.freeRepeatableRefreshUsedCount[repeatableTypeLower] = 0;
// Create stupid redundant change requirements from quest data
for (const quest of generatedRepeatables.activeQuests)
{
for (const quest of generatedRepeatables.activeQuests) {
generatedRepeatables.changeRequirement[quest._id] = {
changeCost: quest.changeCost,
changeStandingCost: this.randomUtil.getArrayValue([0, 0.01]), // Randomise standing cost to replace
@ -202,20 +188,16 @@ export class RepeatableQuestController
* @param generatedRepeatables Repeatables to process (daily/weekly)
* @param pmcData Player profile
*/
protected processExpiredQuests(generatedRepeatables: IPmcDataRepeatableQuest, pmcData: IPmcData): void
{
protected processExpiredQuests(generatedRepeatables: IPmcDataRepeatableQuest, pmcData: IPmcData): void {
const questsToKeep = [];
for (const activeQuest of generatedRepeatables.activeQuests)
{
for (const activeQuest of generatedRepeatables.activeQuests) {
const questStatusInProfile = pmcData.Quests.find((quest) => quest.qid === activeQuest._id);
if (!questStatusInProfile)
{
if (!questStatusInProfile) {
continue;
}
// Keep finished quests in list so player can hand in
if (questStatusInProfile.status === QuestStatus.AvailableForFinish)
{
if (questStatusInProfile.status === QuestStatus.AvailableForFinish) {
questsToKeep.push(activeQuest);
this.logger.debug(
`Keeping repeatable quest: ${activeQuest._id} in activeQuests since it is available to hand in`,
@ -243,17 +225,14 @@ export class RepeatableQuestController
* @param pmcData Player profile
* @returns True if profile is allowed to access dailies
*/
protected canProfileAccessRepeatableQuests(repeatableConfig: IRepeatableQuestConfig, pmcData: IPmcData): boolean
{
protected canProfileAccessRepeatableQuests(repeatableConfig: IRepeatableQuestConfig, pmcData: IPmcData): boolean {
// PMC and daily quests not unlocked yet
if (repeatableConfig.side === "Pmc" && !this.playerHasDailyPmcQuestsUnlocked(pmcData, repeatableConfig))
{
if (repeatableConfig.side === "Pmc" && !this.playerHasDailyPmcQuestsUnlocked(pmcData, repeatableConfig)) {
return false;
}
// Scav and daily quests not unlocked yet
if (repeatableConfig.side === "Scav" && !this.playerHasDailyScavQuestsUnlocked(pmcData))
{
if (repeatableConfig.side === "Scav" && !this.playerHasDailyScavQuestsUnlocked(pmcData)) {
this.logger.debug("Daily scav quests still locked, Intel center not built");
return false;
@ -267,11 +246,10 @@ export class RepeatableQuestController
* @param pmcData Player profile to check
* @returns True if unlocked
*/
protected playerHasDailyScavQuestsUnlocked(pmcData: IPmcData): boolean
{
return pmcData?.Hideout?.Areas
?.find((hideoutArea) => hideoutArea.type === HideoutAreas.INTEL_CENTER)
?.level >= 1;
protected playerHasDailyScavQuestsUnlocked(pmcData: IPmcData): boolean {
return (
pmcData?.Hideout?.Areas?.find((hideoutArea) => hideoutArea.type === HideoutAreas.INTEL_CENTER)?.level >= 1
);
}
/**
@ -280,8 +258,7 @@ export class RepeatableQuestController
* @param repeatableConfig Config of daily type to check
* @returns True if unlocked
*/
protected playerHasDailyPmcQuestsUnlocked(pmcData: IPmcData, repeatableConfig: IRepeatableQuestConfig): boolean
{
protected playerHasDailyPmcQuestsUnlocked(pmcData: IPmcData, repeatableConfig: IRepeatableQuestConfig): boolean {
return pmcData.Info.Level >= repeatableConfig.minPlayerLevel;
}
@ -291,17 +268,15 @@ export class RepeatableQuestController
* @param pmcData Player profile
* @returns Quest count
*/
protected getQuestCount(repeatableConfig: IRepeatableQuestConfig, pmcData: IPmcData): number
{
protected getQuestCount(repeatableConfig: IRepeatableQuestConfig, pmcData: IPmcData): number {
if (
repeatableConfig.name.toLowerCase() === "daily"
&& this.profileHelper.hasEliteSkillLevel(SkillTypes.CHARISMA, pmcData)
)
{
repeatableConfig.name.toLowerCase() === "daily" &&
this.profileHelper.hasEliteSkillLevel(SkillTypes.CHARISMA, pmcData)
) {
// Elite charisma skill gives extra daily quest(s)
return (
repeatableConfig.numQuests
+ this.databaseService.getGlobals().config.SkillsSettings.Charisma.BonusSettings.EliteBonusSettings
repeatableConfig.numQuests +
this.databaseService.getGlobals().config.SkillsSettings.Charisma.BonusSettings.EliteBonusSettings
.RepeatableQuestExtraCount
);
}
@ -318,13 +293,13 @@ export class RepeatableQuestController
protected getRepeatableQuestSubTypeFromProfile(
repeatableConfig: IRepeatableQuestConfig,
pmcData: IPmcData,
): IPmcDataRepeatableQuest
{
): IPmcDataRepeatableQuest {
// Get from profile, add if missing
let repeatableQuestDetails = pmcData.RepeatableQuests
.find((repeatable) => repeatable.name === repeatableConfig.name);
if (!repeatableQuestDetails) // Not in profile, generate
{
let repeatableQuestDetails = pmcData.RepeatableQuests.find(
(repeatable) => repeatable.name === repeatableConfig.name,
);
if (!repeatableQuestDetails) {
// Not in profile, generate
const hasAccess = this.profileHelper.hasAccessToRepeatableFreeRefreshSystem(pmcData);
repeatableQuestDetails = {
id: repeatableConfig.id,
@ -347,13 +322,11 @@ export class RepeatableQuestController
/**
* Just for debug reasons. Draws dailies a random assort of dailies extracted from dumps
*/
public generateDebugDailies(dailiesPool: any, factory: any, number: number): any
{
public generateDebugDailies(dailiesPool: any, factory: any, number: number): any {
let randomQuests = [];
let numberOfQuests = number;
if (factory)
{
if (factory) {
// First is factory extract always add for debugging
randomQuests.push(dailiesPool[0]);
numberOfQuests -= 1;
@ -361,14 +334,11 @@ export class RepeatableQuestController
randomQuests = randomQuests.concat(this.randomUtil.drawRandomFromList(dailiesPool, numberOfQuests, false));
for (const element of randomQuests)
{
for (const element of randomQuests) {
element._id = this.objectId.generate();
const conditions = element.conditions.AvailableForFinish;
for (const condition of conditions)
{
if ("counter" in condition._props)
{
for (const condition of conditions) {
if ("counter" in condition._props) {
condition._props.counter.id = this.objectId.generate();
}
}
@ -384,18 +354,15 @@ export class RepeatableQuestController
* @param pmcLevel level of pmc generating quest pool
* @returns IQuestTypePool
*/
protected generateQuestPool(repeatableConfig: IRepeatableQuestConfig, pmcLevel: number): IQuestTypePool
{
protected generateQuestPool(repeatableConfig: IRepeatableQuestConfig, pmcLevel: number): IQuestTypePool {
const questPool = this.createBaseQuestPool(repeatableConfig);
// Get the allowed locations based on the PMC's level
const locations = this.getAllowedLocationsForPmcLevel(repeatableConfig.locations, pmcLevel);
// Populate Exploration and Pickup quest locations
for (const location in locations)
{
if (location !== ELocationName.ANY)
{
for (const location in locations) {
if (location !== ELocationName.ANY) {
questPool.pool.Exploration.locations[location] = locations[location];
questPool.pool.Pickup.locations[location] = locations[location];
}
@ -408,19 +375,16 @@ export class RepeatableQuestController
const targetsConfig = this.repeatableQuestHelper.probabilityObjectArray(eliminationConfig.targets);
// Populate Elimination quest targets and their locations
for (const { data: target, key: targetKey } of targetsConfig)
{
for (const { data: target, key: targetKey } of targetsConfig) {
// Target is boss
if (target.isBoss)
{
if (target.isBoss) {
questPool.pool.Elimination.targets[targetKey] = { locations: ["any"] };
}
else
{
} else {
// Non-boss targets
const possibleLocations = Object.keys(locations);
const allowedLocations = (targetKey === "Savage")
const allowedLocations =
targetKey === "Savage"
? possibleLocations.filter((location) => location !== "laboratory") // Exclude labs for Savage targets.
: possibleLocations;
@ -431,8 +395,7 @@ export class RepeatableQuestController
return questPool;
}
protected createBaseQuestPool(repeatableConfig: IRepeatableQuestConfig): IQuestTypePool
{
protected createBaseQuestPool(repeatableConfig: IRepeatableQuestConfig): IQuestTypePool {
return {
types: repeatableConfig.types.slice(),
pool: { Exploration: { locations: {} }, Elimination: { targets: {} }, Pickup: { locations: {} } },
@ -448,23 +411,18 @@ export class RepeatableQuestController
protected getAllowedLocationsForPmcLevel(
locations: Record<ELocationName, string[]>,
pmcLevel: number,
): Partial<Record<ELocationName, string[]>>
{
): Partial<Record<ELocationName, string[]>> {
const allowedLocation: Partial<Record<ELocationName, string[]>> = {};
for (const location in locations)
{
for (const location in locations) {
const locationNames = [];
for (const locationName of locations[location])
{
if (this.isPmcLevelAllowedOnLocation(locationName, pmcLevel))
{
for (const locationName of locations[location]) {
if (this.isPmcLevelAllowedOnLocation(locationName, pmcLevel)) {
locationNames.push(locationName);
}
}
if (locationNames.length > 0)
{
if (locationNames.length > 0) {
allowedLocation[location] = locationNames;
}
}
@ -478,36 +436,29 @@ export class RepeatableQuestController
* @param pmcLevel The level of the pmc
* @returns True if the given pmc level is allowed to access the given location
*/
protected isPmcLevelAllowedOnLocation(location: string, pmcLevel: number): boolean
{
protected isPmcLevelAllowedOnLocation(location: string, pmcLevel: number): boolean {
// All PMC levels are allowed for 'any' location requirement
if (location === ELocationName.ANY)
{
if (location === ELocationName.ANY) {
return true;
}
const locationBase = this.databaseService.getLocation(location.toLowerCase())?.base;
if (!locationBase)
{
if (!locationBase) {
return true;
}
return pmcLevel <= locationBase.RequiredPlayerLevelMax && pmcLevel >= locationBase.RequiredPlayerLevelMin;
}
public debugLogRepeatableQuestIds(pmcData: IPmcData): void
{
for (const repeatable of pmcData.RepeatableQuests)
{
public debugLogRepeatableQuestIds(pmcData: IPmcData): void {
for (const repeatable of pmcData.RepeatableQuests) {
const activeQuestsIds = [];
const inactiveQuestsIds = [];
for (const active of repeatable.activeQuests)
{
for (const active of repeatable.activeQuests) {
activeQuestsIds.push(active._id);
}
for (const inactive of repeatable.inactiveQuests)
{
for (const inactive of repeatable.inactiveQuests) {
inactiveQuestsIds.push(inactive._id);
}
@ -529,15 +480,16 @@ export class RepeatableQuestController
pmcData: IPmcData,
changeRequest: IRepeatableQuestChangeRequest,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionID);
const fullProfile = this.profileHelper.getFullProfile(sessionID);
// Check for existing quest in (daily/weekly/scav arrays)
const { quest: questToReplace, repeatableType: repeatablesInProfile }
= this.getRepeatableById(changeRequest.qid, pmcData);
const { quest: questToReplace, repeatableType: repeatablesInProfile } = this.getRepeatableById(
changeRequest.qid,
pmcData,
);
// Subtype name of quest - daily/weekly/scav
const repeatableTypeLower = repeatablesInProfile.name.toLowerCase();
@ -546,8 +498,9 @@ export class RepeatableQuestController
const replacedQuestTraderId = questToReplace.traderId;
// Update active quests to exclude the quest we're replacing
repeatablesInProfile.activeQuests = repeatablesInProfile.activeQuests
.filter((quest) => quest._id !== changeRequest.qid);
repeatablesInProfile.activeQuests = repeatablesInProfile.activeQuests.filter(
(quest) => quest._id !== changeRequest.qid,
);
// Save for later cost calculation
const previousChangeRequirement = this.cloner.clone(repeatablesInProfile.changeRequirement[changeRequest.qid]);
@ -556,15 +509,14 @@ export class RepeatableQuestController
delete repeatablesInProfile.changeRequirement[changeRequest.qid];
// Get config for this repeatable sub-type (daily/weekly/scav)
const repeatableConfig = this.questConfig.repeatableQuests
.find((config) => config.name === repeatablesInProfile.name,
const repeatableConfig = this.questConfig.repeatableQuests.find(
(config) => config.name === repeatablesInProfile.name,
);
// Generate meta-data for what type/levelrange of quests can be generated for player
const allowedQuestTypes = this.generateQuestPool(repeatableConfig, pmcData.Info.Level);
const newRepeatableQuest = this.attemptToGenerateRepeatableQuest(pmcData, allowedQuestTypes, repeatableConfig);
if (!newRepeatableQuest)
{
if (!newRepeatableQuest) {
// Unable to find quest being replaced
const message = `Unable to generate repeatable quest of type: ${repeatableTypeLower} to replace trader: ${replacedQuestTraderId} quest ${changeRequest.qid}`;
this.logger.error(message);
@ -593,20 +545,17 @@ export class RepeatableQuestController
// Check if we should charge player for replacing quest
const isFreeToReplace = this.useFreeRefreshIfAvailable(fullProfile, repeatablesInProfile, repeatableTypeLower);
if (!isFreeToReplace)
{
if (!isFreeToReplace) {
// Reduce standing with trader for not doing their quest
const traderOfReplacedQuest = pmcData.TradersInfo[replacedQuestTraderId];
traderOfReplacedQuest.standing -= previousChangeRequirement.changeStandingCost;
const charismaBonus = this.profileHelper.getSkillFromProfile(pmcData, SkillTypes.CHARISMA)?.Progress ?? 0;
for (const cost of previousChangeRequirement.changeCost)
{
for (const cost of previousChangeRequirement.changeCost) {
// Not free, Charge player + appy charisma bonus to cost of replacement
cost.count = Math.trunc(cost.count * (1 - (Math.trunc((charismaBonus) / 100) * 0.001)) ?? 1);
cost.count = Math.trunc(cost.count * (1 - Math.trunc(charismaBonus / 100) * 0.001) ?? 1);
this.paymentService.addPaymentToOutput(pmcData, cost.templateId, cost.count, sessionID, output);
if (output.warnings.length > 0)
{
if (output.warnings.length > 0) {
return output;
}
}
@ -618,8 +567,7 @@ export class RepeatableQuestController
// Purge inactive repeatables
repeatableToChangeClone.inactiveQuests = [];
if (!repeatableToChangeClone)
{
if (!repeatableToChangeClone) {
// Unable to find quest being replaced
const message = this.localisationService.getText("quest-unable_to_find_repeatable_to_replace");
this.logger.error(message);
@ -642,15 +590,11 @@ export class RepeatableQuestController
* @param pmcData Profile that contains quests to look through
* @returns IGetRepeatableByIdResult
*/
protected getRepeatableById(questId: string, pmcData: IPmcData): IGetRepeatableByIdResult
{
for (const repeatablesInProfile of pmcData.RepeatableQuests)
{
protected getRepeatableById(questId: string, pmcData: IPmcData): IGetRepeatableByIdResult {
for (const repeatablesInProfile of pmcData.RepeatableQuests) {
// Check for existing quest in (daily/weekly/scav arrays)
const questToReplace = repeatablesInProfile.activeQuests
.find((repeatable) => repeatable._id === questId);
if (!questToReplace)
{
const questToReplace = repeatablesInProfile.activeQuests.find((repeatable) => repeatable._id === questId);
if (!questToReplace) {
// Not found, skip to next repeatable sub-type
continue;
}
@ -665,14 +609,11 @@ export class RepeatableQuestController
pmcData: IPmcData,
questTypePool: IQuestTypePool,
repeatableConfig: IRepeatableQuestConfig,
): IRepeatableQuest
{
): IRepeatableQuest {
const maxAttempts = 10;
let newRepeatableQuest: IRepeatableQuest = undefined;
let attempts = 0;
while (attempts < maxAttempts
&& questTypePool.types.length > 0)
{
while (attempts < maxAttempts && questTypePool.types.length > 0) {
newRepeatableQuest = this.repeatableQuestGenerator.generateRepeatableQuest(
pmcData.Info.Level,
pmcData.TradersInfo,
@ -680,8 +621,7 @@ export class RepeatableQuestController
repeatableConfig,
);
if (newRepeatableQuest)
{
if (newRepeatableQuest) {
// Successfully generated a quest, exit loop
break;
}
@ -689,11 +629,8 @@ export class RepeatableQuestController
attempts++;
}
if (attempts > maxAttempts)
{
this.logger.debug(
"We were stuck in repeatable quest generation. This should never happen. Please report",
);
if (attempts > maxAttempts) {
this.logger.debug("We were stuck in repeatable quest generation. This should never happen. Please report");
}
return newRepeatableQuest;
@ -710,11 +647,10 @@ export class RepeatableQuestController
protected useFreeRefreshIfAvailable(
fullProfile: ISptProfile,
repeatableSubType: IPmcDataRepeatableQuest,
repeatableTypeName: string): boolean
{
repeatableTypeName: string,
): boolean {
// No free refreshes, exit early
if (repeatableSubType.freeChangesAvailable <= 0)
{
if (repeatableSubType.freeChangesAvailable <= 0) {
// Reset counter to 0
repeatableSubType.freeChangesAvailable = 0;
@ -722,12 +658,12 @@ export class RepeatableQuestController
}
// Only certain game versions have access to free refreshes
const hasAccessToFreeRefreshSystem
= this.profileHelper.hasAccessToRepeatableFreeRefreshSystem(fullProfile.characters.pmc);
const hasAccessToFreeRefreshSystem = this.profileHelper.hasAccessToRepeatableFreeRefreshSystem(
fullProfile.characters.pmc,
);
// If the player has access and available refreshes:
if (hasAccessToFreeRefreshSystem)
{
if (hasAccessToFreeRefreshSystem) {
// Initialize/retrieve free refresh count for the desired subtype: daily/weekly
fullProfile.spt.freeRepeatableRefreshUsedCount ||= {};
const repeatableRefreshCounts = fullProfile.spt.freeRepeatableRefreshUsedCount;

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { RagfairOfferHelper } from "@spt/helpers/RagfairOfferHelper";
@ -11,10 +10,7 @@ import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRo
import { IRagfairOffer } from "@spt/models/eft/ragfair/IRagfairOffer";
import { IProcessBaseTradeRequestData } from "@spt/models/eft/trade/IProcessBaseTradeRequestData";
import { IProcessBuyTradeRequestData } from "@spt/models/eft/trade/IProcessBuyTradeRequestData";
import {
IOfferRequest,
IProcessRagfairTradeRequestData,
} from "@spt/models/eft/trade/IProcessRagfairTradeRequestData";
import { IOfferRequest, IProcessRagfairTradeRequestData } from "@spt/models/eft/trade/IProcessRagfairTradeRequestData";
import { IProcessSellTradeRequestData } from "@spt/models/eft/trade/IProcessSellTradeRequestData";
import { ISellScavItemsToFenceRequestData } from "@spt/models/eft/trade/ISellScavItemsToFenceRequestData";
import { BackendErrorCodes } from "@spt/models/enums/BackendErrorCodes";
@ -37,10 +33,10 @@ import { HashUtil } from "@spt/utils/HashUtil";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class TradeController
{
export class TradeController {
protected ragfairConfig: IRagfairConfig;
protected traderConfig: ITraderConfig;
@ -62,8 +58,7 @@ export class TradeController
@inject("RagfairPriceService") protected ragfairPriceService: RagfairPriceService,
@inject("MailSendService") protected mailSendService: MailSendService,
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
) {
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
this.traderConfig = this.configServer.getConfig(ConfigTypes.TRADER);
}
@ -73,13 +68,11 @@ export class TradeController
pmcData: IPmcData,
request: IProcessBaseTradeRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionID);
// Buying
if (request.type === "buy_from_trader")
{
if (request.type === "buy_from_trader") {
const foundInRaid = this.traderConfig.purchasesAreFoundInRaid;
const buyData = <IProcessBuyTradeRequestData>request;
this.tradeHelper.buyItem(pmcData, buyData, sessionID, foundInRaid, output);
@ -88,8 +81,7 @@ export class TradeController
}
// Selling
if (request.type === "sell_to_trader")
{
if (request.type === "sell_to_trader") {
const sellData = <IProcessSellTradeRequestData>request;
this.tradeHelper.sellItem(pmcData, pmcData, sellData, sessionID, output);
@ -107,15 +99,12 @@ export class TradeController
pmcData: IPmcData,
request: IProcessRagfairTradeRequestData,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionID);
for (const offer of request.offers)
{
for (const offer of request.offers) {
const fleaOffer = this.ragfairServer.getOffer(offer.id);
if (!fleaOffer)
{
if (!fleaOffer) {
return this.httpResponse.appendErrorToOutput(
output,
`Offer with ID ${offer.id} not found`,
@ -123,8 +112,7 @@ export class TradeController
);
}
if (offer.count === 0)
{
if (offer.count === 0) {
const errorMessage = this.localisationService.getText(
"ragfair-unable_to_purchase_0_count_item",
this.itemHelper.getItem(fleaOffer.items[0]._tpl)[1]._name,
@ -133,18 +121,14 @@ export class TradeController
}
const sellerIsTrader = fleaOffer.user.memberType === MemberCategory.TRADER;
if (sellerIsTrader)
{
if (sellerIsTrader) {
this.buyTraderItemFromRagfair(sessionID, pmcData, fleaOffer, offer, output);
}
else
{
} else {
this.buyPmcItemFromRagfair(sessionID, pmcData, fleaOffer, offer, output);
}
// Exit loop early if problem found
if (output.warnings.length > 0)
{
if (output.warnings.length > 0) {
return output;
}
}
@ -166,11 +150,9 @@ export class TradeController
fleaOffer: IRagfairOffer,
requestOffer: IOfferRequest,
output: IItemEventRouterResponse,
): void
{
): void {
// Skip buying items when player doesn't have needed loyalty
if (this.playerLacksTraderLoyaltyLevelToBuyOffer(fleaOffer, pmcData))
{
if (this.playerLacksTraderLoyaltyLevelToBuyOffer(fleaOffer, pmcData)) {
const errorMessage = `Unable to buy item: ${fleaOffer.items[0]._tpl} from trader: ${fleaOffer.user.id} as loyalty level too low, skipping`;
this.logger.debug(errorMessage);
@ -206,8 +188,7 @@ export class TradeController
fleaOffer: IRagfairOffer,
requestOffer: IOfferRequest,
output: IItemEventRouterResponse,
): void
{
): void {
const buyData: IProcessBuyTradeRequestData = {
Action: "TradingConfirm",
type: "buy_from_ragfair",
@ -226,8 +207,7 @@ export class TradeController
this.ragfairConfig.dynamic.purchasesAreFoundInRaid,
output,
);
if (output.warnings.length > 0)
{
if (output.warnings.length > 0) {
return;
}
@ -236,8 +216,7 @@ export class TradeController
const offerBuyCount = requestOffer.count;
const isPlayerOffer = this.isPlayerOffer(fleaOffer._id, fleaOffer.user?.id);
if (isPlayerOffer)
{
if (isPlayerOffer) {
// Complete selling the offer now its been purchased
this.ragfairOfferHelper.completeOffer(offerOwnerId, fleaOffer, offerBuyCount);
@ -254,17 +233,14 @@ export class TradeController
* @param offerOwnerId Owner id
* @returns true if offer was made by a player
*/
protected isPlayerOffer(offerId: string, offerOwnerId: string): boolean
{
protected isPlayerOffer(offerId: string, offerOwnerId: string): boolean {
// No ownerid, not player offer
if (!offerOwnerId)
{
if (!offerOwnerId) {
return false;
}
const offerCreatorProfile = this.profileHelper.getPmcProfile(offerOwnerId);
if (!offerCreatorProfile || offerCreatorProfile.RagfairInfo.offers?.length === 0)
{
if (!offerCreatorProfile || offerCreatorProfile.RagfairInfo.offers?.length === 0) {
// No profile or no offers
return false;
}
@ -280,8 +256,7 @@ export class TradeController
* @param pmcData Player profile
* @returns True if player can buy offer
*/
protected playerLacksTraderLoyaltyLevelToBuyOffer(fleaOffer: IRagfairOffer, pmcData: IPmcData): boolean
{
protected playerLacksTraderLoyaltyLevelToBuyOffer(fleaOffer: IRagfairOffer, pmcData: IPmcData): boolean {
return fleaOffer.loyaltyLevel > pmcData.TradersInfo[fleaOffer.user.id].loyaltyLevel;
}
@ -290,8 +265,7 @@ export class TradeController
pmcData: IPmcData,
request: ISellScavItemsToFenceRequestData,
sessionId: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
const output = this.eventOutputHolder.getOutput(sessionId);
this.mailMoneyToPlayer(sessionId, request.totalValue, Traders.FENCE);
@ -305,8 +279,7 @@ export class TradeController
* @param trader Trader to sell items to
* @param output IItemEventRouterResponse
*/
protected mailMoneyToPlayer(sessionId: string, roublesToSend: number, trader: Traders): void
{
protected mailMoneyToPlayer(sessionId: string, roublesToSend: number, trader: Traders): void {
this.logger.debug(`Selling scav items to fence for ${roublesToSend} roubles`);
// Create single currency item with all currency on it
@ -325,7 +298,7 @@ export class TradeController
this.traderHelper.getTraderById(trader),
MessageType.MESSAGE_WITH_ITEMS,
this.randomUtil.getArrayValue(this.databaseService.getTrader(trader).dialogue.soldItems),
curencyReward.flatMap((x) => x),
curencyReward.flat(),
this.timeUtil.getHoursAsSeconds(72),
);
}
@ -343,21 +316,18 @@ export class TradeController
items: Item[],
handbookPrices: Record<string, number>,
traderDetails: ITraderBase,
): number
{
): number {
const itemWithChildren = this.itemHelper.findAndReturnChildrenAsItems(items, parentItemId);
let totalPrice = 0;
for (const itemToSell of itemWithChildren)
{
for (const itemToSell of itemWithChildren) {
const itemDetails = this.itemHelper.getItem(itemToSell._tpl);
if (
!(
itemDetails[0]
&& this.itemHelper.isOfBaseclasses(itemDetails[1]._id, traderDetails.items_buy.category)
itemDetails[0] &&
this.itemHelper.isOfBaseclasses(itemDetails[1]._id, traderDetails.items_buy.category)
)
)
{
) {
// Skip if tpl isn't item OR item doesn't fulfil match traders buy categories
continue;
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { FenceBaseAssortGenerator } from "@spt/generators/FenceBaseAssortGenerator";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { TraderAssortHelper } from "@spt/helpers/TraderAssortHelper";
@ -13,12 +12,12 @@ import { DatabaseService } from "@spt/services/DatabaseService";
import { FenceService } from "@spt/services/FenceService";
import { TraderAssortService } from "@spt/services/TraderAssortService";
import { TraderPurchasePersisterService } from "@spt/services/TraderPurchasePersisterService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class TraderController
{
export class TraderController {
protected traderConfig: ITraderConfig;
constructor(
@ -35,8 +34,7 @@ export class TraderController
@inject("FenceBaseAssortGenerator") protected fenceBaseAssortGenerator: FenceBaseAssortGenerator,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
) {
this.traderConfig = this.configServer.getConfig(ConfigTypes.TRADER);
}
@ -45,21 +43,17 @@ export class TraderController
* Iterate over traders, ensure a pristine copy of their assorts is stored in traderAssortService
* Store timestamp of next assort refresh in nextResupply property of traders .base object
*/
public load(): void
{
public load(): void {
const nextHourTimestamp = this.timeUtil.getTimestampOfNextHour();
const traderResetStartsWithServer = this.traderConfig.tradersResetFromServerStart;
const traders = this.databaseService.getTraders();
for (const traderId in traders)
{
if (traderId === "ragfair" || traderId === Traders.LIGHTHOUSEKEEPER)
{
for (const traderId in traders) {
if (traderId === "ragfair" || traderId === Traders.LIGHTHOUSEKEEPER) {
continue;
}
if (traderId === Traders.FENCE)
{
if (traderId === Traders.FENCE) {
this.fenceBaseAssortGenerator.generateFenceBaseAssorts();
this.fenceService.generateFenceAssorts();
continue;
@ -68,8 +62,7 @@ export class TraderController
const trader = traders[traderId];
// Create dict of trader assorts on server start
if (!this.traderAssortService.getPristineTraderAssort(traderId))
{
if (!this.traderAssortService.getPristineTraderAssort(traderId)) {
const assortsClone = this.cloner.clone(trader.assort);
this.traderAssortService.setPristineTraderAssort(traderId, assortsClone);
}
@ -90,19 +83,14 @@ export class TraderController
* Fence is handled slightly differently
* @returns has run
*/
public update(): boolean
{
for (const traderId in this.databaseService.getTables().traders)
{
if (traderId === "ragfair" || traderId === Traders.LIGHTHOUSEKEEPER)
{
public update(): boolean {
for (const traderId in this.databaseService.getTables().traders) {
if (traderId === "ragfair" || traderId === Traders.LIGHTHOUSEKEEPER) {
continue;
}
if (traderId === Traders.FENCE)
{
if (this.fenceService.needsPartialRefresh())
{
if (traderId === Traders.FENCE) {
if (this.fenceService.needsPartialRefresh()) {
this.fenceService.performPartialRefresh();
}
@ -111,8 +99,7 @@ export class TraderController
// Trader needs to be refreshed
const trader = this.databaseService.getTrader(traderId);
if (this.traderAssortHelper.traderAssortsHaveExpired(traderId))
{
if (this.traderAssortHelper.traderAssortsHaveExpired(traderId)) {
this.traderAssortHelper.resetExpiredTrader(trader);
// Reset purchase data per trader as they have independent reset times
@ -129,21 +116,17 @@ export class TraderController
* @param sessionID Session id
* @returns array if ITraderBase objects
*/
public getAllTraders(sessionID: string): ITraderBase[]
{
public getAllTraders(sessionID: string): ITraderBase[] {
const traders: ITraderBase[] = [];
const pmcData = this.profileHelper.getPmcProfile(sessionID);
for (const traderID in this.databaseService.getTables().traders)
{
if (this.databaseService.getTables().traders[traderID].base._id === "ragfair")
{
for (const traderID in this.databaseService.getTables().traders) {
if (this.databaseService.getTables().traders[traderID].base._id === "ragfair") {
continue;
}
traders.push(this.traderHelper.getTrader(traderID, sessionID));
if (pmcData.Info)
{
if (pmcData.Info) {
this.traderHelper.lvlUp(traderID, pmcData);
}
}
@ -157,15 +140,12 @@ export class TraderController
* @param traderB Second trader to compare
* @returns 1,-1 or 0
*/
protected sortByTraderId(traderA: ITraderBase, traderB: ITraderBase): number
{
if (traderA._id > traderB._id)
{
protected sortByTraderId(traderA: ITraderBase, traderB: ITraderBase): number {
if (traderA._id > traderB._id) {
return 1;
}
if (traderA._id < traderB._id)
{
if (traderA._id < traderB._id) {
return -1;
}
@ -173,14 +153,12 @@ export class TraderController
}
/** Handle client/trading/api/getTrader */
public getTrader(sessionID: string, traderID: string): ITraderBase
{
public getTrader(sessionID: string, traderID: string): ITraderBase {
return this.traderHelper.getTrader(sessionID, traderID);
}
/** Handle client/trading/api/getTraderAssort */
public getAssort(sessionId: string, traderId: string): ITraderAssort
{
public getAssort(sessionId: string, traderId: string): ITraderAssort {
return this.traderAssortHelper.getAssort(sessionId, traderId);
}
}

View File

@ -1,16 +1,15 @@
import { inject, injectable } from "tsyringe";
import { WeatherGenerator } from "@spt/generators/WeatherGenerator";
import { IWeatherData } from "@spt/models/eft/weather/IWeatherData";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { IWeatherConfig } from "@spt/models/spt/config/IWeatherConfig";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { IGetLocalWeatherResponseData } from "@spt/models/spt/weather/IGetLocalWeatherResponseData";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { SeasonalEventService } from "@spt/services/SeasonalEventService";
import { inject, injectable } from "tsyringe";
@injectable()
export class WeatherController
{
export class WeatherController {
protected weatherConfig: IWeatherConfig;
constructor(
@ -18,14 +17,12 @@ export class WeatherController
@inject("PrimaryLogger") protected logger: ILogger,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
)
{
) {
this.weatherConfig = this.configServer.getConfig(ConfigTypes.WEATHER);
}
/** Handle client/weather */
public generate(): IWeatherData
{
public generate(): IWeatherData {
let result: IWeatherData = { acceleration: 0, time: "", date: "", weather: undefined, season: 1 }; // defaults, hydrated below
result = this.weatherGenerator.calculateGameTime(result);
@ -38,14 +35,15 @@ export class WeatherController
* Get the current in-raid time (MUST HAVE PLAYER LOGGED INTO CLIENT TO WORK)
* @returns Date object
*/
public getCurrentInRaidTime(): Date
{
public getCurrentInRaidTime(): Date {
return this.weatherGenerator.getInRaidTime();
}
public generateLocal(sesssionID: string): IGetLocalWeatherResponseData
{
let result: IGetLocalWeatherResponseData = { season: this.seasonalEventService.getActiveWeatherSeason(), weather: [] };
public generateLocal(sesssionID: string): IGetLocalWeatherResponseData {
const result: IGetLocalWeatherResponseData = {
season: this.seasonalEventService.getActiveWeatherSeason(),
weather: [],
};
result.weather.push(this.weatherGenerator.generateWeather());

View File

@ -1,22 +1,22 @@
import { inject, injectable } from "tsyringe";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { IAddToWishlistRequest } from "@spt/models/eft/wishlist/IAddToWishlistRequest";
import { IChangeWishlistItemCategoryRequest } from "@spt/models/eft/wishlist/IChangeWishlistItemCategoryRequest";
import { IRemoveFromWishlistRequest } from "@spt/models/eft/wishlist/IRemoveFromWishlistRequest";
import { EventOutputHolder } from "@spt/routers/EventOutputHolder";
import { inject, injectable } from "tsyringe";
@injectable()
export class WishlistController
{
constructor(@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder)
{}
export class WishlistController {
constructor(@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder) {}
/** Handle AddToWishList */
public addToWishList(pmcData: IPmcData, request: IAddToWishlistRequest, sessionID: string): IItemEventRouterResponse
{
for (const itemId of Object.keys(request.items))
{
public addToWishList(
pmcData: IPmcData,
request: IAddToWishlistRequest,
sessionID: string,
): IItemEventRouterResponse {
for (const itemId of Object.keys(request.items)) {
pmcData.WishList[itemId] = request.items[itemId];
}
@ -28,10 +28,8 @@ export class WishlistController
pmcData: IPmcData,
request: IRemoveFromWishlistRequest,
sessionID: string,
): IItemEventRouterResponse
{
for (const itemId of request.items)
{
): IItemEventRouterResponse {
for (const itemId of request.items) {
delete pmcData.WishList[itemId];
}
@ -43,8 +41,7 @@ export class WishlistController
pmcData: IPmcData,
request: IChangeWishlistItemCategoryRequest,
sessionID: string,
): IItemEventRouterResponse
{
): IItemEventRouterResponse {
pmcData.WishList[request.item] = request.category;
return this.eventOutputHolder.getOutput(sessionID);

View File

@ -1,4 +1,3 @@
import { DependencyContainer, Lifecycle } from "tsyringe";
import { AchievementCallbacks } from "@spt/callbacks/AchievementCallbacks";
import { BotCallbacks } from "@spt/callbacks/BotCallbacks";
import { BuildsCallbacks } from "@spt/callbacks/BuildsCallbacks";
@ -70,18 +69,18 @@ import { BotWeaponGenerator } from "@spt/generators/BotWeaponGenerator";
import { FenceBaseAssortGenerator } from "@spt/generators/FenceBaseAssortGenerator";
import { LocationLootGenerator } from "@spt/generators/LocationLootGenerator";
import { LootGenerator } from "@spt/generators/LootGenerator";
import { PlayerScavGenerator } from "@spt/generators/PlayerScavGenerator";
import { PMCLootGenerator } from "@spt/generators/PMCLootGenerator";
import { PlayerScavGenerator } from "@spt/generators/PlayerScavGenerator";
import { RagfairAssortGenerator } from "@spt/generators/RagfairAssortGenerator";
import { RagfairOfferGenerator } from "@spt/generators/RagfairOfferGenerator";
import { RepeatableQuestGenerator } from "@spt/generators/RepeatableQuestGenerator";
import { RepeatableQuestRewardGenerator } from "@spt/generators/RepeatableQuestRewardGenerator";
import { ScavCaseRewardGenerator } from "@spt/generators/ScavCaseRewardGenerator";
import { WeatherGenerator } from "@spt/generators/WeatherGenerator";
import { BarrelInventoryMagGen } from "@spt/generators/weapongen/implementations/BarrelInventoryMagGen";
import { ExternalInventoryMagGen } from "@spt/generators/weapongen/implementations/ExternalInventoryMagGen";
import { InternalMagazineInventoryMagGen } from "@spt/generators/weapongen/implementations/InternalMagazineInventoryMagGen";
import { UbglExternalMagGen } from "@spt/generators/weapongen/implementations/UbglExternalMagGen";
import { WeatherGenerator } from "@spt/generators/WeatherGenerator";
import { AssortHelper } from "@spt/helpers/AssortHelper";
import { BotDifficultyHelper } from "@spt/helpers/BotDifficultyHelper";
import { BotGeneratorHelper } from "@spt/helpers/BotGeneratorHelper";
@ -133,6 +132,10 @@ import { PostSptModLoader } from "@spt/loaders/PostSptModLoader";
import { PreSptModLoader } from "@spt/loaders/PreSptModLoader";
import { IAsyncQueue } from "@spt/models/spt/utils/IAsyncQueue";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt/routers/EventOutputHolder";
import { HttpRouter } from "@spt/routers/HttpRouter";
import { ImageRouter } from "@spt/routers/ImageRouter";
import { ItemEventRouter } from "@spt/routers/ItemEventRouter";
import { BotDynamicRouter } from "@spt/routers/dynamic/BotDynamicRouter";
import { BundleDynamicRouter } from "@spt/routers/dynamic/BundleDynamicRouter";
import { CustomizationDynamicRouter } from "@spt/routers/dynamic/CustomizationDynamicRouter";
@ -142,9 +145,6 @@ import { InraidDynamicRouter } from "@spt/routers/dynamic/InraidDynamicRouter";
import { LocationDynamicRouter } from "@spt/routers/dynamic/LocationDynamicRouter";
import { NotifierDynamicRouter } from "@spt/routers/dynamic/NotifierDynamicRouter";
import { TraderDynamicRouter } from "@spt/routers/dynamic/TraderDynamicRouter";
import { EventOutputHolder } from "@spt/routers/EventOutputHolder";
import { HttpRouter } from "@spt/routers/HttpRouter";
import { ImageRouter } from "@spt/routers/ImageRouter";
import { CustomizationItemEventRouter } from "@spt/routers/item_events/CustomizationItemEventRouter";
import { HealthItemEventRouter } from "@spt/routers/item_events/HealthItemEventRouter";
import { HideoutItemEventRouter } from "@spt/routers/item_events/HideoutItemEventRouter";
@ -156,7 +156,6 @@ import { RagfairItemEventRouter } from "@spt/routers/item_events/RagfairItemEven
import { RepairItemEventRouter } from "@spt/routers/item_events/RepairItemEventRouter";
import { TradeItemEventRouter } from "@spt/routers/item_events/TradeItemEventRouter";
import { WishlistItemEventRouter } from "@spt/routers/item_events/WishlistItemEventRouter";
import { ItemEventRouter } from "@spt/routers/ItemEventRouter";
import { HealthSaveLoadRouter } from "@spt/routers/save_load/HealthSaveLoadRouter";
import { InraidSaveLoadRouter } from "@spt/routers/save_load/InraidSaveLoadRouter";
import { InsuranceSaveLoadRouter } from "@spt/routers/save_load/InsuranceSaveLoadRouter";
@ -188,23 +187,21 @@ import { TraderStaticRouter } from "@spt/routers/static/TraderStaticRouter";
import { WeatherStaticRouter } from "@spt/routers/static/WeatherStaticRouter";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { DatabaseServer } from "@spt/servers/DatabaseServer";
import { SptHttpListener } from "@spt/servers/http/SptHttpListener";
import { HttpServer } from "@spt/servers/HttpServer";
import { RagfairServer } from "@spt/servers/RagfairServer";
import { SaveServer } from "@spt/servers/SaveServer";
import { WebSocketServer } from "@spt/servers/WebSocketServer";
import { SptHttpListener } from "@spt/servers/http/SptHttpListener";
import { IWebSocketConnectionHandler } from "@spt/servers/ws/IWebSocketConnectionHandler";
import { SptWebSocketConnectionHandler } from "@spt/servers/ws/SptWebSocketConnectionHandler";
import { DefaultSptWebSocketMessageHandler } from "@spt/servers/ws/message/DefaultSptWebSocketMessageHandler";
import { ISptWebSocketMessageHandler } from "@spt/servers/ws/message/ISptWebSocketMessageHandler";
import { SptWebSocketConnectionHandler } from "@spt/servers/ws/SptWebSocketConnectionHandler";
import { AirdropService } from "@spt/services/AirdropService";
import { BotEquipmentFilterService } from "@spt/services/BotEquipmentFilterService";
import { BotEquipmentModPoolService } from "@spt/services/BotEquipmentModPoolService";
import { BotGenerationCacheService } from "@spt/services/BotGenerationCacheService";
import { BotLootCacheService } from "@spt/services/BotLootCacheService";
import { BotWeaponModLimitService } from "@spt/services/BotWeaponModLimitService";
import { BundleHashCacheService } from "@spt/services/cache/BundleHashCacheService";
import { ModHashCacheService } from "@spt/services/cache/ModHashCacheService";
import { CustomLocationWaveService } from "@spt/services/CustomLocationWaveService";
import { DatabaseService } from "@spt/services/DatabaseService";
import { FenceService } from "@spt/services/FenceService";
@ -219,13 +216,6 @@ import { MailSendService } from "@spt/services/MailSendService";
import { MapMarkerService } from "@spt/services/MapMarkerService";
import { MatchBotDetailsCacheService } from "@spt/services/MatchBotDetailsCacheService";
import { MatchLocationService } from "@spt/services/MatchLocationService";
import { CustomItemService } from "@spt/services/mod/CustomItemService";
import { DynamicRouterModService } from "@spt/services/mod/dynamicRouter/DynamicRouterModService";
import { HttpListenerModService } from "@spt/services/mod/httpListener/HttpListenerModService";
import { ImageRouteService } from "@spt/services/mod/image/ImageRouteService";
import { OnLoadModService } from "@spt/services/mod/onLoad/OnLoadModService";
import { OnUpdateModService } from "@spt/services/mod/onUpdate/OnUpdateModService";
import { StaticRouterModService } from "@spt/services/mod/staticRouter/StaticRouterModService";
import { ModCompilerService } from "@spt/services/ModCompilerService";
import { NotificationService } from "@spt/services/NotificationService";
import { OpenZoneService } from "@spt/services/OpenZoneService";
@ -246,12 +236,17 @@ import { RepairService } from "@spt/services/RepairService";
import { SeasonalEventService } from "@spt/services/SeasonalEventService";
import { TraderAssortService } from "@spt/services/TraderAssortService";
import { TraderPurchasePersisterService } from "@spt/services/TraderPurchasePersisterService";
import { BundleHashCacheService } from "@spt/services/cache/BundleHashCacheService";
import { ModHashCacheService } from "@spt/services/cache/ModHashCacheService";
import { CustomItemService } from "@spt/services/mod/CustomItemService";
import { DynamicRouterModService } from "@spt/services/mod/dynamicRouter/DynamicRouterModService";
import { HttpListenerModService } from "@spt/services/mod/httpListener/HttpListenerModService";
import { ImageRouteService } from "@spt/services/mod/image/ImageRouteService";
import { OnLoadModService } from "@spt/services/mod/onLoad/OnLoadModService";
import { OnUpdateModService } from "@spt/services/mod/onUpdate/OnUpdateModService";
import { StaticRouterModService } from "@spt/services/mod/staticRouter/StaticRouterModService";
import { App } from "@spt/utils/App";
import { AsyncQueue } from "@spt/utils/AsyncQueue";
import type { ICloner } from "@spt/utils/cloners/ICloner";
import { JsonCloner } from "@spt/utils/cloners/JsonCloner";
import { RecursiveCloner } from "@spt/utils/cloners/RecursiveCloner";
import { StructuredCloner } from "@spt/utils/cloners/StructuredCloner";
import { CompareUtil } from "@spt/utils/CompareUtil";
import { DatabaseImporter } from "@spt/utils/DatabaseImporter";
import { EncodingUtil } from "@spt/utils/EncodingUtil";
@ -260,28 +255,30 @@ import { HttpFileUtil } from "@spt/utils/HttpFileUtil";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { ImporterUtil } from "@spt/utils/ImporterUtil";
import { JsonUtil } from "@spt/utils/JsonUtil";
import { WinstonMainLogger } from "@spt/utils/logging/WinstonMainLogger";
import { WinstonRequestLogger } from "@spt/utils/logging/WinstonRequestLogger";
import { MathUtil } from "@spt/utils/MathUtil";
import { ObjectId } from "@spt/utils/ObjectId";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { VFS } from "@spt/utils/VFS";
import { Watermark, WatermarkLocale } from "@spt/utils/Watermark";
import type { ICloner } from "@spt/utils/cloners/ICloner";
import { JsonCloner } from "@spt/utils/cloners/JsonCloner";
import { RecursiveCloner } from "@spt/utils/cloners/RecursiveCloner";
import { StructuredCloner } from "@spt/utils/cloners/StructuredCloner";
import { WinstonMainLogger } from "@spt/utils/logging/WinstonMainLogger";
import { WinstonRequestLogger } from "@spt/utils/logging/WinstonRequestLogger";
import { DependencyContainer, Lifecycle } from "tsyringe";
/**
* Handle the registration of classes to be used by the Dependency Injection code
*/
export class Container
{
public static registerPostLoadTypes(container: DependencyContainer, childContainer: DependencyContainer): void
{
export class Container {
public static registerPostLoadTypes(container: DependencyContainer, childContainer: DependencyContainer): void {
container.register<SptHttpListener>("SptHttpListener", SptHttpListener, { lifecycle: Lifecycle.Singleton });
childContainer.registerType("HttpListener", "SptHttpListener");
}
public static registerTypes(depContainer: DependencyContainer): void
{
public static registerTypes(depContainer: DependencyContainer): void {
depContainer.register("ApplicationContext", ApplicationContext, { lifecycle: Lifecycle.Singleton });
Container.registerUtils(depContainer);
@ -304,16 +301,20 @@ export class Container
Container.registerPrimaryDependencies(depContainer);
}
public static registerPrimaryDependencies(depContainer: DependencyContainer): void
{
depContainer.register<ILogger>("PrimaryLogger", { useToken: "WinstonLogger" },
{ lifecycle: Lifecycle.Singleton });
depContainer.register<ICloner>("PrimaryCloner", { useToken: "RecursiveCloner" },
{ lifecycle: Lifecycle.Singleton });
public static registerPrimaryDependencies(depContainer: DependencyContainer): void {
depContainer.register<ILogger>(
"PrimaryLogger",
{ useToken: "WinstonLogger" },
{ lifecycle: Lifecycle.Singleton },
);
depContainer.register<ICloner>(
"PrimaryCloner",
{ useToken: "RecursiveCloner" },
{ lifecycle: Lifecycle.Singleton },
);
}
public static registerListTypes(depContainer: DependencyContainer): void
{
public static registerListTypes(depContainer: DependencyContainer): void {
depContainer.register("OnLoadModService", { useValue: new OnLoadModService(depContainer) });
depContainer.register("HttpListenerModService", { useValue: new HttpListenerModService(depContainer) });
depContainer.register("OnUpdateModService", { useValue: new OnUpdateModService(depContainer) });
@ -409,8 +410,7 @@ export class Container
depContainer.registerType("SptWebSocketMessageHandler", "DefaultSptWebSocketMessageHandler");
}
private static registerUtils(depContainer: DependencyContainer): void
{
private static registerUtils(depContainer: DependencyContainer): void {
// Utils
depContainer.register<App>("App", App, { lifecycle: Lifecycle.Singleton });
depContainer.register<DatabaseImporter>("DatabaseImporter", DatabaseImporter, {
@ -444,8 +444,7 @@ export class Container
depContainer.register<ICloner>("RecursiveCloner", RecursiveCloner, { lifecycle: Lifecycle.Singleton });
}
private static registerRouters(depContainer: DependencyContainer): void
{
private static registerRouters(depContainer: DependencyContainer): void {
// Routers
depContainer.register<HttpRouter>("HttpRouter", HttpRouter, { lifecycle: Lifecycle.Singleton });
depContainer.register<ImageRouter>("ImageRouter", ImageRouter);
@ -530,8 +529,7 @@ export class Container
depContainer.register<BuildsStaticRouter>("BuildsStaticRouter", { useClass: BuildsStaticRouter });
}
private static registerGenerators(depContainer: DependencyContainer): void
{
private static registerGenerators(depContainer: DependencyContainer): void {
// Generators
depContainer.register<BotGenerator>("BotGenerator", BotGenerator);
depContainer.register<BotWeaponGenerator>("BotWeaponGenerator", BotWeaponGenerator);
@ -578,8 +576,7 @@ export class Container
depContainer.registerType("InventoryMagGen", "UbglExternalMagGen");
}
private static registerHelpers(depContainer: DependencyContainer): void
{
private static registerHelpers(depContainer: DependencyContainer): void {
// Helpers
depContainer.register<AssortHelper>("AssortHelper", { useClass: AssortHelper });
depContainer.register<BotHelper>("BotHelper", { useClass: BotHelper });
@ -642,8 +639,7 @@ export class Container
});
}
private static registerLoaders(depContainer: DependencyContainer): void
{
private static registerLoaders(depContainer: DependencyContainer): void {
// Loaders
depContainer.register<BundleLoader>("BundleLoader", BundleLoader, { lifecycle: Lifecycle.Singleton });
depContainer.register<PreSptModLoader>("PreSptModLoader", PreSptModLoader, { lifecycle: Lifecycle.Singleton });
@ -652,8 +648,7 @@ export class Container
});
}
private static registerCallbacks(depContainer: DependencyContainer): void
{
private static registerCallbacks(depContainer: DependencyContainer): void {
// Callbacks
depContainer.register<BotCallbacks>("BotCallbacks", { useClass: BotCallbacks });
depContainer.register<BundleCallbacks>("BundleCallbacks", { useClass: BundleCallbacks });
@ -691,8 +686,7 @@ export class Container
depContainer.register<BuildsCallbacks>("BuildsCallbacks", { useClass: BuildsCallbacks });
}
private static registerServices(depContainer: DependencyContainer): void
{
private static registerServices(depContainer: DependencyContainer): void {
// Services
depContainer.register<DatabaseService>("DatabaseService", DatabaseService, { lifecycle: Lifecycle.Singleton });
depContainer.register<ImageRouteService>("ImageRouteService", ImageRouteService, {
@ -800,21 +794,27 @@ export class Container
});
}
private static registerServers(depContainer: DependencyContainer): void
{
private static registerServers(depContainer: DependencyContainer): void {
// Servers
depContainer.register<DatabaseServer>("DatabaseServer", DatabaseServer, { lifecycle: Lifecycle.Singleton });
depContainer.register<HttpServer>("HttpServer", HttpServer, { lifecycle: Lifecycle.Singleton });
depContainer.register<WebSocketServer>("WebSocketServer", WebSocketServer, { lifecycle: Lifecycle.Singleton });
depContainer.register<IWebSocketConnectionHandler>("SptWebSocketConnectionHandler", SptWebSocketConnectionHandler, { lifecycle: Lifecycle.Singleton });
depContainer.register<ISptWebSocketMessageHandler>("DefaultSptWebSocketMessageHandler", DefaultSptWebSocketMessageHandler, { lifecycle: Lifecycle.Singleton });
depContainer.register<IWebSocketConnectionHandler>(
"SptWebSocketConnectionHandler",
SptWebSocketConnectionHandler,
{ lifecycle: Lifecycle.Singleton },
);
depContainer.register<ISptWebSocketMessageHandler>(
"DefaultSptWebSocketMessageHandler",
DefaultSptWebSocketMessageHandler,
{ lifecycle: Lifecycle.Singleton },
);
depContainer.register<RagfairServer>("RagfairServer", RagfairServer);
depContainer.register<SaveServer>("SaveServer", SaveServer, { lifecycle: Lifecycle.Singleton });
depContainer.register<ConfigServer>("ConfigServer", ConfigServer, { lifecycle: Lifecycle.Singleton });
}
private static registerControllers(depContainer: DependencyContainer): void
{
private static registerControllers(depContainer: DependencyContainer): void {
// Controllers
depContainer.register<BotController>("BotController", { useClass: BotController });
depContainer.register<ClientLogController>("ClientLogController", { useClass: ClientLogController });

View File

@ -1,5 +1,4 @@
export interface OnLoad
{
onLoad(): Promise<void>
getRoute(): string
export interface OnLoad {
onLoad(): Promise<void>;
getRoute(): string;
}

View File

@ -1,5 +1,4 @@
export interface OnUpdate
{
onUpdate(timeSinceLastRun: number): Promise<boolean>
getRoute(): string
export interface OnUpdate {
onUpdate(timeSinceLastRun: number): Promise<boolean>;
getRoute(): string;
}

View File

@ -2,33 +2,26 @@ import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { ISptProfile } from "@spt/models/eft/profile/ISptProfile";
export class Router
{
export class Router {
protected handledRoutes: HandledRoute[] = [];
public getTopLevelRoute(): string
{
public getTopLevelRoute(): string {
return "spt";
}
protected getHandledRoutes(): HandledRoute[]
{
protected getHandledRoutes(): HandledRoute[] {
throw new Error("This method needs to be overrode by the router classes");
}
protected getInternalHandledRoutes(): HandledRoute[]
{
if (this.handledRoutes.length === 0)
{
protected getInternalHandledRoutes(): HandledRoute[] {
if (this.handledRoutes.length === 0) {
this.handledRoutes = this.getHandledRoutes();
}
return this.handledRoutes;
}
public canHandle(url: string, partialMatch = false): boolean
{
if (partialMatch)
{
public canHandle(url: string, partialMatch = false): boolean {
if (partialMatch) {
return this.getInternalHandledRoutes()
.filter((r) => r.dynamic)
.some((r) => url.includes(r.route));
@ -39,80 +32,64 @@ export class Router
}
}
export class StaticRouter extends Router
{
constructor(private routes: RouteAction[])
{
export class StaticRouter extends Router {
constructor(private routes: RouteAction[]) {
super();
}
public async handleStatic(url: string, info: any, sessionID: string, output: string): Promise<any>
{
public async handleStatic(url: string, info: any, sessionID: string, output: string): Promise<any> {
return this.routes.find((route) => route.url === url).action(url, info, sessionID, output);
}
public override getHandledRoutes(): HandledRoute[]
{
public override getHandledRoutes(): HandledRoute[] {
return this.routes.map((route) => new HandledRoute(route.url, false));
}
}
export class DynamicRouter extends Router
{
constructor(private routes: RouteAction[])
{
export class DynamicRouter extends Router {
constructor(private routes: RouteAction[]) {
super();
}
public async handleDynamic(url: string, info: any, sessionID: string, output: string): Promise<any>
{
public async handleDynamic(url: string, info: any, sessionID: string, output: string): Promise<any> {
return this.routes.find((r) => url.includes(r.url)).action(url, info, sessionID, output);
}
public override getHandledRoutes(): HandledRoute[]
{
public override getHandledRoutes(): HandledRoute[] {
return this.routes.map((route) => new HandledRoute(route.url, true));
}
}
// The name of this class should be ItemEventRouter, but that name is taken,
// So instead I added the definition
export class ItemEventRouterDefinition extends Router
{
export class ItemEventRouterDefinition extends Router {
public async handleItemEvent(
url: string,
pmcData: IPmcData,
body: any,
sessionID: string,
output: IItemEventRouterResponse,
): Promise<any>
{
): Promise<any> {
throw new Error("This method needs to be overrode by the router classes");
}
}
export class SaveLoadRouter extends Router
{
public handleLoad(profile: ISptProfile): ISptProfile
{
export class SaveLoadRouter extends Router {
public handleLoad(profile: ISptProfile): ISptProfile {
throw new Error("This method needs to be overrode by the router classes");
}
}
export class HandledRoute
{
export class HandledRoute {
constructor(
public route: string,
public dynamic: boolean,
)
{}
) {}
}
export class RouteAction
{
export class RouteAction {
constructor(
public url: string,
public action: (url: string, info: any, sessionID: string, output: string) => Promise<any>,
)
{}
) {}
}

View File

@ -1,14 +1,11 @@
import { IncomingMessage, ServerResponse } from "node:http";
export class Serializer
{
public serialize(sessionID: string, req: IncomingMessage, resp: ServerResponse, body: any): void
{
export class Serializer {
public serialize(sessionID: string, req: IncomingMessage, resp: ServerResponse, body: any): void {
throw new Error("Should be extended and overrode");
}
public canHandle(something: string): boolean
{
public canHandle(something: string): boolean {
throw new Error("Should be extended and overrode");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { BotInventoryGenerator } from "@spt/generators/BotInventoryGenerator";
import { BotLevelGenerator } from "@spt/generators/BotLevelGenerator";
import { BotDifficultyHelper } from "@spt/helpers/BotDifficultyHelper";
@ -31,14 +30,14 @@ import { DatabaseService } from "@spt/services/DatabaseService";
import { ItemFilterService } from "@spt/services/ItemFilterService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { SeasonalEventService } from "@spt/services/SeasonalEventService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HashUtil } from "@spt/utils/HashUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class BotGenerator
{
export class BotGenerator {
protected botConfig: IBotConfig;
protected pmcConfig: IPmcConfig;
@ -60,8 +59,7 @@ export class BotGenerator
@inject("ItemFilterService") protected itemFilterService: ItemFilterService,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
) {
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
}
@ -73,8 +71,7 @@ export class BotGenerator
* @param botTemplate base bot template to use (e.g. assault/pmcbot)
* @returns
*/
public generatePlayerScav(sessionId: string, role: string, difficulty: string, botTemplate: IBotType): IBotBase
{
public generatePlayerScav(sessionId: string, role: string, difficulty: string, botTemplate: IBotType): IBotBase {
let bot = this.getCloneOfBotBase();
bot.Info.Settings.BotDifficulty = difficulty;
bot.Info.Settings.Role = role;
@ -102,12 +99,12 @@ export class BotGenerator
* @param botGenerationDetails details on how to generate bots
* @returns constructed bot
*/
public prepareAndGenerateBot(sessionId: string, botGenerationDetails: BotGenerationDetails): IBotBase
{
public prepareAndGenerateBot(sessionId: string, botGenerationDetails: BotGenerationDetails): IBotBase {
const preparedBotBase = this.getPreparedBotBase(
botGenerationDetails.eventRole ?? botGenerationDetails.role, // Use eventRole if provided,
botGenerationDetails.side,
botGenerationDetails.botDifficulty);
botGenerationDetails.botDifficulty,
);
// Get raw json data for bot (Cloned)
const botRole = botGenerationDetails.isPmc
@ -125,8 +122,7 @@ export class BotGenerator
* @param difficulty Difficult bot should have
* @returns Cloned bot base
*/
protected getPreparedBotBase(botRole: string, botSide: string, difficulty: string): IBotBase
{
protected getPreparedBotBase(botRole: string, botSide: string, difficulty: string): IBotBase {
const botBaseClone = this.getCloneOfBotBase();
botBaseClone.Info.Settings.Role = botRole;
botBaseClone.Info.Side = botSide;
@ -139,8 +135,7 @@ export class BotGenerator
* Get a clone of the database\bots\base.json file
* @returns IBotBase object
*/
protected getCloneOfBotBase(): IBotBase
{
protected getCloneOfBotBase(): IBotBase {
return this.cloner.clone(this.databaseService.getBots().base);
}
@ -157,8 +152,7 @@ export class BotGenerator
bot: IBotBase,
botJsonTemplate: IBotType,
botGenerationDetails: BotGenerationDetails,
): IBotBase
{
): IBotBase {
const botRole = botGenerationDetails.role.toLowerCase();
const botLevel = this.botLevelGenerator.generateBotLevel(
botJsonTemplate.experience.level,
@ -166,8 +160,7 @@ export class BotGenerator
bot,
);
if (!botGenerationDetails.isPlayerScav)
{
if (!botGenerationDetails.isPlayerScav) {
this.botEquipmentFilterService.filterBotEquipment(
sessionId,
botJsonTemplate,
@ -178,11 +171,9 @@ export class BotGenerator
bot.Info.Nickname = this.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole, sessionId);
if (!this.seasonalEventService.christmasEventEnabled())
{
if (!this.seasonalEventService.christmasEventEnabled()) {
// Process all bots EXCEPT gifter, he needs christmas items
if (botGenerationDetails.role !== "gifter")
{
if (botGenerationDetails.role !== "gifter") {
this.seasonalEventService.removeChristmasItemsFromBotInventory(
botJsonTemplate.inventory,
botGenerationDetails.role,
@ -193,8 +184,7 @@ export class BotGenerator
this.removeBlacklistedLootFromBotTemplate(botJsonTemplate.inventory);
// Remove hideout data if bot is not a PMC or pscav - match what live sends
if (!(botGenerationDetails.isPmc || botGenerationDetails.isPlayerScav))
{
if (!(botGenerationDetails.isPmc || botGenerationDetails.isPlayerScav)) {
bot.Hideout = undefined;
}
@ -209,12 +199,10 @@ export class BotGenerator
bot.Health = this.generateHealth(botJsonTemplate.health, botGenerationDetails.isPlayerScav);
bot.Skills = this.generateSkills(<any>botJsonTemplate.skills); // TODO: fix bad type, bot jsons store skills in dict, output needs to be array
if (botGenerationDetails.isPmc)
{
if (botGenerationDetails.isPmc) {
bot.Info.IsStreamerModeAvailable = true; // Set to true so client patches can pick it up later - client sometimes alters botrole to assaultGroup
this.setRandomisedGameVersionAndCategory(bot.Info);
if (bot.Info.GameVersion === GameEditions.UNHEARD)
{
if (bot.Info.GameVersion === GameEditions.UNHEARD) {
this.addAdditionalPocketLootWeightsForUnheardBot(botJsonTemplate);
}
}
@ -230,8 +218,7 @@ export class BotGenerator
bot.Info.GameVersion,
);
if (this.botConfig.botRolesWithDogTags.includes(botRole))
{
if (this.botConfig.botRolesWithDogTags.includes(botRole)) {
this.addDogtagToBot(bot);
}
@ -242,16 +229,14 @@ export class BotGenerator
this.generateInventoryId(bot);
// Set role back to originally requested now its been generated
if (botGenerationDetails.eventRole)
{
if (botGenerationDetails.eventRole) {
bot.Info.Settings.Role = botGenerationDetails.eventRole;
}
return bot;
}
protected addAdditionalPocketLootWeightsForUnheardBot(botJsonTemplate: IBotType): void
{
protected addAdditionalPocketLootWeightsForUnheardBot(botJsonTemplate: IBotType): void {
// Adjust pocket loot weights to allow for 5 or 6 items
const pocketWeights = botJsonTemplate.generation.items.pocketLoot.weights;
pocketWeights["5"] = 1;
@ -262,31 +247,25 @@ export class BotGenerator
* Remove items from item.json/lootableItemBlacklist from bots inventory
* @param botInventory Bot to filter
*/
protected removeBlacklistedLootFromBotTemplate(botInventory: Inventory): void
{
protected removeBlacklistedLootFromBotTemplate(botInventory: Inventory): void {
const lootContainersToFilter = ["Backpack", "Pockets", "TacticalVest"];
// Remove blacklisted loot from loot containers
for (const lootContainerKey of lootContainersToFilter)
{
for (const lootContainerKey of lootContainersToFilter) {
// No container, skip
if (botInventory.items[lootContainerKey]?.length === 0)
{
if (botInventory.items[lootContainerKey]?.length === 0) {
continue;
}
const tplsToRemove: string[] = [];
const containerItems = botInventory.items[lootContainerKey];
for (const tplKey of Object.keys(containerItems))
{
if (this.itemFilterService.isLootableItemBlacklisted(tplKey))
{
for (const tplKey of Object.keys(containerItems)) {
if (this.itemFilterService.isLootableItemBlacklisted(tplKey)) {
tplsToRemove.push(tplKey);
}
}
for (const blacklistedTplToRemove of tplsToRemove)
{
for (const blacklistedTplToRemove of tplsToRemove) {
delete containerItems[blacklistedTplToRemove];
}
}
@ -302,8 +281,7 @@ export class BotGenerator
bot: IBotBase,
appearance: Appearance,
botGenerationDetails: BotGenerationDetails,
): void
{
): void {
bot.Customization.Head = this.weightedRandomHelper.getWeightedValue<string>(appearance.head);
bot.Customization.Body = this.weightedRandomHelper.getWeightedValue<string>(appearance.body);
bot.Customization.Feet = this.weightedRandomHelper.getWeightedValue<string>(appearance.feet);
@ -314,8 +292,7 @@ export class BotGenerator
// Find the body/hands mapping
const matchingBody: IWildBody = bodyGlobalDict[chosenBodyTemplate?._name];
if (matchingBody?.isNotRandom)
{
if (matchingBody?.isNotRandom) {
// Has fixed hands for this body, set them
bot.Customization.Hands = matchingBody.hands;
}
@ -334,8 +311,7 @@ export class BotGenerator
botGenerationDetails: BotGenerationDetails,
botRole: string,
sessionId?: string,
): string
{
): string {
const isPlayerScav = botGenerationDetails.isPlayerScav;
let name = `${this.randomUtil.getArrayValue(botJsonTemplate.firstName)} ${
@ -345,19 +321,16 @@ export class BotGenerator
// Simulate bot looking like a player scav with the PMC name in brackets.
// E.g. "ScavName (PMCName)"
if (this.shouldSimulatePlayerScavName(botRole, isPlayerScav))
{
if (this.shouldSimulatePlayerScavName(botRole, isPlayerScav)) {
return this.addPlayerScavNameSimulationSuffix(name);
}
if (this.botConfig.showTypeInNickname && !isPlayerScav)
{
if (this.botConfig.showTypeInNickname && !isPlayerScav) {
name += ` ${botRole}`;
}
// We want to replace pmc bot names with player name + prefix
if (botGenerationDetails.isPmc && botGenerationDetails.allPmcsHaveSameNameAsPlayer)
{
if (botGenerationDetails.isPmc && botGenerationDetails.allPmcsHaveSameNameAsPlayer) {
const prefix = this.localisationService.getRandomTextThatMatchesPartialKey("pmc-name_prefix_");
name = `${prefix} ${name}`;
}
@ -365,15 +338,15 @@ export class BotGenerator
return name;
}
protected shouldSimulatePlayerScavName(botRole: string, isPlayerScav: boolean): boolean
{
return botRole === "assault"
&& this.randomUtil.getChance100(this.botConfig.chanceAssaultScavHasPlayerScavName)
&& !isPlayerScav;
protected shouldSimulatePlayerScavName(botRole: string, isPlayerScav: boolean): boolean {
return (
botRole === "assault" &&
this.randomUtil.getChance100(this.botConfig.chanceAssaultScavHasPlayerScavName) &&
!isPlayerScav
);
}
protected addPlayerScavNameSimulationSuffix(nickname: string): string
{
protected addPlayerScavNameSimulationSuffix(nickname: string): string {
const pmcNames = [
...this.databaseService.getBots().types.usec.firstName,
...this.databaseService.getBots().types.bear.firstName,
@ -385,10 +358,8 @@ export class BotGenerator
* Log the number of PMCs generated to the debug console
* @param output Generated bot array, ready to send to client
*/
protected logPmcGeneratedCount(output: IBotBase[]): void
{
const pmcCount = output.reduce((acc, cur) =>
{
protected logPmcGeneratedCount(output: IBotBase[]): void {
const pmcCount = output.reduce((acc, cur) => {
return cur.Info.Side === "Bear" || cur.Info.Side === "Usec" ? acc + 1 : acc;
}, 0);
this.logger.debug(`Generated ${output.length} total bots. Replaced ${pmcCount} with PMCs`);
@ -400,8 +371,7 @@ export class BotGenerator
* @param playerScav Is a pscav bot being generated
* @returns PmcHealth object
*/
protected generateHealth(healthObj: Health, playerScav = false): PmcHealth
{
protected generateHealth(healthObj: Health, playerScav = false): PmcHealth {
const bodyParts = playerScav
? this.getLowestHpBody(healthObj.BodyParts)
: this.randomUtil.getArrayValue(healthObj.BodyParts);
@ -474,21 +444,17 @@ export class BotGenerator
* @param bodies Body parts to sum up
* @returns Lowest hp collection
*/
protected getLowestHpBody(bodies: BodyPart[]): BodyPart | undefined
{
if (bodies.length === 0)
{
protected getLowestHpBody(bodies: BodyPart[]): BodyPart | undefined {
if (bodies.length === 0) {
// Handle empty input
return undefined;
}
let result: BodyPart;
let currentHighest = Number.POSITIVE_INFINITY;
for (const bodyParts of bodies)
{
for (const bodyParts of bodies) {
const hpTotal = Object.values(bodyParts).reduce((acc, curr) => acc + curr.max, 0);
if (hpTotal < currentHighest)
{
if (hpTotal < currentHighest) {
// Found collection with lower value that previous, use it
currentHighest = hpTotal;
result = bodyParts;
@ -503,8 +469,7 @@ export class BotGenerator
* @param botSkills Skills that should have their progress value randomised
* @returns
*/
protected generateSkills(botSkills: IBaseJsonSkills): botSkills
{
protected generateSkills(botSkills: IBaseJsonSkills): botSkills {
const skillsToReturn: botSkills = {
Common: this.getSkillsWithRandomisedProgressValue(botSkills.Common, true),
Mastering: this.getSkillsWithRandomisedProgressValue(botSkills.Mastering, false),
@ -523,20 +488,16 @@ export class BotGenerator
protected getSkillsWithRandomisedProgressValue(
skills: Record<string, IBaseSkill>,
isCommonSkills: boolean,
): IBaseSkill[]
{
if (Object.keys(skills ?? []).length === 0)
{
): IBaseSkill[] {
if (Object.keys(skills ?? []).length === 0) {
return [];
}
return Object.keys(skills)
.map((skillKey): IBaseSkill =>
{
.map((skillKey): IBaseSkill => {
// Get skill from dict, skip if not found
const skill = skills[skillKey];
if (!skill)
{
if (!skill) {
return undefined;
}
@ -544,8 +505,7 @@ export class BotGenerator
const skillToAdd: IBaseSkill = { Id: skillKey, Progress: this.randomUtil.getInt(skill.min, skill.max) };
// Common skills have additional props
if (isCommonSkills)
{
if (isCommonSkills) {
(skillToAdd as Common).PointsEarnedDuringSession = 0;
(skillToAdd as Common).LastAccess = 0;
}
@ -560,8 +520,7 @@ export class BotGenerator
* @param bot bot to update
* @returns updated IBotBase object
*/
protected addIdsToBot(bot: IBotBase): void
{
protected addIdsToBot(bot: IBotBase): void {
const botId = this.hashUtil.generate();
bot._id = botId;
@ -573,15 +532,12 @@ export class BotGenerator
* Update all inventory items that make use of this value too
* @param profile Profile to update
*/
protected generateInventoryId(profile: IBotBase): void
{
protected generateInventoryId(profile: IBotBase): void {
const newInventoryItemId = this.hashUtil.generate();
for (const item of profile.Inventory.items)
{
for (const item of profile.Inventory.items) {
// Root item found, update its _id value to newly generated id
if (item._tpl === ItemTpl.INVENTORY_DEFAULT)
{
if (item._tpl === ItemTpl.INVENTORY_DEFAULT) {
item._id = newInventoryItemId;
continue;
@ -589,14 +545,12 @@ export class BotGenerator
// Optimisation - skip items without a parentId
// They are never linked to root inventory item + we already handled root item above
if (!item.parentId)
{
if (!item.parentId) {
continue;
}
// Item is a child of root inventory item, update its parentId value to newly generated id
if (item.parentId === profile.Inventory.equipment)
{
if (item.parentId === profile.Inventory.equipment) {
item.parentId = newInventoryItemId;
}
}
@ -612,11 +566,9 @@ export class BotGenerator
* @param botInfo bot info object to update
* @returns Chosen game version
*/
protected setRandomisedGameVersionAndCategory(botInfo: Info): string
{
protected setRandomisedGameVersionAndCategory(botInfo: Info): string {
// Special case
if (botInfo.Nickname.toLowerCase() === "nikita")
{
if (botInfo.Nickname.toLowerCase() === "nikita") {
botInfo.GameVersion = GameEditions.UNHEARD;
botInfo.MemberCategory = MemberCategory.DEVELOPER;
@ -627,8 +579,7 @@ export class BotGenerator
botInfo.GameVersion = this.weightedRandomHelper.getWeightedValue(this.pmcConfig.gameVersionWeight);
// Choose appropriate member category value
switch (botInfo.GameVersion)
{
switch (botInfo.GameVersion) {
case GameEditions.EDGE_OF_DARKNESS:
botInfo.MemberCategory = MemberCategory.UNIQUE_ID;
break;
@ -654,8 +605,7 @@ export class BotGenerator
* @param bot bot to add dogtag to
* @returns Bot with dogtag added
*/
protected addDogtagToBot(bot: IBotBase): void
{
protected addDogtagToBot(bot: IBotBase): void {
const dogtagUpd: Upd = {
SpawnedInSession: true,
Dogtag: {
@ -691,12 +641,9 @@ export class BotGenerator
* @param gameVersion edge_of_darkness / standard
* @returns item tpl
*/
protected getDogtagTplByGameVersionAndSide(side: string, gameVersion: string): string
{
if (side.toLowerCase() == "usec")
{
switch (gameVersion)
{
protected getDogtagTplByGameVersionAndSide(side: string, gameVersion: string): string {
if (side.toLowerCase() == "usec") {
switch (gameVersion) {
case GameEditions.EDGE_OF_DARKNESS:
return ItemTpl.BARTER_DOGTAG_USEC_EOD;
case GameEditions.UNHEARD:
@ -706,8 +653,7 @@ export class BotGenerator
}
}
switch (gameVersion)
{
switch (gameVersion) {
case GameEditions.EDGE_OF_DARKNESS:
return ItemTpl.BARTER_DOGTAG_BEAR_EOD;
case GameEditions.UNHEARD:
@ -721,10 +667,8 @@ export class BotGenerator
* Adjust a PMCs pocket tpl to UHD if necessary, otherwise do nothing
* @param bot Pmc object to adjust
*/
protected setPmcPocketsByGameVersion(bot: IBotBase): void
{
if (bot.Info.GameVersion === GameEditions.UNHEARD)
{
protected setPmcPocketsByGameVersion(bot: IBotBase): void {
if (bot.Info.GameVersion === GameEditions.UNHEARD) {
const pockets = bot.Inventory.items.find((item) => item.slotId === "Pockets");
pockets._tpl = ItemTpl.POCKETS_1X4_TUE;
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { BotEquipmentModGenerator } from "@spt/generators/BotEquipmentModGenerator";
import { BotLootGenerator } from "@spt/generators/BotLootGenerator";
import { BotWeaponGenerator } from "@spt/generators/BotWeaponGenerator";
@ -14,10 +13,7 @@ import { EquipmentSlots } from "@spt/models/enums/EquipmentSlots";
import { GameEditions } from "@spt/models/enums/GameEditions";
import { ItemTpl } from "@spt/models/enums/ItemTpl";
import { IGenerateEquipmentProperties } from "@spt/models/spt/bots/IGenerateEquipmentProperties";
import {
EquipmentFilterDetails,
IBotConfig,
} from "@spt/models/spt/config/IBotConfig";
import { EquipmentFilterDetails, IBotConfig } from "@spt/models/spt/config/IBotConfig";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { BotEquipmentModPoolService } from "@spt/services/BotEquipmentModPoolService";
@ -25,10 +21,10 @@ import { DatabaseService } from "@spt/services/DatabaseService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { HashUtil } from "@spt/utils/HashUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class BotInventoryGenerator
{
export class BotInventoryGenerator {
protected botConfig: IBotConfig;
constructor(
@ -46,8 +42,7 @@ export class BotInventoryGenerator
@inject("BotEquipmentModPoolService") protected botEquipmentModPoolService: BotEquipmentModPoolService,
@inject("BotEquipmentModGenerator") protected botEquipmentModGenerator: BotEquipmentModGenerator,
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
) {
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
}
@ -68,8 +63,7 @@ export class BotInventoryGenerator
isPmc: boolean,
botLevel: number,
chosenGameVersion: string,
): PmcInventory
{
): PmcInventory {
const templateInventory = botJsonTemplate.inventory;
const wornItemChances = botJsonTemplate.chances;
const itemGenerationLimitsMinMax = botJsonTemplate.generation;
@ -83,7 +77,8 @@ export class BotInventoryGenerator
botRole,
botInventory,
botLevel,
chosenGameVersion);
chosenGameVersion,
);
// Roll weapon spawns (primary/secondary/holster) and generate a weapon for each roll that passed
this.generateAndAddWeaponsToBot(
@ -107,8 +102,7 @@ export class BotInventoryGenerator
* Create a pmcInventory object with all the base/generic items needed
* @returns PmcInventory object
*/
protected generateInventoryBase(): PmcInventory
{
protected generateInventoryBase(): PmcInventory {
const equipmentId = this.hashUtil.generate();
const stashId = this.hashUtil.generate();
const questRaidItemsId = this.hashUtil.generate();
@ -150,8 +144,7 @@ export class BotInventoryGenerator
botInventory: PmcInventory,
botLevel: number,
chosenGameVersion: string,
): void
{
): void {
// These will be handled later
const excludedSlots: string[] = [
EquipmentSlots.POCKETS,
@ -167,11 +160,9 @@ export class BotInventoryGenerator
const botEquipConfig = this.botConfig.equipment[this.botGeneratorHelper.getBotEquipmentRole(botRole)];
const randomistionDetails = this.botHelper.getBotRandomizationDetails(botLevel, botEquipConfig);
for (const equipmentSlot in templateInventory.equipment)
{
for (const equipmentSlot in templateInventory.equipment) {
// Weapons have special generation and will be generated separately; ArmorVest should be generated after TactivalVest
if (excludedSlots.includes(equipmentSlot))
{
if (excludedSlots.includes(equipmentSlot)) {
continue;
}
@ -191,7 +182,8 @@ export class BotInventoryGenerator
// Generate below in specific order
this.generateEquipment({
rootEquipmentSlot: EquipmentSlots.POCKETS,
rootEquipmentPool: chosenGameVersion === GameEditions.UNHEARD
rootEquipmentPool:
chosenGameVersion === GameEditions.UNHEARD
? { [ItemTpl.POCKETS_1X4_TUE]: 1 }
: templateInventory.equipment.Pockets,
modPool: templateInventory.mods,
@ -249,15 +241,13 @@ export class BotInventoryGenerator
});
// Bot has no armor vest and flagged to be foreced to wear armored rig in this event
if (botEquipConfig.forceOnlyArmoredRigWhenNoArmor && !hasArmorVest)
{
if (botEquipConfig.forceOnlyArmoredRigWhenNoArmor && !hasArmorVest) {
// Filter rigs down to only those with armor
this.filterRigsToThoseWithProtection(templateInventory.equipment);
}
// Optimisation - Remove armored rigs from pool
if (hasArmorVest)
{
if (hasArmorVest) {
// Filter rigs down to only those with armor
this.filterRigsToThoseWithoutProtection(templateInventory.equipment);
}
@ -279,13 +269,10 @@ export class BotInventoryGenerator
* Remove non-armored rigs from parameter data
* @param templateEquipment Equpiment to filter TacticalVest of
*/
protected filterRigsToThoseWithProtection(templateEquipment: Equipment): void
{
protected filterRigsToThoseWithProtection(templateEquipment: Equipment): void {
const tacVestsWithArmor = Object.entries(templateEquipment.TacticalVest).reduce(
(newVestDictionary, [tplKey]) =>
{
if (this.itemHelper.itemHasSlots(tplKey))
{
(newVestDictionary, [tplKey]) => {
if (this.itemHelper.itemHasSlots(tplKey)) {
newVestDictionary[tplKey] = templateEquipment.TacticalVest[tplKey];
}
return newVestDictionary;
@ -300,13 +287,10 @@ export class BotInventoryGenerator
* Remove armored rigs from parameter data
* @param templateEquipment Equpiment to filter TacticalVest of
*/
protected filterRigsToThoseWithoutProtection(templateEquipment: Equipment): void
{
protected filterRigsToThoseWithoutProtection(templateEquipment: Equipment): void {
const tacVestsWithoutArmor = Object.entries(templateEquipment.TacticalVest).reduce(
(newVestDictionary, [tplKey]) =>
{
if (!this.itemHelper.itemHasSlots(tplKey))
{
(newVestDictionary, [tplKey]) => {
if (!this.itemHelper.itemHasSlots(tplKey)) {
newVestDictionary[tplKey] = templateEquipment.TacticalVest[tplKey];
}
return newVestDictionary;
@ -322,16 +306,14 @@ export class BotInventoryGenerator
* @param settings Values to adjust how item is chosen and added to bot
* @returns true when item added
*/
protected generateEquipment(settings: IGenerateEquipmentProperties): boolean
{
protected generateEquipment(settings: IGenerateEquipmentProperties): boolean {
const spawnChance = ([EquipmentSlots.POCKETS, EquipmentSlots.SECURED_CONTAINER] as string[]).includes(
settings.rootEquipmentSlot,
)
? 100
: settings.spawnChances.equipment[settings.rootEquipmentSlot];
if (typeof spawnChance === "undefined")
{
if (typeof spawnChance === "undefined") {
this.logger.warning(
this.localisationService.getText(
"bot-no_spawn_chance_defined_for_equipment_slot",
@ -343,25 +325,21 @@ export class BotInventoryGenerator
}
const shouldSpawn = this.randomUtil.getChance100(spawnChance);
if (shouldSpawn && Object.keys(settings.rootEquipmentPool).length)
{
if (shouldSpawn && Object.keys(settings.rootEquipmentPool).length) {
let pickedItemDb: ITemplateItem;
let found = false;
const maxAttempts = Math.round(Object.keys(settings.rootEquipmentPool).length * 0.75); // Roughly 75% of pool size
let attempts = 0;
while (!found)
{
if (Object.values(settings.rootEquipmentPool).length === 0)
{
while (!found) {
if (Object.values(settings.rootEquipmentPool).length === 0) {
return false;
}
const chosenItemTpl = this.weightedRandomHelper.getWeightedValue<string>(settings.rootEquipmentPool);
const dbResult = this.itemHelper.getItem(chosenItemTpl);
if (!dbResult[0])
{
if (!dbResult[0]) {
this.logger.error(this.localisationService.getText("bot-missing_item_template", chosenItemTpl));
this.logger.info(`EquipmentSlot -> ${settings.rootEquipmentSlot}`);
@ -378,11 +356,9 @@ export class BotInventoryGenerator
chosenItemTpl,
settings.rootEquipmentSlot,
);
if (compatabilityResult.incompatible)
{
if (compatabilityResult.incompatible) {
// Tried x different items that failed, stop
if (attempts > maxAttempts)
{
if (attempts > maxAttempts) {
return false;
}
@ -390,9 +366,7 @@ export class BotInventoryGenerator
delete settings.rootEquipmentPool[chosenItemTpl];
attempts++;
}
else
{
} else {
// Success
found = true;
pickedItemDb = dbResult[1];
@ -412,10 +386,9 @@ export class BotInventoryGenerator
// Use dynamic mod pool if enabled in config for this bot
const botEquipmentRole = this.botGeneratorHelper.getBotEquipmentRole(settings.botRole);
if (
this.botConfig.equipment[botEquipmentRole]
&& settings.randomisationDetails?.randomisedArmorSlots?.includes(settings.rootEquipmentSlot)
)
{
this.botConfig.equipment[botEquipmentRole] &&
settings.randomisationDetails?.randomisedArmorSlots?.includes(settings.rootEquipmentSlot)
) {
settings.modPool[pickedItemDb._id] = this.getFilteredDynamicModsForItem(
pickedItemDb._id,
this.botConfig.equipment[botEquipmentRole].blacklist,
@ -423,8 +396,7 @@ export class BotInventoryGenerator
}
// Item has slots, fill them
if (pickedItemDb._props.Slots?.length > 0 && !settings.generateModsBlacklist?.includes(pickedItemDb._id))
{
if (pickedItemDb._props.Slots?.length > 0 && !settings.generateModsBlacklist?.includes(pickedItemDb._id)) {
const items = this.botEquipmentModGenerator.generateModsForEquipment(
[item],
id,
@ -432,9 +404,7 @@ export class BotInventoryGenerator
settings,
);
settings.inventory.items.push(...items);
}
else
{
} else {
// No slots, push root item only
settings.inventory.items.push(item);
}
@ -454,16 +424,13 @@ export class BotInventoryGenerator
protected getFilteredDynamicModsForItem(
itemTpl: string,
equipmentBlacklist: EquipmentFilterDetails[],
): Record<string, string[]>
{
): Record<string, string[]> {
const modPool = this.botEquipmentModPoolService.getModsForGearSlot(itemTpl);
for (const modSlot of Object.keys(modPool ?? []))
{
for (const modSlot of Object.keys(modPool ?? [])) {
const blacklistedMods = equipmentBlacklist[0]?.equipment[modSlot] || [];
const filteredMods = modPool[modSlot].filter((x) => !blacklistedMods.includes(x));
if (filteredMods.length > 0)
{
if (filteredMods.length > 0) {
modPool[modSlot] = filteredMods;
}
}
@ -491,14 +458,11 @@ export class BotInventoryGenerator
isPmc: boolean,
itemGenerationLimitsMinMax: Generation,
botLevel: number,
): void
{
): void {
const weaponSlotsToFill = this.getDesiredWeaponsForBot(equipmentChances);
for (const weaponSlot of weaponSlotsToFill)
{
for (const weaponSlot of weaponSlotsToFill) {
// Add weapon to bot if true and bot json has something to put into the slot
if (weaponSlot.shouldSpawn && Object.keys(templateInventory.equipment[weaponSlot.slot]).length)
{
if (weaponSlot.shouldSpawn && Object.keys(templateInventory.equipment[weaponSlot.slot]).length) {
this.addWeaponAndMagazinesToInventory(
sessionId,
weaponSlot,
@ -519,8 +483,7 @@ export class BotInventoryGenerator
* @param equipmentChances Chances bot has certain equipment
* @returns What slots bot should have weapons generated for
*/
protected getDesiredWeaponsForBot(equipmentChances: Chances): { slot: EquipmentSlots, shouldSpawn: boolean }[]
{
protected getDesiredWeaponsForBot(equipmentChances: Chances): { slot: EquipmentSlots; shouldSpawn: boolean }[] {
const shouldSpawnPrimary = this.randomUtil.getChance100(equipmentChances.equipment.FirstPrimaryWeapon);
return [
{ slot: EquipmentSlots.FIRST_PRIMARY_WEAPON, shouldSpawn: shouldSpawnPrimary },
@ -552,7 +515,7 @@ export class BotInventoryGenerator
*/
protected addWeaponAndMagazinesToInventory(
sessionId: string,
weaponSlot: { slot: EquipmentSlots, shouldSpawn: boolean },
weaponSlot: { slot: EquipmentSlots; shouldSpawn: boolean },
templateInventory: Inventory,
botInventory: PmcInventory,
equipmentChances: Chances,
@ -560,8 +523,7 @@ export class BotInventoryGenerator
isPmc: boolean,
itemGenerationWeights: Generation,
botLevel: number,
): void
{
): void {
const generatedWeapon = this.botWeaponGenerator.generateRandomWeapon(
sessionId,
weaponSlot.slot,

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { MinMax } from "@spt/models/common/MinMax";
import { IRandomisedBotLevelResult } from "@spt/models/eft/bot/IRandomisedBotLevelResult";
import { IBotBase } from "@spt/models/eft/common/tables/IBotBase";
@ -6,16 +5,15 @@ import { BotGenerationDetails } from "@spt/models/spt/bots/BotGenerationDetails"
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { DatabaseService } from "@spt/services/DatabaseService";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class BotLevelGenerator
{
export class BotLevelGenerator {
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("DatabaseService") protected databaseService: DatabaseService,
)
{}
) {}
/**
* Return a randomised bot level and exp value
@ -28,8 +26,7 @@ export class BotLevelGenerator
levelDetails: MinMax,
botGenerationDetails: BotGenerationDetails,
bot: IBotBase,
): IRandomisedBotLevelResult
{
): IRandomisedBotLevelResult {
const expTable = this.databaseService.getGlobals().config.exp.level.exp_table;
const highestLevel = this.getHighestRelativeBotLevel(botGenerationDetails, levelDetails, expTable.length);
const lowestLevel = this.getLowestRelativeBotLevel(botGenerationDetails, levelDetails, expTable.length);
@ -38,14 +35,12 @@ export class BotLevelGenerator
let exp = 0;
const level = this.randomUtil.getInt(lowestLevel, highestLevel);
for (let i = 0; i < level; i++)
{
for (let i = 0; i < level; i++) {
exp += expTable[i].exp;
}
// Sprinkle in some random exp within the level, unless we are at max level.
if (level < expTable.length - 1)
{
if (level < expTable.length - 1) {
exp += this.randomUtil.getInt(0, expTable[level].exp - 1);
}
@ -63,16 +58,14 @@ export class BotLevelGenerator
botGenerationDetails: BotGenerationDetails,
levelDetails: MinMax,
maxLevel: number,
): number
{
const maxPossibleLevel
= botGenerationDetails.isPmc && botGenerationDetails.locationSpecificPmcLevelOverride
): number {
const maxPossibleLevel =
botGenerationDetails.isPmc && botGenerationDetails.locationSpecificPmcLevelOverride
? Math.min(botGenerationDetails.locationSpecificPmcLevelOverride.max, maxLevel) // Was a PMC and they have a level override
: Math.min(levelDetails.max, maxLevel); // Not pmc with override or non-pmc
let level = botGenerationDetails.playerLevel + botGenerationDetails.botRelativeLevelDeltaMax;
if (level > maxPossibleLevel)
{
if (level > maxPossibleLevel) {
level = maxPossibleLevel;
}
@ -90,10 +83,9 @@ export class BotLevelGenerator
botGenerationDetails: BotGenerationDetails,
levelDetails: MinMax,
maxlevel: number,
): number
{
const minPossibleLevel
= botGenerationDetails.isPmc && botGenerationDetails.locationSpecificPmcLevelOverride
): number {
const minPossibleLevel =
botGenerationDetails.isPmc && botGenerationDetails.locationSpecificPmcLevelOverride
? Math.min(
Math.max(levelDetails.min, botGenerationDetails.locationSpecificPmcLevelOverride.min), // Biggest between json min and the botgen min
maxlevel, // Fallback if value above is crazy (default is 79)
@ -101,8 +93,7 @@ export class BotLevelGenerator
: Math.min(levelDetails.min, maxlevel); // Not pmc with override or non-pmc
let level = botGenerationDetails.playerLevel - botGenerationDetails.botRelativeLevelDeltaMin;
if (level < minPossibleLevel)
{
if (level < minPossibleLevel) {
level = minPossibleLevel;
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { BotWeaponGenerator } from "@spt/generators/BotWeaponGenerator";
import { BotGeneratorHelper } from "@spt/helpers/BotGeneratorHelper";
import { BotHelper } from "@spt/helpers/BotHelper";
@ -23,13 +22,13 @@ import { ConfigServer } from "@spt/servers/ConfigServer";
import { BotLootCacheService } from "@spt/services/BotLootCacheService";
import { DatabaseService } from "@spt/services/DatabaseService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HashUtil } from "@spt/utils/HashUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class BotLootGenerator
{
export class BotLootGenerator {
protected botConfig: IBotConfig;
protected pmcConfig: IPmcConfig;
@ -49,14 +48,12 @@ export class BotLootGenerator
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
) {
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
}
protected getItemSpawnLimitsForBot(botRole: string): IItemSpawnLimitSettings
{
protected getItemSpawnLimitsForBot(botRole: string): IItemSpawnLimitSettings {
// Init item limits
const limitsForBotDict: Record<string, number> = {};
this.initItemLimitArray(botRole, limitsForBotDict);
@ -80,25 +77,23 @@ export class BotLootGenerator
botRole: string,
botInventory: PmcInventory,
botLevel: number,
): void
{
): void {
// Limits on item types to be added as loot
const itemCounts = botJsonTemplate.generation.items;
if (
!itemCounts.backpackLoot.weights
|| !itemCounts.pocketLoot.weights
|| !itemCounts.vestLoot.weights
|| !itemCounts.specialItems.weights
|| !itemCounts.healing.weights
|| !itemCounts.drugs.weights
|| !itemCounts.food.weights
|| !itemCounts.drink.weights
|| !itemCounts.currency.weights
|| !itemCounts.stims.weights
|| !itemCounts.grenades.weights
)
{
!itemCounts.backpackLoot.weights ||
!itemCounts.pocketLoot.weights ||
!itemCounts.vestLoot.weights ||
!itemCounts.specialItems.weights ||
!itemCounts.healing.weights ||
!itemCounts.drugs.weights ||
!itemCounts.food.weights ||
!itemCounts.drink.weights ||
!itemCounts.currency.weights ||
!itemCounts.stims.weights ||
!itemCounts.grenades.weights
) {
this.logger.warning(this.localisationService.getText("bot-unable_to_generate_bot_loot", botRole));
return;
@ -107,9 +102,7 @@ export class BotLootGenerator
let backpackLootCount = Number(
this.weightedRandomHelper.getWeightedValue<number>(itemCounts.backpackLoot.weights),
);
let pocketLootCount = Number(
this.weightedRandomHelper.getWeightedValue<number>(itemCounts.pocketLoot.weights),
);
let pocketLootCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.pocketLoot.weights));
let vestLootCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.vestLoot.weights);
const specialLootItemCount = Number(
this.weightedRandomHelper.getWeightedValue<number>(itemCounts.specialItems.weights),
@ -120,16 +113,13 @@ export class BotLootGenerator
const foodItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.food.weights));
const drinkItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.drink.weights));
let currencyItemCount = Number(
this.weightedRandomHelper.getWeightedValue<number>(itemCounts.currency.weights),
);
let currencyItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.currency.weights));
const stimItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.stims.weights));
const grenadeCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.grenades.weights));
// If bot has been flagged as not having loot, set below counts to 0
if (this.botConfig.disableLootOnBotTypes?.includes(botRole.toLowerCase()))
{
if (this.botConfig.disableLootOnBotTypes?.includes(botRole.toLowerCase())) {
backpackLootCount = 0;
pocketLootCount = 0;
vestLootCount = 0;
@ -137,8 +127,7 @@ export class BotLootGenerator
}
// Forced pmc healing loot into secure container
if (isPmc && this.pmcConfig.forceHealingItemsIntoSecure)
{
if (isPmc && this.pmcConfig.forceHealingItemsIntoSecure) {
this.addForcedMedicalItemsToPmcSecure(botInventory, botRole);
}
@ -255,11 +244,9 @@ export class BotLootGenerator
);
// Backpack - generate loot if they have one
if (containersBotHasAvailable.includes(EquipmentSlots.BACKPACK))
{
if (containersBotHasAvailable.includes(EquipmentSlots.BACKPACK)) {
// Add randomly generated weapon to PMC backpacks
if (isPmc && this.randomUtil.getChance100(this.pmcConfig.looseWeaponInBackpackChancePercent))
{
if (isPmc && this.randomUtil.getChance100(this.pmcConfig.looseWeaponInBackpackChancePercent)) {
this.addLooseWeaponsToInventorySlot(
sessionId,
botInventory,
@ -287,8 +274,7 @@ export class BotLootGenerator
}
// TacticalVest - generate loot if they have one
if (containersBotHasAvailable.includes(EquipmentSlots.TACTICAL_VEST))
{
if (containersBotHasAvailable.includes(EquipmentSlots.TACTICAL_VEST)) {
// Vest
this.addLootFromPool(
this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.VEST, botJsonTemplate),
@ -319,8 +305,7 @@ export class BotLootGenerator
// Secure
// only add if not a pmc or is pmc and flag is true
if (!isPmc || (isPmc && this.pmcConfig.addSecureContainerLootFromBotConfig))
{
if (!isPmc || (isPmc && this.pmcConfig.addSecureContainerLootFromBotConfig)) {
this.addLootFromPool(
this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.SECURE, botJsonTemplate),
[EquipmentSlots.SECURED_CONTAINER],
@ -340,17 +325,14 @@ export class BotLootGenerator
* @param botInventory Bot to check
* @returns Array of available slots
*/
protected getAvailableContainersBotCanStoreItemsIn(botInventory: PmcInventory): EquipmentSlots[]
{
protected getAvailableContainersBotCanStoreItemsIn(botInventory: PmcInventory): EquipmentSlots[] {
const result = [EquipmentSlots.POCKETS];
if (botInventory.items.some((item) => item.slotId === EquipmentSlots.TACTICAL_VEST))
{
if (botInventory.items.some((item) => item.slotId === EquipmentSlots.TACTICAL_VEST)) {
result.push(EquipmentSlots.TACTICAL_VEST);
}
if (botInventory.items.some((item) => item.slotId === EquipmentSlots.BACKPACK))
{
if (botInventory.items.some((item) => item.slotId === EquipmentSlots.BACKPACK)) {
result.push(EquipmentSlots.BACKPACK);
}
@ -362,8 +344,7 @@ export class BotLootGenerator
* @param botInventory Inventory to add items to
* @param botRole Role of bot (pmcBEAR/pmcUSEC)
*/
protected addForcedMedicalItemsToPmcSecure(botInventory: PmcInventory, botRole: string): void
{
protected addForcedMedicalItemsToPmcSecure(botInventory: PmcInventory, botRole: string): void {
// surv12
this.addLootFromPool(
{ "5d02797c86f774203f38e30a": 1 },
@ -396,8 +377,7 @@ export class BotLootGenerator
* @param nValue Value to bias choice
* @returns Chosen number
*/
protected getRandomisedCount(min: number, max: number, nValue: number): number
{
protected getRandomisedCount(min: number, max: number, nValue: number): number {
const range = max - min;
return this.randomUtil.getBiasedRandomNumber(min, max, range, nValue);
}
@ -423,28 +403,23 @@ export class BotLootGenerator
totalValueLimitRub = 0,
isPmc = false,
containersIdFull = new Set<string>(),
): void
{
): void {
// Loot pool has items
const poolSize = Object.keys(pool).length;
if (poolSize > 0)
{
if (poolSize > 0) {
let currentTotalRub = 0;
let fitItemIntoContainerAttempts = 0;
for (let i = 0; i < totalItemCount; i++)
{
for (let i = 0; i < totalItemCount; i++) {
// Pool can become empty if item spawn limits keep removing items
if (Object.keys(pool).length === 0)
{
if (Object.keys(pool).length === 0) {
return;
}
const weightedItemTpl = this.weightedRandomHelper.getWeightedValue<string>(pool);
const itemResult = this.itemHelper.getItem(weightedItemTpl);
const itemToAddTemplate = itemResult[1];
if (!itemResult[0])
{
if (!itemResult[0]) {
this.logger.warning(
`Unable to process item tpl: ${weightedItemTpl} for slots: ${equipmentSlots} on bot: ${botRole}`,
);
@ -452,10 +427,8 @@ export class BotLootGenerator
continue;
}
if (itemSpawnLimits)
{
if (this.itemHasReachedSpawnLimit(itemToAddTemplate, botRole, itemSpawnLimits))
{
if (itemSpawnLimits) {
if (this.itemHasReachedSpawnLimit(itemToAddTemplate, botRole, itemSpawnLimits)) {
// Remove item from pool to prevent it being picked again
delete pool[weightedItemTpl];
@ -474,11 +447,9 @@ export class BotLootGenerator
];
// Is Simple-Wallet / WZ wallet
if (this.botConfig.walletLoot.walletTplPool.includes(weightedItemTpl))
{
if (this.botConfig.walletLoot.walletTplPool.includes(weightedItemTpl)) {
const addCurrencyToWallet = this.randomUtil.getChance100(this.botConfig.walletLoot.chancePercent);
if (addCurrencyToWallet)
{
if (addCurrencyToWallet) {
// Create the currency items we want to add to wallet
const itemsToAdd = this.createWalletLoot(newRootItemId);
@ -490,11 +461,9 @@ export class BotLootGenerator
this.cloner.clone(containerGrid), // MUST clone grid before passing in as function modifies grid
itemsToAdd,
);
if (canAddToContainer)
{
if (canAddToContainer) {
// Add each currency to wallet
for (const itemToAdd of itemsToAdd)
{
for (const itemToAdd of itemsToAdd) {
this.inventoryHelper.placeItemInContainer(
containerGrid,
itemToAdd,
@ -503,7 +472,7 @@ export class BotLootGenerator
);
}
itemWithChildrenToAdd.push(...itemsToAdd.flatMap((moneyStack) => moneyStack));
itemWithChildrenToAdd.push(...itemsToAdd.flat());
}
}
}
@ -521,10 +490,8 @@ export class BotLootGenerator
);
// Handle when item cannot be added
if (itemAddedResult !== ItemAddedResult.SUCCESS)
{
if (itemAddedResult === ItemAddedResult.NO_CONTAINERS)
{
if (itemAddedResult !== ItemAddedResult.SUCCESS) {
if (itemAddedResult === ItemAddedResult.NO_CONTAINERS) {
// Bot has no container to put item in, exit
this.logger.debug(
`Unable to add: ${totalItemCount} items to bot as it lacks a container to include them`,
@ -533,8 +500,7 @@ export class BotLootGenerator
}
fitItemIntoContainerAttempts++;
if (fitItemIntoContainerAttempts >= 4)
{
if (fitItemIntoContainerAttempts >= 4) {
this.logger.debug(
`Failed to place item ${i} of ${totalItemCount} items into ${botRole} containers: ${equipmentSlots.join(
",",
@ -554,11 +520,9 @@ export class BotLootGenerator
fitItemIntoContainerAttempts = 0;
// Stop adding items to bots pool if rolling total is over total limit
if (totalValueLimitRub > 0)
{
if (totalValueLimitRub > 0) {
currentTotalRub += this.handbookHelper.getTemplatePrice(itemToAddTemplate._id);
if (currentTotalRub > totalValueLimitRub)
{
if (currentTotalRub > totalValueLimitRub) {
break;
}
}
@ -566,8 +530,7 @@ export class BotLootGenerator
}
}
protected createWalletLoot(walletId: string): Item[][]
{
protected createWalletLoot(walletId: string): Item[][] {
const result: Item[][] = [];
// Choose how many stacks of currency will be added to wallet
@ -575,8 +538,7 @@ export class BotLootGenerator
this.botConfig.walletLoot.itemCount.min,
this.botConfig.walletLoot.itemCount.max,
);
for (let index = 0; index < itemCount; index++)
{
for (let index = 0; index < itemCount; index++) {
// Choose the size of the currency stack - default is 5k, 10k, 15k, 20k, 25k
const chosenStackCount = Number(
this.weightedRandomHelper.getWeightedValue<string>(this.botConfig.walletLoot.stackSizeWeight),
@ -606,26 +568,21 @@ export class BotLootGenerator
itemToAddChildrenTo: Item[],
isPmc: boolean,
botRole: string,
): void
{
): void {
// Fill ammo box
if (this.itemHelper.isOfBaseclass(itemToAddTemplate._id, BaseClasses.AMMO_BOX))
{
if (this.itemHelper.isOfBaseclass(itemToAddTemplate._id, BaseClasses.AMMO_BOX)) {
this.itemHelper.addCartridgesToAmmoBox(itemToAddChildrenTo, itemToAddTemplate);
}
// Make money a stack
else if (this.itemHelper.isOfBaseclass(itemToAddTemplate._id, BaseClasses.MONEY))
{
else if (this.itemHelper.isOfBaseclass(itemToAddTemplate._id, BaseClasses.MONEY)) {
this.randomiseMoneyStackSize(botRole, itemToAddTemplate, itemToAddChildrenTo[0]);
}
// Make ammo a stack
else if (this.itemHelper.isOfBaseclass(itemToAddTemplate._id, BaseClasses.AMMO))
{
else if (this.itemHelper.isOfBaseclass(itemToAddTemplate._id, BaseClasses.AMMO)) {
this.randomiseAmmoStackSize(isPmc, itemToAddTemplate, itemToAddChildrenTo[0]);
}
// Must add soft inserts/plates
else if (this.itemHelper.itemRequiresSoftInserts(itemToAddTemplate._id))
{
else if (this.itemHelper.itemRequiresSoftInserts(itemToAddTemplate._id)) {
this.itemHelper.addChildSlotItems(itemToAddChildrenTo, itemToAddTemplate, undefined, false);
}
}
@ -649,8 +606,7 @@ export class BotLootGenerator
isPmc: boolean,
botLevel: number,
containersIdFull?: Set<string>,
): void
{
): void {
const chosenWeaponType = this.randomUtil.getArrayValue([
EquipmentSlots.FIRST_PRIMARY_WEAPON,
EquipmentSlots.FIRST_PRIMARY_WEAPON,
@ -661,10 +617,8 @@ export class BotLootGenerator
this.pmcConfig.looseWeaponInBackpackLootMinMax.min,
this.pmcConfig.looseWeaponInBackpackLootMinMax.max,
);
if (randomisedWeaponCount > 0)
{
for (let i = 0; i < randomisedWeaponCount; i++)
{
if (randomisedWeaponCount > 0) {
for (let i = 0; i < randomisedWeaponCount; i++) {
const generatedWeapon = this.botWeaponGenerator.generateRandomWeapon(
sessionId,
chosenWeaponType,
@ -684,8 +638,7 @@ export class BotLootGenerator
containersIdFull,
);
if (result !== ItemAddedResult.SUCCESS)
{
if (result !== ItemAddedResult.SUCCESS) {
this.logger.debug(
`Failed to add additional weapon ${generatedWeapon.weapon[0]._id} to bot backpack, reason: ${ItemAddedResult[result]}`,
);
@ -700,12 +653,10 @@ export class BotLootGenerator
* @param botRole Role the bot has
* @param limitCount
*/
protected initItemLimitArray(botRole: string, limitCount: Record<string, number>): void
{
protected initItemLimitArray(botRole: string, limitCount: Record<string, number>): void {
// Init current count of items we want to limit
const spawnLimits = this.getItemSpawnLimitsForBotType(botRole);
for (const limit in spawnLimits)
{
for (const limit in spawnLimits) {
limitCount[limit] = 0;
}
}
@ -721,24 +672,20 @@ export class BotLootGenerator
itemTemplate: ITemplateItem,
botRole: string,
itemSpawnLimits: IItemSpawnLimitSettings,
): boolean
{
): boolean {
// PMCs and scavs have different sections of bot config for spawn limits
if (!!itemSpawnLimits && Object.keys(itemSpawnLimits.globalLimits).length === 0)
{
if (!!itemSpawnLimits && Object.keys(itemSpawnLimits.globalLimits).length === 0) {
// No items found in spawn limit, drop out
return false;
}
// No spawn limits, skipping
if (!itemSpawnLimits)
{
if (!itemSpawnLimits) {
return false;
}
const idToCheckFor = this.getMatchingIdFromSpawnLimits(itemTemplate, itemSpawnLimits.globalLimits);
if (!idToCheckFor)
{
if (!idToCheckFor) {
// ParentId or tplid not found in spawnLimits, not a spawn limited item, skip
return false;
}
@ -747,11 +694,9 @@ export class BotLootGenerator
itemSpawnLimits.currentLimits[idToCheckFor]++;
// Check if over limit
if (itemSpawnLimits.currentLimits[idToCheckFor] > itemSpawnLimits.globalLimits[idToCheckFor])
{
if (itemSpawnLimits.currentLimits[idToCheckFor] > itemSpawnLimits.globalLimits[idToCheckFor]) {
// Prevent edge-case of small loot pools + code trying to add limited item over and over infinitely
if (itemSpawnLimits.currentLimits[idToCheckFor] > itemSpawnLimits[idToCheckFor] * 10)
{
if (itemSpawnLimits.currentLimits[idToCheckFor] > itemSpawnLimits[idToCheckFor] * 10) {
this.logger.debug(
this.localisationService.getText("bot-item_spawn_limit_reached_skipping_item", {
botRole: botRole,
@ -775,12 +720,10 @@ export class BotLootGenerator
* @param itemTemplate item details from db
* @param moneyItem Money item to randomise
*/
protected randomiseMoneyStackSize(botRole: string, itemTemplate: ITemplateItem, moneyItem: Item): void
{
protected randomiseMoneyStackSize(botRole: string, itemTemplate: ITemplateItem, moneyItem: Item): void {
// Get all currency weights for this bot type
let currencyWeights = this.botConfig.currencyStackSize[botRole];
if (!currencyWeights)
{
if (!currencyWeights) {
currencyWeights = this.botConfig.currencyStackSize.default;
}
@ -797,10 +740,9 @@ export class BotLootGenerator
* @param itemTemplate item details from db
* @param ammoItem Ammo item to randomise
*/
protected randomiseAmmoStackSize(isPmc: boolean, itemTemplate: ITemplateItem, ammoItem: Item): void
{
const randomSize
= itemTemplate._props.StackMaxSize === 1
protected randomiseAmmoStackSize(isPmc: boolean, itemTemplate: ITemplateItem, ammoItem: Item): void {
const randomSize =
itemTemplate._props.StackMaxSize === 1
? 1
: this.randomUtil.getInt(
itemTemplate._props.StackMinRandom,
@ -818,15 +760,12 @@ export class BotLootGenerator
* @param botRole what role does the bot have
* @returns Dictionary of tplIds and limit
*/
protected getItemSpawnLimitsForBotType(botRole: string): Record<string, number>
{
if (this.botHelper.isBotPmc(botRole))
{
protected getItemSpawnLimitsForBotType(botRole: string): Record<string, number> {
if (this.botHelper.isBotPmc(botRole)) {
return this.botConfig.itemSpawnLimits.pmc;
}
if (this.botConfig.itemSpawnLimits[botRole.toLowerCase()])
{
if (this.botConfig.itemSpawnLimits[botRole.toLowerCase()]) {
return this.botConfig.itemSpawnLimits[botRole.toLowerCase()];
}
@ -843,16 +782,13 @@ export class BotLootGenerator
* @param spawnLimits Limits to check for item
* @returns id as string, otherwise undefined
*/
protected getMatchingIdFromSpawnLimits(itemTemplate: ITemplateItem, spawnLimits: Record<string, number>): string
{
if (itemTemplate._id in spawnLimits)
{
protected getMatchingIdFromSpawnLimits(itemTemplate: ITemplateItem, spawnLimits: Record<string, number>): string {
if (itemTemplate._id in spawnLimits) {
return itemTemplate._id;
}
// tplId not found in spawnLimits, check if parentId is
if (itemTemplate._parent in spawnLimits)
{
if (itemTemplate._parent in spawnLimits) {
return itemTemplate._parent;
}

View File

@ -1,4 +1,3 @@
import { inject, injectAll, injectable } from "tsyringe";
import { BotEquipmentModGenerator } from "@spt/generators/BotEquipmentModGenerator";
import { IInventoryMagGen } from "@spt/generators/weapongen/IInventoryMagGen";
import { InventoryMagGen } from "@spt/generators/weapongen/InventoryMagGen";
@ -24,13 +23,13 @@ import { BotWeaponModLimitService } from "@spt/services/BotWeaponModLimitService
import { DatabaseService } from "@spt/services/DatabaseService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { RepairService } from "@spt/services/RepairService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HashUtil } from "@spt/utils/HashUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectAll, injectable } from "tsyringe";
@injectable()
export class BotWeaponGenerator
{
export class BotWeaponGenerator {
protected readonly modMagazineSlotId = "mod_magazine";
protected botConfig: IBotConfig;
protected pmcConfig: IPmcConfig;
@ -52,8 +51,7 @@ export class BotWeaponGenerator
@inject("RepairService") protected repairService: RepairService,
@injectAll("InventoryMagGen") protected inventoryMagGenComponents: IInventoryMagGen[],
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
) {
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
this.repairConfig = this.configServer.getConfig(ConfigTypes.REPAIR);
@ -79,8 +77,7 @@ export class BotWeaponGenerator
botRole: string,
isPmc: boolean,
botLevel: number,
): GenerateWeaponResult
{
): GenerateWeaponResult {
const weaponTpl = this.pickWeightedWeaponTplFromPool(equipmentSlot, botTemplateInventory);
return this.generateWeaponByTpl(
sessionId,
@ -101,8 +98,7 @@ export class BotWeaponGenerator
* @param botTemplateInventory e.g. assault.json
* @returns weapon tpl
*/
public pickWeightedWeaponTplFromPool(equipmentSlot: string, botTemplateInventory: Inventory): string
{
public pickWeightedWeaponTplFromPool(equipmentSlot: string, botTemplateInventory: Inventory): string {
const weaponPool = botTemplateInventory.equipment[equipmentSlot];
return this.weightedRandomHelper.getWeightedValue<string>(weaponPool);
}
@ -128,13 +124,11 @@ export class BotWeaponGenerator
botRole: string,
isPmc: boolean,
botLevel: number,
): GenerateWeaponResult
{
): GenerateWeaponResult {
const modPool = botTemplateInventory.mods;
const weaponItemTemplate = this.itemHelper.getItem(weaponTpl)[1];
if (!weaponItemTemplate)
{
if (!weaponItemTemplate) {
this.logger.error(this.localisationService.getText("bot-missing_item_template", weaponTpl));
this.logger.error(`WeaponSlot -> ${equipmentSlot}`);
@ -142,8 +136,7 @@ export class BotWeaponGenerator
}
// Find ammo to use when filling magazines/chamber
if (!botTemplateInventory.Ammo)
{
if (!botTemplateInventory.Ammo) {
this.logger.error(this.localisationService.getText("bot-no_ammo_found_in_bot_json", botRole));
throw new Error(this.localisationService.getText("bot-generation_failed"));
@ -160,15 +153,13 @@ export class BotWeaponGenerator
);
// Chance to add randomised weapon enhancement
if (isPmc && this.randomUtil.getChance100(this.pmcConfig.weaponHasEnhancementChancePercent))
{
if (isPmc && this.randomUtil.getChance100(this.pmcConfig.weaponHasEnhancementChancePercent)) {
const weaponConfig = this.repairConfig.repairKit.weapon;
this.repairService.addBuff(weaponConfig, weaponWithModsArray[0]);
}
// Add mods to weapon base
if (Object.keys(modPool).includes(weaponTpl))
{
if (Object.keys(modPool).includes(weaponTpl)) {
const botEquipmentRole = this.botGeneratorHelper.getBotEquipmentRole(botRole);
const modLimits = this.botWeaponModLimitService.getWeaponModLimits(botEquipmentRole);
@ -190,8 +181,7 @@ export class BotWeaponGenerator
}
// Use weapon preset from globals.json if weapon isnt valid
if (!this.isWeaponValid(weaponWithModsArray, botRole))
{
if (!this.isWeaponValid(weaponWithModsArray, botRole)) {
// Weapon is bad, fall back to weapons preset
weaponWithModsArray = this.getPresetWeaponMods(
weaponTpl,
@ -203,17 +193,15 @@ export class BotWeaponGenerator
}
// Fill existing magazines to full and sync ammo type
for (const magazine of weaponWithModsArray.filter((item) => item.slotId === this.modMagazineSlotId))
{
for (const magazine of weaponWithModsArray.filter((item) => item.slotId === this.modMagazineSlotId)) {
this.fillExistingMagazines(weaponWithModsArray, magazine, ammoTpl);
}
// Add cartridge(s) to gun chamber(s)
if (
weaponItemTemplate._props.Chambers?.length > 0
&& weaponItemTemplate._props.Chambers[0]?._props?.filters[0]?.Filter?.includes(ammoTpl)
)
{
weaponItemTemplate._props.Chambers?.length > 0 &&
weaponItemTemplate._props.Chambers[0]?._props?.filters[0]?.Filter?.includes(ammoTpl)
) {
// Guns have variety of possible Chamber ids, patron_in_weapon/patron_in_weapon_000/patron_in_weapon_001
const chamberSlotNames = weaponItemTemplate._props.Chambers.map((x) => x._name);
this.addCartridgeToChamber(weaponWithModsArray, ammoTpl, chamberSlotNames);
@ -222,8 +210,7 @@ export class BotWeaponGenerator
// Fill UBGL if found
const ubglMod = weaponWithModsArray.find((x) => x.slotId === "mod_launcher");
let ubglAmmoTpl: string = undefined;
if (ubglMod)
{
if (ubglMod) {
const ubglTemplate = this.itemHelper.getItem(ubglMod._tpl)[1];
ubglAmmoTpl = this.getWeightedCompatibleAmmo(botTemplateInventory.Ammo, ubglTemplate);
this.fillUbgl(weaponWithModsArray, ubglMod, ubglAmmoTpl);
@ -245,13 +232,10 @@ export class BotWeaponGenerator
* @param ammoTpl Cartridge to add to weapon
* @param chamberSlotIds name of slots to create or add ammo to
*/
protected addCartridgeToChamber(weaponWithModsArray: Item[], ammoTpl: string, chamberSlotIds: string[]): void
{
for (const slotId of chamberSlotIds)
{
protected addCartridgeToChamber(weaponWithModsArray: Item[], ammoTpl: string, chamberSlotIds: string[]): void {
for (const slotId of chamberSlotIds) {
const existingItemWithSlot = weaponWithModsArray.find((x) => x.slotId === slotId);
if (!existingItemWithSlot)
{
if (!existingItemWithSlot) {
// Not found, add new slot to weapon
weaponWithModsArray.push({
_id: this.hashUtil.generate(),
@ -260,9 +244,7 @@ export class BotWeaponGenerator
slotId: slotId,
upd: { StackObjectsCount: 1 },
});
}
else
{
} else {
// Already exists, update values
existingItemWithSlot._tpl = ammoTpl;
existingItemWithSlot.upd = { StackObjectsCount: 1 };
@ -286,8 +268,7 @@ export class BotWeaponGenerator
equipmentSlot: string,
weaponItemTemplate: ITemplateItem,
botRole: string,
): Item[]
{
): Item[] {
return [
{
_id: this.hashUtil.generate(),
@ -312,8 +293,7 @@ export class BotWeaponGenerator
weaponParentId: string,
itemTemplate: ITemplateItem,
botRole: string,
): Item[]
{
): Item[] {
// Invalid weapon generated, fallback to preset
this.logger.warning(
this.localisationService.getText(
@ -325,17 +305,14 @@ export class BotWeaponGenerator
// TODO: Right now, preset weapons trigger a lot of warnings regarding missing ammo in magazines & such
let preset: IPreset;
for (const presetObj of Object.values(this.databaseService.getGlobals().ItemPresets))
{
if (presetObj._items[0]._tpl === weaponTpl)
{
for (const presetObj of Object.values(this.databaseService.getGlobals().ItemPresets)) {
if (presetObj._items[0]._tpl === weaponTpl) {
preset = this.cloner.clone(presetObj);
break;
}
}
if (preset)
{
if (preset) {
const parentItem = preset._items[0];
preset._items[0] = {
...parentItem,
@ -346,9 +323,7 @@ export class BotWeaponGenerator
},
};
weaponMods.push(...preset._items);
}
else
{
} else {
throw new Error(this.localisationService.getText("bot-missing_weapon_preset", weaponTpl));
}
@ -361,25 +336,20 @@ export class BotWeaponGenerator
* @param botRole role of bot weapon is for
* @returns true if valid
*/
protected isWeaponValid(weaponItemArray: Item[], botRole: string): boolean
{
for (const mod of weaponItemArray)
{
protected isWeaponValid(weaponItemArray: Item[], botRole: string): boolean {
for (const mod of weaponItemArray) {
const modTemplate = this.itemHelper.getItem(mod._tpl)[1];
if (!modTemplate._props.Slots?.length)
{
if (!modTemplate._props.Slots?.length) {
continue;
}
// Iterate over required slots in db item, check mod exists for that slot
for (const modSlotTemplate of modTemplate._props.Slots.filter((slot) => slot._required))
{
for (const modSlotTemplate of modTemplate._props.Slots.filter((slot) => slot._required)) {
const slotName = modSlotTemplate._name;
const hasWeaponSlotItem = weaponItemArray.some(
(weaponItem) => weaponItem.parentId === mod._id && weaponItem.slotId === slotName,
);
if (!hasWeaponSlotItem)
{
if (!hasWeaponSlotItem) {
this.logger.warning(
this.localisationService.getText("bot-weapons_required_slot_missing_item", {
modSlot: modSlotTemplate._name,
@ -410,23 +380,20 @@ export class BotWeaponGenerator
magWeights: GenerationData,
inventory: PmcInventory,
botRole: string,
): void
{
): void {
const weaponAndMods = generatedWeaponResult.weapon;
const weaponTemplate = generatedWeaponResult.weaponTemplate;
const magazineTpl = this.getMagazineTplFromWeaponTemplate(weaponAndMods, weaponTemplate, botRole);
const magTemplate = this.itemHelper.getItem(magazineTpl)[1];
if (!magTemplate)
{
if (!magTemplate) {
this.logger.error(this.localisationService.getText("bot-unable_to_find_magazine_item", magazineTpl));
return;
}
const ammoTemplate = this.itemHelper.getItem(generatedWeaponResult.chosenAmmoTpl)[1];
if (!ammoTemplate)
{
if (!ammoTemplate) {
this.logger.error(
this.localisationService.getText("bot-unable_to_find_ammo_item", generatedWeaponResult.chosenAmmoTpl),
);
@ -435,8 +402,7 @@ export class BotWeaponGenerator
}
// Has an UBGL
if (generatedWeaponResult.chosenUbglAmmoTpl)
{
if (generatedWeaponResult.chosenUbglAmmoTpl) {
this.addUbglGrenadesToBotInventory(weaponAndMods, generatedWeaponResult, inventory);
}
@ -470,8 +436,7 @@ export class BotWeaponGenerator
weaponMods: Item[],
generatedWeaponResult: GenerateWeaponResult,
inventory: PmcInventory,
): void
{
): void {
// Find ubgl mod item + get details of it from db
const ubglMod = weaponMods.find((x) => x.slotId === "mod_launcher");
const ubglDbTemplate = this.itemHelper.getItem(ubglMod._tpl)[1];
@ -513,10 +478,8 @@ export class BotWeaponGenerator
ammoTpl: string,
stackSize: number,
inventory: PmcInventory,
): void
{
for (let i = 0; i < stackCount; i++)
{
): void {
for (let i = 0; i < stackCount; i++) {
const id = this.hashUtil.generate();
this.botGeneratorHelper.addItemWithChildrenToEquipmentSlot(
[EquipmentSlots.SECURED_CONTAINER],
@ -539,21 +502,17 @@ export class BotWeaponGenerator
weaponMods: Item[],
weaponTemplate: ITemplateItem,
botRole: string,
): string
{
): string {
const magazine = weaponMods.find((m) => m.slotId === this.modMagazineSlotId);
if (!magazine)
{
if (!magazine) {
// Edge case - magazineless chamber loaded weapons dont have magazines, e.g. mp18
// return default mag tpl
if (weaponTemplate._props.ReloadMode === "OnlyBarrel")
{
if (weaponTemplate._props.ReloadMode === "OnlyBarrel") {
return this.botWeaponGeneratorHelper.getWeaponsDefaultMagazineTpl(weaponTemplate);
}
// log error if no magazine AND not a chamber loaded weapon (e.g. shotgun revolver)
if (!weaponTemplate._props.isChamberLoad)
{
if (!weaponTemplate._props.isChamberLoad) {
// Shouldn't happen
this.logger.warning(
this.localisationService.getText("bot-weapon_missing_magazine_or_chamber", {
@ -583,13 +542,11 @@ export class BotWeaponGenerator
protected getWeightedCompatibleAmmo(
ammo: Record<string, Record<string, number>>,
weaponTemplate: ITemplateItem,
): string
{
): string {
const desiredCaliber = this.getWeaponCaliber(weaponTemplate);
const compatibleCartridges = this.cloner.clone(ammo[desiredCaliber]);
if (!compatibleCartridges || compatibleCartridges?.length === 0)
{
if (!compatibleCartridges || compatibleCartridges?.length === 0) {
this.logger.debug(
this.localisationService.getText("bot-no_caliber_data_for_weapon_falling_back_to_default", {
weaponId: weaponTemplate._id,
@ -603,19 +560,16 @@ export class BotWeaponGenerator
}
let chosenAmmoTpl: string;
while (!chosenAmmoTpl)
{
while (!chosenAmmoTpl) {
const possibleAmmo = this.weightedRandomHelper.getWeightedValue<string>(compatibleCartridges);
// Weapon has chamber but does not support cartridge
if (
weaponTemplate._props.Chambers[0]
&& !weaponTemplate._props.Chambers[0]._props.filters[0].Filter.includes(possibleAmmo)
)
{
weaponTemplate._props.Chambers[0] &&
!weaponTemplate._props.Chambers[0]._props.filters[0].Filter.includes(possibleAmmo)
) {
// Ran out of possible choices, use default ammo
if (Object.keys(compatibleCartridges).length === 0)
{
if (Object.keys(compatibleCartridges).length === 0) {
this.logger.debug(
this.localisationService.getText("bot-incompatible_ammo_for_weapon_falling_back_to_default", {
chosenAmmo: chosenAmmoTpl,
@ -632,9 +586,7 @@ export class BotWeaponGenerator
// Not compatible, remove item from possible list and try again
delete compatibleCartridges[possibleAmmo];
}
else
{
} else {
// Compatible ammo found
chosenAmmoTpl = possibleAmmo;
break;
@ -649,28 +601,23 @@ export class BotWeaponGenerator
* @param weaponTemplate Weapon to look up caliber of
* @returns caliber as string
*/
protected getWeaponCaliber(weaponTemplate: ITemplateItem): string
{
if (weaponTemplate._props.Caliber)
{
protected getWeaponCaliber(weaponTemplate: ITemplateItem): string {
if (weaponTemplate._props.Caliber) {
return weaponTemplate._props.Caliber;
}
if (weaponTemplate._props.ammoCaliber)
{
if (weaponTemplate._props.ammoCaliber) {
// 9x18pmm has a typo, should be Caliber9x18PM
return weaponTemplate._props.ammoCaliber === "Caliber9x18PMM"
? "Caliber9x18PM"
: weaponTemplate._props.ammoCaliber;
}
if (weaponTemplate._props.LinkedWeapon)
{
if (weaponTemplate._props.LinkedWeapon) {
const ammoInChamber = this.itemHelper.getItem(
weaponTemplate._props.Chambers[0]._props.filters[0].Filter[0],
);
if (!ammoInChamber[0])
{
if (!ammoInChamber[0]) {
return;
}
@ -684,11 +631,9 @@ export class BotWeaponGenerator
* @param magazine Magazine item
* @param cartridgeTpl Cartridge to insert into magazine
*/
protected fillExistingMagazines(weaponMods: Item[], magazine: Item, cartridgeTpl: string): void
{
protected fillExistingMagazines(weaponMods: Item[], magazine: Item, cartridgeTpl: string): void {
const magazineTemplate = this.itemHelper.getItem(magazine._tpl)[1];
if (!magazineTemplate)
{
if (!magazineTemplate) {
this.logger.error(this.localisationService.getText("bot-unable_to_find_magazine_item", magazine._tpl));
return;
@ -699,12 +644,9 @@ export class BotWeaponGenerator
// the revolver shotgun uses a magazine with chambers, not cartridges ("camora_xxx")
// Exchange of the camora ammo is not necessary we could also just check for stackSize > 0 here
// and remove the else
if (this.botWeaponGeneratorHelper.magazineIsCylinderRelated(parentItem._name))
{
if (this.botWeaponGeneratorHelper.magazineIsCylinderRelated(parentItem._name)) {
this.fillCamorasWithAmmo(weaponMods, magazine._id, cartridgeTpl);
}
else
{
} else {
this.addOrUpdateMagazinesChildWithAmmo(weaponMods, magazine, cartridgeTpl, magazineTemplate);
}
}
@ -715,8 +657,7 @@ export class BotWeaponGenerator
* @param ubglMod UBGL item
* @param ubglAmmoTpl Grenade ammo tpl
*/
protected fillUbgl(weaponMods: Item[], ubglMod: Item, ubglAmmoTpl: string): void
{
protected fillUbgl(weaponMods: Item[], ubglMod: Item, ubglAmmoTpl: string): void {
weaponMods.push({
_id: this.hashUtil.generate(),
_tpl: ubglAmmoTpl,
@ -739,13 +680,11 @@ export class BotWeaponGenerator
magazine: Item,
chosenAmmoTpl: string,
magazineTemplate: ITemplateItem,
): void
{
): void {
const magazineCartridgeChildItem = weaponWithMods.find(
(m) => m.parentId === magazine._id && m.slotId === "cartridges",
);
if (magazineCartridgeChildItem)
{
if (magazineCartridgeChildItem) {
// Delete the existing cartridge object and create fresh below
weaponWithMods.splice(weaponWithMods.indexOf(magazineCartridgeChildItem), 1);
}
@ -766,21 +705,16 @@ export class BotWeaponGenerator
* @param magazineId magazine id to find and add to
* @param ammoTpl ammo template id to hydate with
*/
protected fillCamorasWithAmmo(weaponMods: Item[], magazineId: string, ammoTpl: string): void
{
protected fillCamorasWithAmmo(weaponMods: Item[], magazineId: string, ammoTpl: string): void {
// for CylinderMagazine we exchange the ammo in the "camoras".
// This might not be necessary since we already filled the camoras with a random whitelisted and compatible ammo type,
// but I'm not sure whether this is also used elsewhere
const camoras = weaponMods.filter((x) => x.parentId === magazineId && x.slotId.startsWith("camora"));
for (const camora of camoras)
{
for (const camora of camoras) {
camora._tpl = ammoTpl;
if (camora.upd)
{
if (camora.upd) {
camora.upd.StackObjectsCount = 1;
}
else
{
} else {
camora.upd = { StackObjectsCount: 1 };
}
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { HandbookHelper } from "@spt/helpers/HandbookHelper";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { PresetHelper } from "@spt/helpers/PresetHelper";
@ -18,10 +17,10 @@ import { ItemFilterService } from "@spt/services/ItemFilterService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { SeasonalEventService } from "@spt/services/SeasonalEventService";
import { HashUtil } from "@spt/utils/HashUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class FenceBaseAssortGenerator
{
export class FenceBaseAssortGenerator {
protected traderConfig: ITraderConfig;
constructor(
@ -36,54 +35,45 @@ export class FenceBaseAssortGenerator
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("FenceService") protected fenceService: FenceService,
)
{
) {
this.traderConfig = this.configServer.getConfig(ConfigTypes.TRADER);
}
/**
* Create base fence assorts dynamically and store in memory
*/
public generateFenceBaseAssorts(): void
{
public generateFenceBaseAssorts(): void {
const blockedSeasonalItems = this.seasonalEventService.getInactiveSeasonalEventItems();
const baseFenceAssort = this.databaseService.getTrader(Traders.FENCE).assort;
for (const rootItemDb of this.itemHelper.getItems().filter((item) => this.isValidFenceItem(item)))
{
for (const rootItemDb of this.itemHelper.getItems().filter((item) => this.isValidFenceItem(item))) {
// Skip blacklisted items
if (this.itemFilterService.isItemBlacklisted(rootItemDb._id))
{
if (this.itemFilterService.isItemBlacklisted(rootItemDb._id)) {
continue;
}
// Invalid
if (!this.itemHelper.isValidItem(rootItemDb._id))
{
if (!this.itemHelper.isValidItem(rootItemDb._id)) {
continue;
}
// Item base type blacklisted
if (this.traderConfig.fence.blacklist.length > 0)
{
if (this.traderConfig.fence.blacklist.length > 0) {
if (
this.traderConfig.fence.blacklist.includes(rootItemDb._id)
|| this.itemHelper.isOfBaseclasses(rootItemDb._id, this.traderConfig.fence.blacklist)
)
{
this.traderConfig.fence.blacklist.includes(rootItemDb._id) ||
this.itemHelper.isOfBaseclasses(rootItemDb._id, this.traderConfig.fence.blacklist)
) {
continue;
}
}
// Only allow rigs with no slots (carrier rigs)
if (this.itemHelper.isOfBaseclass(rootItemDb._id, BaseClasses.VEST) && rootItemDb._props.Slots.length > 0)
{
if (this.itemHelper.isOfBaseclass(rootItemDb._id, BaseClasses.VEST) && rootItemDb._props.Slots.length > 0) {
continue;
}
// Skip seasonal event items when not in seasonal event
if (this.traderConfig.fence.blacklistSeasonalItems && blockedSeasonalItems.includes(rootItemDb._id))
{
if (this.traderConfig.fence.blacklistSeasonalItems && blockedSeasonalItems.includes(rootItemDb._id)) {
continue;
}
@ -99,27 +89,22 @@ export class FenceBaseAssortGenerator
];
// Ensure ammo is not above penetration limit value
if (this.itemHelper.isOfBaseclasses(rootItemDb._id, [BaseClasses.AMMO_BOX, BaseClasses.AMMO]))
{
if (this.isAmmoAbovePenetrationLimit(rootItemDb))
{
if (this.itemHelper.isOfBaseclasses(rootItemDb._id, [BaseClasses.AMMO_BOX, BaseClasses.AMMO])) {
if (this.isAmmoAbovePenetrationLimit(rootItemDb)) {
continue;
}
}
if (this.itemHelper.isOfBaseclass(rootItemDb._id, BaseClasses.AMMO_BOX))
{
if (this.itemHelper.isOfBaseclass(rootItemDb._id, BaseClasses.AMMO_BOX)) {
// Only add cartridges to box if box has no children
if (itemWithChildrenToAdd.length === 1)
{
if (itemWithChildrenToAdd.length === 1) {
this.itemHelper.addCartridgesToAmmoBox(itemWithChildrenToAdd, rootItemDb);
}
}
// Ensure IDs are unique
this.itemHelper.remapRootItemId(itemWithChildrenToAdd);
if (itemWithChildrenToAdd.length > 1)
{
if (itemWithChildrenToAdd.length > 1) {
this.itemHelper.reparentItemAndChildren(itemWithChildrenToAdd[0], itemWithChildrenToAdd);
itemWithChildrenToAdd[0].parentId = "hideout";
}
@ -142,11 +127,9 @@ export class FenceBaseAssortGenerator
// Add all default presets to base fence assort
const defaultPresets = Object.values(this.presetHelper.getDefaultPresets());
for (const defaultPreset of defaultPresets)
{
for (const defaultPreset of defaultPresets) {
// Skip presets we've already added
if (baseFenceAssort.items.some((item) => item.upd && item.upd.sptPresetId === defaultPreset._id))
{
if (baseFenceAssort.items.some((item) => item.upd && item.upd.sptPresetId === defaultPreset._id)) {
continue;
}
@ -154,13 +137,11 @@ export class FenceBaseAssortGenerator
const itemAndChildren: Item[] = this.itemHelper.replaceIDs(defaultPreset._items);
// Find root item and add some properties to it
for (let i = 0; i < itemAndChildren.length; i++)
{
for (let i = 0; i < itemAndChildren.length; i++) {
const mod = itemAndChildren[i];
// Build root Item info
if (!("parentId" in mod))
{
if (!("parentId" in mod)) {
mod.parentId = "hideout";
mod.slotId = "hideout";
mod.upd = {
@ -196,12 +177,12 @@ export class FenceBaseAssortGenerator
* @param rootItemDb Ammo box or ammo item from items.db
* @returns True if penetration value is above limit set in config
*/
protected isAmmoAbovePenetrationLimit(rootItemDb: ITemplateItem): boolean
{
protected isAmmoAbovePenetrationLimit(rootItemDb: ITemplateItem): boolean {
const ammoPenetrationPower = this.getAmmoPenetrationPower(rootItemDb);
if (ammoPenetrationPower === undefined)
{
this.logger.warning(this.localisationService.getText("fence-unable_to_get_ammo_penetration_value", rootItemDb._id));
if (ammoPenetrationPower === undefined) {
this.logger.warning(
this.localisationService.getText("fence-unable_to_get_ammo_penetration_value", rootItemDb._id),
);
return false;
}
@ -214,17 +195,14 @@ export class FenceBaseAssortGenerator
* @param rootItemDb Ammo box or ammo item from items.db
* @returns Penetration power of passed in item, undefined if it doesnt have a power
*/
protected getAmmoPenetrationPower(rootItemDb: ITemplateItem): number | undefined
{
if (this.itemHelper.isOfBaseclass(rootItemDb._id, BaseClasses.AMMO_BOX))
{
protected getAmmoPenetrationPower(rootItemDb: ITemplateItem): number | undefined {
if (this.itemHelper.isOfBaseclass(rootItemDb._id, BaseClasses.AMMO_BOX)) {
// Get the cartridge tpl found inside ammo box
const cartridgeTplInBox = rootItemDb._props.StackSlots[0]._props.filters[0].Filter[0];
// Look up cartridge tpl in db
const ammoItemDb = this.itemHelper.getItem(cartridgeTplInBox);
if (!ammoItemDb[0])
{
if (!ammoItemDb[0]) {
this.logger.warning(this.localisationService.getText("fence-ammo_not_found_in_db", cartridgeTplInBox));
return undefined;
@ -234,8 +212,7 @@ export class FenceBaseAssortGenerator
}
// Plain old ammo, get its pen property
if (this.itemHelper.isOfBaseclass(rootItemDb._id, BaseClasses.AMMO))
{
if (this.itemHelper.isOfBaseclass(rootItemDb._id, BaseClasses.AMMO)) {
return rootItemDb._props.PenetrationPower;
}
@ -248,26 +225,21 @@ export class FenceBaseAssortGenerator
* @param armor Armor item array to add mods into
* @param itemDbDetails Armor items db template
*/
protected addChildrenToArmorModSlots(armor: Item[], itemDbDetails: ITemplateItem): void
{
protected addChildrenToArmorModSlots(armor: Item[], itemDbDetails: ITemplateItem): void {
// Armor has no mods, make no additions
const hasMods = itemDbDetails._props.Slots.length > 0;
if (!hasMods)
{
if (!hasMods) {
return;
}
// Check for and add required soft inserts to armors
const requiredSlots = itemDbDetails._props.Slots.filter((slot) => slot._required);
const hasRequiredSlots = requiredSlots.length > 0;
if (hasRequiredSlots)
{
for (const requiredSlot of requiredSlots)
{
if (hasRequiredSlots) {
for (const requiredSlot of requiredSlots) {
const modItemDbDetails = this.itemHelper.getItem(requiredSlot._props.filters[0].Plate)[1];
const plateTpl = requiredSlot._props.filters[0].Plate; // `Plate` property appears to be the 'default' item for slot
if (plateTpl === "")
{
if (plateTpl === "") {
// Some bsg plate properties are empty, skip mod
continue;
}
@ -293,13 +265,10 @@ export class FenceBaseAssortGenerator
const plateSlots = itemDbDetails._props.Slots.filter((slot) =>
this.itemHelper.isRemovablePlateSlot(slot._name),
);
if (plateSlots.length > 0)
{
for (const plateSlot of plateSlots)
{
if (plateSlots.length > 0) {
for (const plateSlot of plateSlots) {
const plateTpl = plateSlot._props.filters[0].Plate;
if (!plateTpl)
{
if (!plateTpl) {
// Bsg data lacks a default plate, skip adding mod
continue;
}
@ -325,10 +294,8 @@ export class FenceBaseAssortGenerator
* @param item Item to check
* @returns true if valid fence item
*/
protected isValidFenceItem(item: ITemplateItem): boolean
{
if (item._type === "Item")
{
protected isValidFenceItem(item: ITemplateItem): boolean {
if (item._type === "Item") {
return true;
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { ContainerHelper } from "@spt/helpers/ContainerHelper";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { PresetHelper } from "@spt/helpers/PresetHelper";
@ -22,29 +21,27 @@ import { DatabaseService } from "@spt/services/DatabaseService";
import { ItemFilterService } from "@spt/services/ItemFilterService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { SeasonalEventService } from "@spt/services/SeasonalEventService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { MathUtil } from "@spt/utils/MathUtil";
import { ObjectId } from "@spt/utils/ObjectId";
import { ProbabilityObject, ProbabilityObjectArray, RandomUtil } from "@spt/utils/RandomUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
export interface IContainerItem
{
items: Item[]
width: number
height: number
export interface IContainerItem {
items: Item[];
width: number;
height: number;
}
export interface IContainerGroupCount
{
export interface IContainerGroupCount {
/** Containers this group has + probabilty to spawn */
containerIdsWithProbability: Record<string, number>
containerIdsWithProbability: Record<string, number>;
/** How many containers the map should spawn with this group id */
chosenCount: number
chosenCount: number;
}
@injectable()
export class LocationLootGenerator
{
export class LocationLootGenerator {
protected locationConfig: ILocationConfig;
constructor(
@ -61,8 +58,7 @@ export class LocationLootGenerator
@inject("ItemFilterService") protected itemFilterService: ItemFilterService,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
) {
this.locationConfig = this.configServer.getConfig(ConfigTypes.LOCATION);
}
@ -75,8 +71,7 @@ export class LocationLootGenerator
public generateStaticContainers(
locationBase: ILocationBase,
staticAmmoDist: Record<string, IStaticAmmoDetails[]>,
): SpawnpointTemplate[]
{
): SpawnpointTemplate[] {
let staticLootItemCount = 0;
const result: SpawnpointTemplate[] = [];
const locationId = locationBase.Id.toLowerCase();
@ -84,9 +79,10 @@ export class LocationLootGenerator
const mapData = this.databaseService.getLocation(locationId);
const staticWeaponsOnMapClone = this.cloner.clone(mapData.staticContainers.staticWeapons);
if (!staticWeaponsOnMapClone)
{
this.logger.error(this.localisationService.getText("location-unable_to_find_static_weapon_for_map", locationBase.Name));
if (!staticWeaponsOnMapClone) {
this.logger.error(
this.localisationService.getText("location-unable_to_find_static_weapon_for_map", locationBase.Name),
);
}
// Add mounted weapons to output loot
@ -94,18 +90,23 @@ export class LocationLootGenerator
const allStaticContainersOnMapClone = this.cloner.clone(mapData.staticContainers.staticContainers);
if (!allStaticContainersOnMapClone)
{
this.logger.error(this.localisationService.getText("location-unable_to_find_static_container_for_map", locationBase.Name));
if (!allStaticContainersOnMapClone) {
this.logger.error(
this.localisationService.getText("location-unable_to_find_static_container_for_map", locationBase.Name),
);
}
const staticRandomisableContainersOnMap = this.getRandomisableContainersOnMap(allStaticContainersOnMapClone);
// Containers that MUST be added to map (quest containers etc)
const staticForcedOnMapClone = this.cloner.clone(mapData.staticContainers.staticForced);
if (!staticForcedOnMapClone)
{
this.logger.error(this.localisationService.getText("location-unable_to_find_forced_static_data_for_map", locationBase.Name));
if (!staticForcedOnMapClone) {
this.logger.error(
this.localisationService.getText(
"location-unable_to_find_forced_static_data_for_map",
locationBase.Name,
),
);
}
// Keep track of static loot count
@ -117,8 +118,7 @@ export class LocationLootGenerator
staticContainerCount += guaranteedContainers.length;
// Add loot to guaranteed containers and add to result
for (const container of guaranteedContainers)
{
for (const container of guaranteedContainers) {
const containerWithLoot = this.addLootToContainer(
container,
staticForcedOnMapClone,
@ -136,16 +136,14 @@ export class LocationLootGenerator
// Randomisation is turned off globally or just turned off for this map
if (
!(
this.locationConfig.containerRandomisationSettings.enabled
&& this.locationConfig.containerRandomisationSettings.maps[locationId]
this.locationConfig.containerRandomisationSettings.enabled &&
this.locationConfig.containerRandomisationSettings.maps[locationId]
)
)
{
) {
this.logger.debug(
`Container randomisation disabled, Adding ${staticRandomisableContainersOnMap.length} containers to ${locationBase.Name}`,
);
for (const container of staticRandomisableContainersOnMap)
{
for (const container of staticRandomisableContainersOnMap) {
const containerWithLoot = this.addLootToContainer(
container,
staticForcedOnMapClone,
@ -164,27 +162,25 @@ export class LocationLootGenerator
}
// Group containers by their groupId
if (!mapData.statics)
{
this.logger.warning(this.localisationService.getText("location-unable_to_generate_static_loot", locationId));
if (!mapData.statics) {
this.logger.warning(
this.localisationService.getText("location-unable_to_generate_static_loot", locationId),
);
return result;
}
const mapping = this.getGroupIdToContainerMappings(mapData.statics, staticRandomisableContainersOnMap);
// For each of the container groups, choose from the pool of containers, hydrate container with loot and add to result array
for (const groupId in mapping)
{
for (const groupId in mapping) {
const data = mapping[groupId];
// Count chosen was 0, skip
if (data.chosenCount === 0)
{
if (data.chosenCount === 0) {
continue;
}
if (Object.keys(data.containerIdsWithProbability).length === 0)
{
if (Object.keys(data.containerIdsWithProbability).length === 0) {
this.logger.debug(
`Group: ${groupId} has no containers with < 100% spawn chance to choose from, skipping`,
);
@ -192,15 +188,12 @@ export class LocationLootGenerator
}
// EDGE CASE: These are containers without a group and have a probability < 100%
if (groupId === "")
{
if (groupId === "") {
const containerIdsCopy = this.cloner.clone(data.containerIdsWithProbability);
// Roll each containers probability, if it passes, it gets added
data.containerIdsWithProbability = {};
for (const containerId in containerIdsCopy)
{
if (this.randomUtil.getChance100(containerIdsCopy[containerId] * 100))
{
for (const containerId in containerIdsCopy) {
if (this.randomUtil.getChance100(containerIdsCopy[containerId] * 100)) {
data.containerIdsWithProbability[containerId] = containerIdsCopy[containerId];
}
}
@ -209,22 +202,19 @@ export class LocationLootGenerator
data.chosenCount = Object.keys(data.containerIdsWithProbability).length;
// EDGE CASE: chosen container count could be 0
if (data.chosenCount === 0)
{
if (data.chosenCount === 0) {
continue;
}
}
// Pass possible containers into function to choose some
const chosenContainerIds = this.getContainersByProbabilty(groupId, data);
for (const chosenContainerId of chosenContainerIds)
{
for (const chosenContainerId of chosenContainerIds) {
// Look up container object from full list of containers on map
const containerObject = staticRandomisableContainersOnMap.find(
(staticContainer) => staticContainer.template.Id === chosenContainerId,
);
if (!containerObject)
{
if (!containerObject) {
this.logger.debug(
`Container: ${chosenContainerIds[chosenContainerId]} not found in staticRandomisableContainersOnMap, this is bad`,
);
@ -260,13 +250,12 @@ export class LocationLootGenerator
* @param staticContainers
* @returns IStaticContainerData array
*/
protected getRandomisableContainersOnMap(staticContainers: IStaticContainerData[]): IStaticContainerData[]
{
protected getRandomisableContainersOnMap(staticContainers: IStaticContainerData[]): IStaticContainerData[] {
return staticContainers.filter(
(staticContainer) =>
staticContainer.probability !== 1
&& !staticContainer.template.IsAlwaysSpawn
&& !this.locationConfig.containerRandomisationSettings.containerTypesToNotRandomise.includes(
staticContainer.probability !== 1 &&
!staticContainer.template.IsAlwaysSpawn &&
!this.locationConfig.containerRandomisationSettings.containerTypesToNotRandomise.includes(
staticContainer.template.Items[0]._tpl,
),
);
@ -277,13 +266,12 @@ export class LocationLootGenerator
* @param staticContainersOnMap
* @returns IStaticContainerData array
*/
protected getGuaranteedContainers(staticContainersOnMap: IStaticContainerData[]): IStaticContainerData[]
{
protected getGuaranteedContainers(staticContainersOnMap: IStaticContainerData[]): IStaticContainerData[] {
return staticContainersOnMap.filter(
(staticContainer) =>
staticContainer.probability === 1
|| staticContainer.template.IsAlwaysSpawn
|| this.locationConfig.containerRandomisationSettings.containerTypesToNotRandomise.includes(
staticContainer.probability === 1 ||
staticContainer.template.IsAlwaysSpawn ||
this.locationConfig.containerRandomisationSettings.containerTypesToNotRandomise.includes(
staticContainer.template.Items[0]._tpl,
),
);
@ -295,13 +283,11 @@ export class LocationLootGenerator
* @param containerData Containers and probability values for a groupId
* @returns List of chosen container Ids
*/
protected getContainersByProbabilty(groupId: string, containerData: IContainerGroupCount): string[]
{
protected getContainersByProbabilty(groupId: string, containerData: IContainerGroupCount): string[] {
const chosenContainerIds: string[] = [];
const containerIds = Object.keys(containerData.containerIdsWithProbability);
if (containerData.chosenCount > containerIds.length)
{
if (containerData.chosenCount > containerIds.length) {
this.logger.debug(
`Group: ${groupId} wants ${containerData.chosenCount} containers but pool only has ${containerIds.length}, add what's available`,
);
@ -310,8 +296,7 @@ export class LocationLootGenerator
// Create probability array with all possible container ids in this group and their relataive probability of spawning
const containerDistribution = new ProbabilityObjectArray<string>(this.mathUtil, this.cloner);
for (const x of containerIds)
{
for (const x of containerIds) {
containerDistribution.push(new ProbabilityObject(x, containerData.containerIdsWithProbability[x]));
}
@ -328,25 +313,22 @@ export class LocationLootGenerator
protected getGroupIdToContainerMappings(
staticContainerGroupData: IStaticContainer | Record<string, IContainerMinMax>,
staticContainersOnMap: IStaticContainerData[],
): Record<string, IContainerGroupCount>
{
): Record<string, IContainerGroupCount> {
// Create dictionary of all group ids and choose a count of containers the map will spawn of that group
const mapping: Record<string, IContainerGroupCount> = {};
for (const groupId in staticContainerGroupData.containersGroups)
{
for (const groupId in staticContainerGroupData.containersGroups) {
const groupData = staticContainerGroupData.containersGroups[groupId];
if (!mapping[groupId])
{
if (!mapping[groupId]) {
mapping[groupId] = {
containerIdsWithProbability: {},
chosenCount: this.randomUtil.getInt(
Math.round(
groupData.minContainers
* this.locationConfig.containerRandomisationSettings.containerGroupMinSizeMultiplier,
groupData.minContainers *
this.locationConfig.containerRandomisationSettings.containerGroupMinSizeMultiplier,
),
Math.round(
groupData.maxContainers
* this.locationConfig.containerRandomisationSettings.containerGroupMaxSizeMultiplier,
groupData.maxContainers *
this.locationConfig.containerRandomisationSettings.containerGroupMaxSizeMultiplier,
),
),
};
@ -359,18 +341,20 @@ export class LocationLootGenerator
// Iterate over all containers and add to group keyed by groupId
// Containers without a group go into a group with empty key ""
for (const container of staticContainersOnMap)
{
for (const container of staticContainersOnMap) {
const groupData = staticContainerGroupData.containers[container.template.Id];
if (!groupData)
{
this.logger.error(this.localisationService.getText("location-unable_to_find_container_in_statics_json", container.template.Id));
if (!groupData) {
this.logger.error(
this.localisationService.getText(
"location-unable_to_find_container_in_statics_json",
container.template.Id,
),
);
continue;
}
if (container.probability === 1)
{
if (container.probability === 1) {
this.logger.debug(
`Container ${container.template.Id} with group ${groupData.groupId} had 100% chance to spawn was picked as random container, skipping`,
);
@ -399,8 +383,7 @@ export class LocationLootGenerator
staticLootDist: Record<string, IStaticLootDetails>,
staticAmmoDist: Record<string, IStaticAmmoDetails[]>,
locationName: string,
): IStaticContainerData
{
): IStaticContainerData {
const containerClone = this.cloner.clone(staticContainer);
const containerTpl = containerClone.template.Items[0]._tpl;
@ -435,8 +418,7 @@ export class LocationLootGenerator
// Add forced loot to chosen item pool
const tplsToAddToContainer = tplsForced.concat(chosenTpls);
for (const tplToAdd of tplsToAddToContainer)
{
for (const tplToAdd of tplsToAddToContainer) {
const chosenItemWithChildren = this.createStaticLootItem(tplToAdd, staticAmmoDist, parentId);
const items = chosenItemWithChildren.items;
const width = chosenItemWithChildren.width;
@ -444,10 +426,8 @@ export class LocationLootGenerator
// look for open slot to put chosen item into
const result = this.containerHelper.findSlotForItem(containerMap, width, height);
if (!result.success)
{
if (failedToFitCount >= this.locationConfig.fitLootIntoContainerAttempts)
{
if (!result.success) {
if (failedToFitCount >= this.locationConfig.fitLootIntoContainerAttempts) {
// x attempts to fit an item, container is probably full, stop trying to add more
break;
}
@ -472,8 +452,7 @@ export class LocationLootGenerator
items[0].location = { x: result.x!, y: result.y!, r: rotation };
// Add loot to container before returning
for (const item of items)
{
for (const item of items) {
containerClone.template.Items.push(item);
}
}
@ -486,8 +465,7 @@ export class LocationLootGenerator
* @param containerTpl Tpl id of the container
* @returns number[][]
*/
protected getContainerMapping(containerTpl: string): number[][]
{
protected getContainerMapping(containerTpl: string): number[][] {
// Get template from db
const containerTemplate = this.itemHelper.getItem(containerTpl)[1];
@ -512,24 +490,22 @@ export class LocationLootGenerator
containerTypeId: string,
staticLootDist: Record<string, IStaticLootDetails>,
locationName: string,
): number
{
): number {
// Create probability array to calcualte the total count of lootable items inside container
const itemCountArray = new ProbabilityObjectArray<number>(this.mathUtil, this.cloner);
const countDistribution = staticLootDist[containerTypeId]?.itemcountDistribution;
if (!countDistribution)
{
this.logger.warning(this.localisationService.getText("location-unable_to_find_count_distribution_for_container",
{
if (!countDistribution) {
this.logger.warning(
this.localisationService.getText("location-unable_to_find_count_distribution_for_container", {
containerId: containerTypeId,
locationName: locationName,
}));
}),
);
return 0;
}
for (const itemCountDistribution of countDistribution)
{
for (const itemCountDistribution of countDistribution) {
// Add each count of items into array
itemCountArray.push(
new ProbabilityObject(itemCountDistribution.count, itemCountDistribution.relativeProbability),
@ -549,31 +525,28 @@ export class LocationLootGenerator
protected getPossibleLootItemsForContainer(
containerTypeId: string,
staticLootDist: Record<string, IStaticLootDetails>,
): ProbabilityObjectArray<string, number>
{
): ProbabilityObjectArray<string, number> {
const seasonalEventActive = this.seasonalEventService.seasonalEventEnabled();
const seasonalItemTplBlacklist = this.seasonalEventService.getInactiveSeasonalEventItems();
const itemDistribution = new ProbabilityObjectArray<string, number>(this.mathUtil, this.cloner);
const itemContainerDistribution = staticLootDist[containerTypeId]?.itemDistribution;
if (!itemContainerDistribution)
{
this.logger.warning(this.localisationService.getText("location-missing_item_distribution_data", containerTypeId));
if (!itemContainerDistribution) {
this.logger.warning(
this.localisationService.getText("location-missing_item_distribution_data", containerTypeId),
);
return itemDistribution;
}
for (const icd of itemContainerDistribution)
{
if (!seasonalEventActive && seasonalItemTplBlacklist.includes(icd.tpl))
{
for (const icd of itemContainerDistribution) {
if (!seasonalEventActive && seasonalItemTplBlacklist.includes(icd.tpl)) {
// Skip seasonal event items if they're not enabled
continue;
}
// Ensure no blacklisted lootable items are in pool
if (this.itemFilterService.isLootableItemBlacklisted(icd.tpl))
{
if (this.itemFilterService.isLootableItemBlacklisted(icd.tpl)) {
continue;
}
@ -583,13 +556,11 @@ export class LocationLootGenerator
return itemDistribution;
}
protected getLooseLootMultiplerForLocation(location: string): number
{
protected getLooseLootMultiplerForLocation(location: string): number {
return this.locationConfig.looseLootMultiplier[location];
}
protected getStaticLootMultiplerForLocation(location: string): number
{
protected getStaticLootMultiplerForLocation(location: string): number {
return this.locationConfig.staticLootMultiplier[location];
}
@ -604,8 +575,7 @@ export class LocationLootGenerator
dynamicLootDist: ILooseLoot,
staticAmmoDist: Record<string, IStaticAmmoDetails[]>,
locationName: string,
): SpawnpointTemplate[]
{
): SpawnpointTemplate[] {
const loot: SpawnpointTemplate[] = [];
const dynamicForcedSpawnPoints: SpawnpointsForced[] = [];
@ -620,8 +590,8 @@ export class LocationLootGenerator
// Draw from random distribution
const desiredSpawnpointCount = Math.round(
this.getLooseLootMultiplerForLocation(locationName)
* this.randomUtil.getNormallyDistributedRandomNumber(
this.getLooseLootMultiplerForLocation(locationName) *
this.randomUtil.getNormallyDistributedRandomNumber(
dynamicLootDist.spawnpointCount.mean,
dynamicLootDist.spawnpointCount.std,
),
@ -633,24 +603,20 @@ export class LocationLootGenerator
const blacklistedSpawnpoints = this.locationConfig.looseLootBlacklist[locationName];
const spawnpointArray = new ProbabilityObjectArray<string, Spawnpoint>(this.mathUtil, this.cloner);
for (const spawnpoint of allDynamicSpawnpoints)
{
for (const spawnpoint of allDynamicSpawnpoints) {
// Point is blacklsited, skip
if (blacklistedSpawnpoints?.includes(spawnpoint.template.Id))
{
if (blacklistedSpawnpoints?.includes(spawnpoint.template.Id)) {
this.logger.debug(`Ignoring loose loot location: ${spawnpoint.template.Id}`);
continue;
}
// We've handled IsAlwaysSpawn above, so skip them
if (spawnpoint.template.IsAlwaysSpawn)
{
if (spawnpoint.template.IsAlwaysSpawn) {
continue;
}
// 100%, add it to guaranteed
if (spawnpoint.probability === 1)
{
if (spawnpoint.probability === 1) {
guaranteedLoosePoints.push(spawnpoint);
continue;
}
@ -664,11 +630,9 @@ export class LocationLootGenerator
const randomSpawnpointCount = desiredSpawnpointCount - chosenSpawnpoints.length;
// Only draw random spawn points if needed
if (randomSpawnpointCount > 0 && spawnpointArray.length > 0)
{
if (randomSpawnpointCount > 0 && spawnpointArray.length > 0) {
// Add randomly chosen spawn points
for (const si of spawnpointArray.draw(randomSpawnpointCount, false))
{
for (const si of spawnpointArray.draw(randomSpawnpointCount, false)) {
chosenSpawnpoints.push(spawnpointArray.data(si)!);
}
}
@ -680,8 +644,7 @@ export class LocationLootGenerator
// Do we have enough items in pool to fulfill requirement
const tooManySpawnPointsRequested = desiredSpawnpointCount - chosenSpawnpoints.length > 0;
if (tooManySpawnPointsRequested)
{
if (tooManySpawnPointsRequested) {
this.logger.debug(
this.localisationService.getText("location-spawn_point_count_requested_vs_found", {
requested: desiredSpawnpointCount + guaranteedLoosePoints.length,
@ -694,11 +657,9 @@ export class LocationLootGenerator
// Iterate over spawnpoints
const seasonalEventActive = this.seasonalEventService.seasonalEventEnabled();
const seasonalItemTplBlacklist = this.seasonalEventService.getInactiveSeasonalEventItems();
for (const spawnPoint of chosenSpawnpoints)
{
for (const spawnPoint of chosenSpawnpoints) {
// Spawnpoint is invalid, skip it
if (!spawnPoint.template)
{
if (!spawnPoint.template) {
this.logger.warning(
this.localisationService.getText("location-missing_dynamic_template", spawnPoint.locationId),
);
@ -707,19 +668,19 @@ export class LocationLootGenerator
}
// Ensure no blacklisted lootable items are in pool
spawnPoint.template.Items = spawnPoint.template.Items
.filter((item) => !this.itemFilterService.isLootableItemBlacklisted(item._tpl));
spawnPoint.template.Items = spawnPoint.template.Items.filter(
(item) => !this.itemFilterService.isLootableItemBlacklisted(item._tpl),
);
// Ensure no seasonal items are in pool if not in-season
if (!seasonalEventActive)
{
spawnPoint.template.Items = spawnPoint.template.Items
.filter((item) => !seasonalItemTplBlacklist.includes(item._tpl));
if (!seasonalEventActive) {
spawnPoint.template.Items = spawnPoint.template.Items.filter(
(item) => !seasonalItemTplBlacklist.includes(item._tpl),
);
}
// Spawn point has no items after filtering, skip
if (!spawnPoint.template.Items || spawnPoint.template.Items.length === 0)
{
if (!spawnPoint.template.Items || spawnPoint.template.Items.length === 0) {
this.logger.warning(
this.localisationService.getText("location-spawnpoint_missing_items", spawnPoint.template.Id),
);
@ -732,19 +693,18 @@ export class LocationLootGenerator
// Construct container to hold above filtered items, letting us pick an item for the spot
const itemArray = new ProbabilityObjectArray<string>(this.mathUtil, this.cloner);
for (const itemDist of spawnPoint.itemDistribution)
{
if (!validItemIds.includes(itemDist.composedKey.key))
{
for (const itemDist of spawnPoint.itemDistribution) {
if (!validItemIds.includes(itemDist.composedKey.key)) {
continue;
}
itemArray.push(new ProbabilityObject(itemDist.composedKey.key, itemDist.relativeProbability));
}
if (itemArray.length === 0)
{
this.logger.warning(this.localisationService.getText("location-loot_pool_is_empty_skipping", spawnPoint.template.Id));
if (itemArray.length === 0) {
this.logger.warning(
this.localisationService.getText("location-loot_pool_is_empty_skipping", spawnPoint.template.Id),
);
continue;
}
@ -775,20 +735,16 @@ export class LocationLootGenerator
lootLocationTemplates: SpawnpointTemplate[],
forcedSpawnPoints: SpawnpointsForced[],
locationName: string,
): void
{
): void {
const lootToForceSingleAmountOnMap = this.locationConfig.forcedLootSingleSpawnById[locationName];
if (lootToForceSingleAmountOnMap)
{
if (lootToForceSingleAmountOnMap) {
// Process loot items defined as requiring only 1 spawn position as they appear in multiple positions on the map
for (const itemTpl of lootToForceSingleAmountOnMap)
{
for (const itemTpl of lootToForceSingleAmountOnMap) {
// Get all spawn positions for item tpl in forced loot array
const items = forcedSpawnPoints.filter(
(forcedSpawnPoint) => forcedSpawnPoint.template.Items[0]._tpl === itemTpl,
);
if (!items || items.length === 0)
{
if (!items || items.length === 0) {
this.logger.debug(
`Unable to adjust loot item ${itemTpl} as it does not exist inside ${locationName} forced loot.`,
);
@ -800,20 +756,19 @@ export class LocationLootGenerator
this.mathUtil,
this.cloner,
);
for (const si of items)
{
for (const si of items) {
// use locationId as template.Id is the same across all items
spawnpointArray.push(new ProbabilityObject(si.locationId, si.probability, si));
}
// Choose 1 out of all found spawn positions for spawn id and add to loot array
for (const spawnPointLocationId of spawnpointArray.draw(1, false))
{
for (const spawnPointLocationId of spawnpointArray.draw(1, false)) {
const itemToAdd = items.find((item) => item.locationId === spawnPointLocationId);
const lootItem = itemToAdd?.template;
if (!lootItem)
{
this.logger.warning(`Item with spawn point id ${spawnPointLocationId} could not be found, skipping`);
if (!lootItem) {
this.logger.warning(
`Item with spawn point id ${spawnPointLocationId} could not be found, skipping`,
);
continue;
}
lootItem.Root = this.objectId.generate();
@ -827,19 +782,16 @@ export class LocationLootGenerator
const seasonalItemTplBlacklist = this.seasonalEventService.getInactiveSeasonalEventItems();
// Add remaining forced loot to array
for (const forcedLootLocation of forcedSpawnPoints)
{
for (const forcedLootLocation of forcedSpawnPoints) {
const firstLootItemTpl = forcedLootLocation.template.Items[0]._tpl;
// Skip spawn positions processed already
if (lootToForceSingleAmountOnMap?.includes(firstLootItemTpl))
{
if (lootToForceSingleAmountOnMap?.includes(firstLootItemTpl)) {
continue;
}
// Skip adding seasonal items when seasonal event is not active
if (!seasonalEventActive && seasonalItemTplBlacklist.includes(firstLootItemTpl))
{
if (!seasonalEventActive && seasonalItemTplBlacklist.includes(firstLootItemTpl)) {
continue;
}
@ -853,12 +805,9 @@ export class LocationLootGenerator
const existingLocation = lootLocationTemplates.some(
(spawnPoint) => spawnPoint.Id === locationTemplateToAdd.Id,
);
if (!existingLocation)
{
if (!existingLocation) {
lootLocationTemplates.push(locationTemplateToAdd);
}
else
{
} else {
this.logger.debug(
`Attempted to add a forced loot location with Id: ${locationTemplateToAdd.Id} to map ${locationName} that already has that id in use, skipping`,
);
@ -877,12 +826,10 @@ export class LocationLootGenerator
chosenComposedKey: string,
spawnPoint: Spawnpoint,
staticAmmoDist: Record<string, IStaticAmmoDetails[]>,
): IContainerItem
{
): IContainerItem {
const chosenItem = spawnPoint.template.Items.find((item) => item._id === chosenComposedKey);
const chosenTpl = chosenItem?._tpl;
if (!chosenTpl)
{
if (!chosenTpl) {
throw new Error(`Item for tpl ${chosenComposedKey} was not found in the spawn point`);
}
const itemTemplate = this.itemHelper.getItem(chosenTpl)[1];
@ -891,10 +838,9 @@ export class LocationLootGenerator
const itemWithMods: Item[] = [];
// Money/Ammo - don't rely on items in spawnPoint.template.Items so we can randomise it ourselves
if (this.itemHelper.isOfBaseclasses(chosenTpl, [BaseClasses.MONEY, BaseClasses.AMMO]))
{
const stackCount
= itemTemplate._props.StackMaxSize === 1
if (this.itemHelper.isOfBaseclasses(chosenTpl, [BaseClasses.MONEY, BaseClasses.AMMO])) {
const stackCount =
itemTemplate._props.StackMaxSize === 1
? 1
: this.randomUtil.getInt(itemTemplate._props.StackMinRandom!, itemTemplate._props.StackMaxRandom!);
@ -903,21 +849,16 @@ export class LocationLootGenerator
_tpl: chosenTpl,
upd: { StackObjectsCount: stackCount },
});
}
else if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.AMMO_BOX))
{
} else if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.AMMO_BOX)) {
// Fill with cartridges
const ammoBoxItem: Item[] = [{ _id: this.objectId.generate(), _tpl: chosenTpl }];
this.itemHelper.addCartridgesToAmmoBox(ammoBoxItem, itemTemplate);
itemWithMods.push(...ammoBoxItem);
}
else if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.MAGAZINE))
{
} else if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.MAGAZINE)) {
// Create array with just magazine
const magazineItem: Item[] = [{ _id: this.objectId.generate(), _tpl: chosenTpl }];
if (this.randomUtil.getChance100(this.locationConfig.staticMagazineLootHasAmmoChancePercent))
{
if (this.randomUtil.getChance100(this.locationConfig.staticMagazineLootHasAmmoChancePercent)) {
// Add randomised amount of cartridges
this.itemHelper.fillMagazineWithRandomCartridge(
magazineItem,
@ -929,9 +870,7 @@ export class LocationLootGenerator
}
itemWithMods.push(...magazineItem);
}
else
{
} else {
// Also used by armors to get child mods
// Get item + children and add into array we return
const itemWithChildren = this.itemHelper.findAndReturnChildrenAsItems(
@ -956,8 +895,7 @@ export class LocationLootGenerator
* @param itemWithChildren Item with mods to update
* @param newId new id to add on chidren of base item
*/
protected reparentItemAndChildren(itemWithChildren: Item[], newId = this.objectId.generate()): void
{
protected reparentItemAndChildren(itemWithChildren: Item[], newId = this.objectId.generate()): void {
// original id on base item
const oldId = itemWithChildren[0]._id;
@ -965,10 +903,8 @@ export class LocationLootGenerator
itemWithChildren[0]._id = newId;
// Update all parentIds of items attached to base item to use new id
for (const item of itemWithChildren)
{
if (item.parentId === oldId)
{
for (const item of itemWithChildren) {
if (item.parentId === oldId) {
item.parentId = newId;
}
}
@ -980,10 +916,8 @@ export class LocationLootGenerator
* @param chosenTpl Tpl we want to get item with
* @returns Item object
*/
protected getItemInArray(items: Item[], chosenTpl: string): Item | undefined
{
if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.WEAPON))
{
protected getItemInArray(items: Item[], chosenTpl: string): Item | undefined {
if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.WEAPON)) {
return items.find((v) => v._tpl === chosenTpl && v.parentId === undefined);
}
@ -995,8 +929,7 @@ export class LocationLootGenerator
chosenTpl: string,
staticAmmoDist: Record<string, IStaticAmmoDetails[]>,
parentId?: string,
): IContainerItem
{
): IContainerItem {
const itemTemplate = this.itemHelper.getItem(chosenTpl)[1];
let width = itemTemplate._props.Width!;
let height = itemTemplate._props.Height!;
@ -1004,37 +937,30 @@ export class LocationLootGenerator
const rootItem = items[0];
// Use passed in parentId as override for new item
if (parentId)
{
if (parentId) {
rootItem.parentId = parentId;
}
if (
this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.MONEY)
|| this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.AMMO)
)
{
this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.MONEY) ||
this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.AMMO)
) {
// Edge case - some ammos e.g. flares or M406 grenades shouldn't be stacked
const stackCount
= itemTemplate._props.StackMaxSize === 1
const stackCount =
itemTemplate._props.StackMaxSize === 1
? 1
: this.randomUtil.getInt(itemTemplate._props.StackMinRandom!, itemTemplate._props.StackMaxRandom!);
rootItem.upd = { StackObjectsCount: stackCount };
}
// No spawn point, use default template
else if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.WEAPON))
{
else if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.WEAPON)) {
let children: Item[] = [];
const defaultPreset = this.cloner.clone(this.presetHelper.getDefaultPreset(chosenTpl));
if (defaultPreset?._items)
{
try
{
if (defaultPreset?._items) {
try {
children = this.itemHelper.reparentItemAndChildren(defaultPreset._items[0], defaultPreset._items);
}
catch (error)
{
} catch (error) {
// this item already broke it once without being reproducible tpl = "5839a40f24597726f856b511"; AKS-74UB Default
// 5ea03f7400685063ec28bfa8 // ppsh default
// 5ba26383d4351e00334c93d9 //mp7_devgru
@ -1049,16 +975,13 @@ export class LocationLootGenerator
throw error;
}
}
else
{
} else {
// RSP30 (62178be9d0050232da3485d9/624c0b3340357b5f566e8766/6217726288ed9f0845317459) doesnt have any default presets and kills this code below as it has no chidren to reparent
this.logger.debug(`createItem() No preset found for weapon: ${chosenTpl}`);
}
const rootItem = items[0];
if (!rootItem)
{
if (!rootItem) {
this.logger.error(
this.localisationService.getText("location-missing_root_item", {
tpl: chosenTpl,
@ -1069,15 +992,11 @@ export class LocationLootGenerator
throw new Error(this.localisationService.getText("location-critical_error_see_log"));
}
try
{
if (children?.length > 0)
{
try {
if (children?.length > 0) {
items = this.itemHelper.reparentItemAndChildren(rootItem, children);
}
}
catch (error)
{
} catch (error) {
this.logger.error(
this.localisationService.getText("location-unable_to_reparent_item", {
tpl: chosenTpl,
@ -1094,8 +1013,7 @@ export class LocationLootGenerator
// BotGenerator
const magazine = items.filter((item) => item.slotId === "mod_magazine")[0];
// some weapon presets come without magazine; only fill the mag if it exists
if (magazine)
{
if (magazine) {
const magTemplate = this.itemHelper.getItem(magazine._tpl)[1];
const weaponTemplate = this.itemHelper.getItem(chosenTpl)[1];
@ -1121,14 +1039,10 @@ export class LocationLootGenerator
height = size.height;
}
// No spawnpoint to fall back on, generate manually
else if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.AMMO_BOX))
{
else if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.AMMO_BOX)) {
this.itemHelper.addCartridgesToAmmoBox(items, itemTemplate);
}
else if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.MAGAZINE))
{
if (this.randomUtil.getChance100(this.locationConfig.magazineLootHasAmmoChancePercent))
{
} else if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.MAGAZINE)) {
if (this.randomUtil.getChance100(this.locationConfig.magazineLootHasAmmoChancePercent)) {
// Create array with just magazine
const magazineWithCartridges = [rootItem];
this.itemHelper.fillMagazineWithRandomCartridge(
@ -1142,24 +1056,18 @@ export class LocationLootGenerator
// Replace existing magazine with above array
items.splice(items.indexOf(rootItem), 1, ...magazineWithCartridges);
}
}
else if (this.itemHelper.armorItemCanHoldMods(chosenTpl))
{
} else if (this.itemHelper.armorItemCanHoldMods(chosenTpl)) {
const defaultPreset = this.presetHelper.getDefaultPreset(chosenTpl);
if (defaultPreset)
{
if (defaultPreset) {
const presetAndMods: Item[] = this.itemHelper.replaceIDs(defaultPreset._items);
this.itemHelper.remapRootItemId(presetAndMods);
// Use original items parentId otherwise item doesnt get added to container correctly
presetAndMods[0].parentId = rootItem.parentId;
items = presetAndMods;
}
else
{
} else {
// We make base item above, at start of function, no need to do it here
if ((itemTemplate._props.Slots?.length ?? 0) > 0)
{
if ((itemTemplate._props.Slots?.length ?? 0) > 0) {
items = this.itemHelper.addChildSlotItems(
items,
itemTemplate,

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { InventoryHelper } from "@spt/helpers/InventoryHelper";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { PresetHelper } from "@spt/helpers/PresetHelper";
@ -16,12 +15,12 @@ import { LocalisationService } from "@spt/services/LocalisationService";
import { RagfairLinkedItemService } from "@spt/services/RagfairLinkedItemService";
import { HashUtil } from "@spt/utils/HashUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { inject, injectable } from "tsyringe";
type ItemLimit = { current: number, max: number };
type ItemLimit = { current: number; max: number };
@injectable()
export class LootGenerator
{
export class LootGenerator {
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("HashUtil") protected hashUtil: HashUtil,
@ -34,16 +33,14 @@ export class LootGenerator
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("RagfairLinkedItemService") protected ragfairLinkedItemService: RagfairLinkedItemService,
@inject("ItemFilterService") protected itemFilterService: ItemFilterService,
)
{}
) {}
/**
* Generate a list of items based on configuration options parameter
* @param options parameters to adjust how loot is generated
* @returns An array of loot items
*/
public createRandomLoot(options: LootRequest): Item[]
{
public createRandomLoot(options: LootRequest): Item[] {
const result: Item[] = [];
const itemTypeCounts = this.initItemLimitCounter(options.itemLimits);
@ -53,16 +50,13 @@ export class LootGenerator
...options.itemBlacklist,
]);
if (options.useRewarditemBlacklist)
{
if (options.useRewarditemBlacklist) {
const itemsToAdd = this.itemFilterService.getItemRewardBlacklist();
itemBlacklist = new Set([...itemBlacklist, ...itemsToAdd]);
}
if (!options.allowBossItems)
{
for (const bossItem of this.itemFilterService.getBossItems())
{
if (!options.allowBossItems) {
for (const bossItem of this.itemFilterService.getBossItems()) {
itemBlacklist.add(bossItem);
}
}
@ -72,15 +66,13 @@ export class LootGenerator
options.weaponCrateCount.min,
options.weaponCrateCount.max,
);
if (sealedWeaponCrateCount > 0)
{
if (sealedWeaponCrateCount > 0) {
// Get list of all sealed containers from db - they're all the same, just for flavor
const sealedWeaponContainerPool = Object.values(itemsDb).filter((item) =>
item._name.includes("event_container_airdrop"),
);
for (let index = 0; index < sealedWeaponCrateCount; index++)
{
for (let index = 0; index < sealedWeaponCrateCount; index++) {
// Choose one at random + add to results array
const chosenSealedContainer = this.randomUtil.getArrayValue(sealedWeaponContainerPool);
result.push({
@ -97,20 +89,17 @@ export class LootGenerator
// Get items from items.json that have a type of item + not in global blacklist + basetype is in whitelist
const items = Object.entries(itemsDb).filter(
(item) =>
!itemBlacklist.has(item[1]._id)
&& item[1]._type.toLowerCase() === "item"
&& !item[1]._props.QuestItem
&& options.itemTypeWhitelist.includes(item[1]._parent),
!itemBlacklist.has(item[1]._id) &&
item[1]._type.toLowerCase() === "item" &&
!item[1]._props.QuestItem &&
options.itemTypeWhitelist.includes(item[1]._parent),
);
// Pool has items we could add as loot, proceed
if (items.length > 0)
{
if (items.length > 0) {
const randomisedItemCount = this.randomUtil.getInt(options.itemCount.min, options.itemCount.max);
for (let index = 0; index < randomisedItemCount; index++)
{
if (!this.findAndAddRandomItemToLoot(items, itemTypeCounts, options, result))
{
for (let index = 0; index < randomisedItemCount; index++) {
if (!this.findAndAddRandomItemToLoot(items, itemTypeCounts, options, result)) {
// Failed to add, reduce index so we get another attempt
index--;
}
@ -125,16 +114,13 @@ export class LootGenerator
options.weaponPresetCount.min,
options.weaponPresetCount.max,
);
if (randomisedWeaponPresetCount > 0)
{
if (randomisedWeaponPresetCount > 0) {
const weaponDefaultPresets = globalDefaultPresets.filter((preset) =>
this.itemHelper.isOfBaseclass(preset._encyclopedia, BaseClasses.WEAPON),
);
if (weaponDefaultPresets.length > 0)
{
for (let index = 0; index < randomisedWeaponPresetCount; index++)
{
if (weaponDefaultPresets.length > 0) {
for (let index = 0; index < randomisedWeaponPresetCount; index++) {
if (
!this.findAndAddRandomPresetToLoot(
weaponDefaultPresets,
@ -142,8 +128,7 @@ export class LootGenerator
itemBlacklistArray,
result,
)
)
{
) {
// Failed to add, reduce index so we get another attempt
index--;
}
@ -156,8 +141,7 @@ export class LootGenerator
options.armorPresetCount.min,
options.armorPresetCount.max,
);
if (randomisedArmorPresetCount > 0)
{
if (randomisedArmorPresetCount > 0) {
const armorDefaultPresets = globalDefaultPresets.filter((preset) =>
this.itemHelper.armorItemCanHoldMods(preset._encyclopedia),
);
@ -166,10 +150,8 @@ export class LootGenerator
);
// Add some armors to rewards
if (levelFilteredArmorPresets.length > 0)
{
for (let index = 0; index < randomisedArmorPresetCount; index++)
{
if (levelFilteredArmorPresets.length > 0) {
for (let index = 0; index < randomisedArmorPresetCount; index++) {
if (
!this.findAndAddRandomPresetToLoot(
levelFilteredArmorPresets,
@ -177,8 +159,7 @@ export class LootGenerator
itemBlacklistArray,
result,
)
)
{
) {
// Failed to add, reduce index so we get another attempt
index--;
}
@ -195,14 +176,11 @@ export class LootGenerator
* @param options Loot request options - armor level etc
* @returns True if item has desired armor level
*/
protected isArmorOfDesiredProtectionLevel(armor: IPreset, options: LootRequest): boolean
{
protected isArmorOfDesiredProtectionLevel(armor: IPreset, options: LootRequest): boolean {
const relevantSlots = ["front_plate", "helmet_top", "soft_armor_front"];
for (const slotId of relevantSlots)
{
for (const slotId of relevantSlots) {
const armorItem = armor._items.find((item) => item?.slotId?.toLowerCase() === slotId);
if (!armorItem)
{
if (!armorItem) {
continue;
}
@ -220,11 +198,9 @@ export class LootGenerator
* @param limits limits as defined in config
* @returns record, key: item tplId, value: current/max item count allowed
*/
protected initItemLimitCounter(limits: Record<string, number>): Record<string, ItemLimit>
{
protected initItemLimitCounter(limits: Record<string, number>): Record<string, ItemLimit> {
const itemTypeCounts: Record<string, ItemLimit> = {};
for (const itemTypeId in limits)
{
for (const itemTypeId in limits) {
itemTypeCounts[itemTypeId] = { current: 0, max: limits[itemTypeId] };
}
@ -241,22 +217,19 @@ export class LootGenerator
*/
protected findAndAddRandomItemToLoot(
items: [string, ITemplateItem][],
itemTypeCounts: Record<string, { current: number, max: number }>,
itemTypeCounts: Record<string, { current: number; max: number }>,
options: LootRequest,
result: Item[],
): boolean
{
): boolean {
const randomItem = this.randomUtil.getArrayValue(items)[1];
const itemLimitCount = itemTypeCounts[randomItem._parent];
if (itemLimitCount && itemLimitCount.current > itemLimitCount.max)
{
if (itemLimitCount && itemLimitCount.current > itemLimitCount.max) {
return false;
}
// Skip armors as they need to come from presets
if (this.itemHelper.armorItemCanHoldMods(randomItem._id))
{
if (this.itemHelper.armorItemCanHoldMods(randomItem._id)) {
return false;
}
@ -270,16 +243,14 @@ export class LootGenerator
};
// Special case - handle items that need a stackcount > 1
if (randomItem._props.StackMaxSize > 1)
{
if (randomItem._props.StackMaxSize > 1) {
newLootItem.upd.StackObjectsCount = this.getRandomisedStackCount(randomItem, options);
}
newLootItem._tpl = randomItem._id;
result.push(newLootItem);
if (itemLimitCount)
{
if (itemLimitCount) {
// Increment item count as it's in limit array
itemLimitCount.current++;
}
@ -294,13 +265,11 @@ export class LootGenerator
* @param options loot options
* @returns stack count
*/
protected getRandomisedStackCount(item: ITemplateItem, options: LootRequest): number
{
protected getRandomisedStackCount(item: ITemplateItem, options: LootRequest): number {
let min = item._props.StackMinRandom;
let max = item._props.StackMaxSize;
if (options.itemStackLimits[item._id])
{
if (options.itemStackLimits[item._id]) {
min = options.itemStackLimits[item._id].min;
max = options.itemStackLimits[item._id].max;
}
@ -318,23 +287,20 @@ export class LootGenerator
*/
protected findAndAddRandomPresetToLoot(
presetPool: IPreset[],
itemTypeCounts: Record<string, { current: number, max: number }>,
itemTypeCounts: Record<string, { current: number; max: number }>,
itemBlacklist: string[],
result: Item[],
): boolean
{
): boolean {
// Choose random preset and get details from item db using encyclopedia value (encyclopedia === tplId)
const chosenPreset = this.randomUtil.getArrayValue(presetPool);
if (!chosenPreset)
{
if (!chosenPreset) {
this.logger.warning("Unable to find random preset in given presets, skipping");
return false;
}
// No `_encyclopedia` property, not possible to reliably get root item tpl
if (!chosenPreset?._encyclopedia)
{
if (!chosenPreset?._encyclopedia) {
this.logger.debug(`Preset with id: ${chosenPreset?._id} lacks encyclopedia property, skipping`);
return false;
@ -342,22 +308,19 @@ export class LootGenerator
// Get preset root item db details via its `_encyclopedia` property
const itemDbDetails = this.itemHelper.getItem(chosenPreset._encyclopedia);
if (!itemDbDetails[0])
{
if (!itemDbDetails[0]) {
this.logger.debug(`Unable to find preset with tpl: ${chosenPreset._encyclopedia}, skipping`);
return false;
}
// Skip preset if root item is blacklisted
if (itemBlacklist.includes(chosenPreset._items[0]._tpl))
{
if (itemBlacklist.includes(chosenPreset._items[0]._tpl)) {
return false;
}
// Some custom mod items lack a parent property
if (!itemDbDetails[1]._parent)
{
if (!itemDbDetails[1]._parent) {
this.logger.error(this.localisationService.getText("loot-item_missing_parentid", itemDbDetails[1]?._name));
return false;
@ -365,21 +328,18 @@ export class LootGenerator
// Check chosen preset hasn't exceeded spawn limit
const itemLimitCount = itemTypeCounts[itemDbDetails[1]._parent];
if (itemLimitCount && itemLimitCount.current > itemLimitCount.max)
{
if (itemLimitCount && itemLimitCount.current > itemLimitCount.max) {
return false;
}
const presetAndMods: Item[] = this.itemHelper.replaceIDs(chosenPreset._items);
this.itemHelper.remapRootItemId(presetAndMods);
// Add chosen preset tpl to result array
presetAndMods.forEach((item) =>
{
presetAndMods.forEach((item) => {
result.push(item);
});
if (itemLimitCount)
{
if (itemLimitCount) {
// Increment item count as item has been chosen and its inside itemLimitCount dictionary
itemLimitCount.current++;
}
@ -393,8 +353,7 @@ export class LootGenerator
* @param containerSettings sealed weapon container settings
* @returns Array of item with children arrays
*/
public getSealedWeaponCaseLoot(containerSettings: ISealedAirdropContainerSettings): Item[][]
{
public getSealedWeaponCaseLoot(containerSettings: ISealedAirdropContainerSettings): Item[][] {
const itemsToReturn: Item[][] = [];
// Choose a weapon to give to the player (weighted)
@ -404,8 +363,7 @@ export class LootGenerator
// Get itemDb details of weapon
const weaponDetailsDb = this.itemHelper.getItem(chosenWeaponTpl);
if (!weaponDetailsDb[0])
{
if (!weaponDetailsDb[0]) {
this.logger.error(
this.localisationService.getText("loot-non_item_picked_as_sealed_weapon_crate_reward", chosenWeaponTpl),
);
@ -419,9 +377,10 @@ export class LootGenerator
: this.randomUtil.getArrayValue(this.presetHelper.getPresets(chosenWeaponTpl));
// No default preset found for weapon, choose a random one
if (!chosenWeaponPreset)
{
this.logger.warning(this.localisationService.getText("loot-default_preset_not_found_using_random", chosenWeaponTpl));
if (!chosenWeaponPreset) {
this.logger.warning(
this.localisationService.getText("loot-default_preset_not_found_using_random", chosenWeaponTpl),
);
chosenWeaponPreset = this.randomUtil.getArrayValue(this.presetHelper.getPresets(chosenWeaponTpl));
}
@ -453,26 +412,21 @@ export class LootGenerator
protected getSealedContainerNonWeaponModRewards(
containerSettings: ISealedAirdropContainerSettings,
weaponDetailsDb: ITemplateItem,
): Item[][]
{
): Item[][] {
const rewards: Item[][] = [];
for (const rewardTypeId in containerSettings.rewardTypeLimits)
{
for (const rewardTypeId in containerSettings.rewardTypeLimits) {
const settings = containerSettings.rewardTypeLimits[rewardTypeId];
const rewardCount = this.randomUtil.getInt(settings.min, settings.max);
if (rewardCount === 0)
{
if (rewardCount === 0) {
continue;
}
// Edge case - ammo boxes
if (rewardTypeId === BaseClasses.AMMO_BOX)
{
if (rewardTypeId === BaseClasses.AMMO_BOX) {
// Get ammoboxes from db
const ammoBoxesDetails = containerSettings.ammoBoxWhitelist.map((tpl) =>
{
const ammoBoxesDetails = containerSettings.ammoBoxWhitelist.map((tpl) => {
const itemDetails = this.itemHelper.getItem(tpl);
return itemDetails[1];
});
@ -480,15 +434,13 @@ export class LootGenerator
// Need to find boxes that matches weapons caliber
const weaponCaliber = weaponDetailsDb._props.ammoCaliber;
const ammoBoxesMatchingCaliber = ammoBoxesDetails.filter((x) => x._props.ammoCaliber === weaponCaliber);
if (ammoBoxesMatchingCaliber.length === 0)
{
if (ammoBoxesMatchingCaliber.length === 0) {
this.logger.debug(`No ammo box with caliber ${weaponCaliber} found, skipping`);
continue;
}
for (let index = 0; index < rewardCount; index++)
{
for (let index = 0; index < rewardCount; index++) {
const chosenAmmoBox = this.randomUtil.getArrayValue(ammoBoxesMatchingCaliber);
const ammoBoxItem: Item[] = [{ _id: this.hashUtil.generate(), _tpl: chosenAmmoBox._id }];
this.itemHelper.addCartridgesToAmmoBox(ammoBoxItem, chosenAmmoBox);
@ -501,22 +453,20 @@ export class LootGenerator
// Get all items of the desired type + not quest items + not globally blacklisted
const rewardItemPool = Object.values(this.databaseService.getItems()).filter(
(item) =>
item._parent === rewardTypeId
&& item._type.toLowerCase() === "item"
&& !this.itemFilterService.isItemBlacklisted(item._id)
&& !(containerSettings.allowBossItems || this.itemFilterService.isBossItem(item._id))
&& !item._props.QuestItem,
item._parent === rewardTypeId &&
item._type.toLowerCase() === "item" &&
!this.itemFilterService.isItemBlacklisted(item._id) &&
!(containerSettings.allowBossItems || this.itemFilterService.isBossItem(item._id)) &&
!item._props.QuestItem,
);
if (rewardItemPool.length === 0)
{
if (rewardItemPool.length === 0) {
this.logger.debug(`No items with base type of ${rewardTypeId} found, skipping`);
continue;
}
for (let index = 0; index < rewardCount; index++)
{
for (let index = 0; index < rewardCount; index++) {
// Choose a random item from pool
const chosenRewardItem = this.randomUtil.getArrayValue(rewardItemPool);
const rewardItem: Item[] = [{ _id: this.hashUtil.generate(), _tpl: chosenRewardItem._id }];
@ -539,17 +489,14 @@ export class LootGenerator
containerSettings: ISealedAirdropContainerSettings,
linkedItemsToWeapon: ITemplateItem[],
chosenWeaponPreset: IPreset,
): Item[][]
{
): Item[][] {
const modRewards: Item[][] = [];
for (const rewardTypeId in containerSettings.weaponModRewardLimits)
{
for (const rewardTypeId in containerSettings.weaponModRewardLimits) {
const settings = containerSettings.weaponModRewardLimits[rewardTypeId];
const rewardCount = this.randomUtil.getInt(settings.min, settings.max);
// Nothing to add, skip reward type
if (rewardCount === 0)
{
if (rewardCount === 0) {
continue;
}
@ -557,8 +504,7 @@ export class LootGenerator
const relatedItems = linkedItemsToWeapon?.filter(
(item) => item._parent === rewardTypeId && !this.itemFilterService.isItemBlacklisted(item._id),
);
if (!relatedItems || relatedItems.length === 0)
{
if (!relatedItems || relatedItems.length === 0) {
this.logger.debug(
`No items found to fulfil reward type: ${rewardTypeId} for weapon: ${chosenWeaponPreset._name}, skipping type`,
);
@ -566,8 +512,7 @@ export class LootGenerator
}
// Find a random item of the desired type and add as reward
for (let index = 0; index < rewardCount; index++)
{
for (let index = 0; index < rewardCount; index++) {
const chosenItem = this.randomUtil.drawRandomFromList(relatedItems);
const item: Item[] = [{ _id: this.hashUtil.generate(), _tpl: chosenItem[0]._id }];
@ -583,13 +528,11 @@ export class LootGenerator
* @param rewardContainerDetails
* @returns Array of item with children arrays
*/
public getRandomLootContainerLoot(rewardContainerDetails: RewardDetails): Item[][]
{
public getRandomLootContainerLoot(rewardContainerDetails: RewardDetails): Item[][] {
const itemsToReturn: Item[][] = [];
// Get random items and add to newItemRequest
for (let index = 0; index < rewardContainerDetails.rewardCount; index++)
{
for (let index = 0; index < rewardContainerDetails.rewardCount; index++) {
// Pick random reward from pool, add to request object
const chosenRewardItemTpl = this.weightedRandomHelper.getWeightedValue<string>(
rewardContainerDetails.rewardTplPool,

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { WeightedRandomHelper } from "@spt/helpers/WeightedRandomHelper";
import { ITemplateItem } from "@spt/models/eft/common/tables/ITemplateItem";
@ -10,14 +9,14 @@ import { DatabaseService } from "@spt/services/DatabaseService";
import { ItemFilterService } from "@spt/services/ItemFilterService";
import { RagfairPriceService } from "@spt/services/RagfairPriceService";
import { SeasonalEventService } from "@spt/services/SeasonalEventService";
import { inject, injectable } from "tsyringe";
/**
* Handle the generation of dynamic PMC loot in pockets and backpacks
* and the removal of blacklisted items
*/
@injectable()
export class PMCLootGenerator
{
export class PMCLootGenerator {
protected pocketLootPool: Record<string, number> = {};
protected vestLootPool: Record<string, number> = {};
protected backpackLootPool: Record<string, number> = {};
@ -31,8 +30,7 @@ export class PMCLootGenerator
@inject("RagfairPriceService") protected ragfairPriceService: RagfairPriceService,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
)
{
) {
this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
}
@ -40,15 +38,12 @@ export class PMCLootGenerator
* Create an array of loot items a PMC can have in their pockets
* @returns string array of tpls
*/
public generatePMCPocketLootPool(botRole: string): Record<string, number>
{
public generatePMCPocketLootPool(botRole: string): Record<string, number> {
// Hydrate loot dictionary if empty
if (Object.keys(this.pocketLootPool).length === 0)
{
if (Object.keys(this.pocketLootPool).length === 0) {
const items = this.databaseService.getItems();
const pmcPriceOverrides
= this.databaseService.getBots().types[botRole === "pmcBEAR" ? "bear" : "usec"].inventory.items
.Pockets;
const pmcPriceOverrides =
this.databaseService.getBots().types[botRole === "pmcBEAR" ? "bear" : "usec"].inventory.items.Pockets;
const allowedItemTypes = this.pmcConfig.pocketLoot.whitelist;
const pmcItemBlacklist = this.pmcConfig.pocketLoot.blacklist;
@ -59,22 +54,18 @@ export class PMCLootGenerator
const itemsToAdd = Object.values(items).filter(
(item) =>
allowedItemTypes.includes(item._parent)
&& this.itemHelper.isValidItem(item._id)
&& !pmcItemBlacklist.includes(item._id)
&& !itemBlacklist.includes(item._id)
&& this.itemFitsInto1By2Slot(item),
allowedItemTypes.includes(item._parent) &&
this.itemHelper.isValidItem(item._id) &&
!pmcItemBlacklist.includes(item._id) &&
!itemBlacklist.includes(item._id) &&
this.itemFitsInto1By2Slot(item),
);
for (const itemToAdd of itemsToAdd)
{
for (const itemToAdd of itemsToAdd) {
// If pmc has override, use that. Otherwise use flea price
if (pmcPriceOverrides[itemToAdd._id])
{
if (pmcPriceOverrides[itemToAdd._id]) {
this.pocketLootPool[itemToAdd._id] = pmcPriceOverrides[itemToAdd._id];
}
else
{
} else {
// Set price of item as its weight
const price = this.ragfairPriceService.getDynamicItemPrice(itemToAdd._id, Money.ROUBLES);
this.pocketLootPool[itemToAdd._id] = price;
@ -82,8 +73,7 @@ export class PMCLootGenerator
}
const highestPrice = Math.max(...Object.values(this.backpackLootPool));
for (const key of Object.keys(this.pocketLootPool))
{
for (const key of Object.keys(this.pocketLootPool)) {
// Invert price so cheapest has a larger weight
// Times by highest price so most expensive item has weight of 1
this.pocketLootPool[key] = Math.round((1 / this.pocketLootPool[key]) * highestPrice);
@ -99,14 +89,12 @@ export class PMCLootGenerator
* Create an array of loot items a PMC can have in their vests
* @returns string array of tpls
*/
public generatePMCVestLootPool(botRole: string): Record<string, number>
{
public generatePMCVestLootPool(botRole: string): Record<string, number> {
// Hydrate loot dictionary if empty
if (Object.keys(this.vestLootPool).length === 0)
{
if (Object.keys(this.vestLootPool).length === 0) {
const items = this.databaseService.getItems();
const pmcPriceOverrides
= this.databaseService.getBots().types[botRole === "pmcBEAR" ? "bear" : "usec"].inventory.items
const pmcPriceOverrides =
this.databaseService.getBots().types[botRole === "pmcBEAR" ? "bear" : "usec"].inventory.items
.TacticalVest;
const allowedItemTypes = this.pmcConfig.vestLoot.whitelist;
@ -118,22 +106,18 @@ export class PMCLootGenerator
const itemsToAdd = Object.values(items).filter(
(item) =>
allowedItemTypes.includes(item._parent)
&& this.itemHelper.isValidItem(item._id)
&& !pmcItemBlacklist.includes(item._id)
&& !itemBlacklist.includes(item._id)
&& this.itemFitsInto2By2Slot(item),
allowedItemTypes.includes(item._parent) &&
this.itemHelper.isValidItem(item._id) &&
!pmcItemBlacklist.includes(item._id) &&
!itemBlacklist.includes(item._id) &&
this.itemFitsInto2By2Slot(item),
);
for (const itemToAdd of itemsToAdd)
{
for (const itemToAdd of itemsToAdd) {
// If pmc has override, use that. Otherwise use flea price
if (pmcPriceOverrides[itemToAdd._id])
{
if (pmcPriceOverrides[itemToAdd._id]) {
this.vestLootPool[itemToAdd._id] = pmcPriceOverrides[itemToAdd._id];
}
else
{
} else {
// Set price of item as its weight
const price = this.ragfairPriceService.getDynamicItemPrice(itemToAdd._id, Money.ROUBLES);
this.vestLootPool[itemToAdd._id] = price;
@ -141,8 +125,7 @@ export class PMCLootGenerator
}
const highestPrice = Math.max(...Object.values(this.backpackLootPool));
for (const key of Object.keys(this.vestLootPool))
{
for (const key of Object.keys(this.vestLootPool)) {
// Invert price so cheapest has a larger weight
// Times by highest price so most expensive item has weight of 1
this.vestLootPool[key] = Math.round((1 / this.vestLootPool[key]) * highestPrice);
@ -160,8 +143,7 @@ export class PMCLootGenerator
* @param item Item to check size of
* @returns true if it fits
*/
protected itemFitsInto2By2Slot(item: ITemplateItem): boolean
{
protected itemFitsInto2By2Slot(item: ITemplateItem): boolean {
return item._props.Width <= 2 && item._props.Height <= 2;
}
@ -171,10 +153,8 @@ export class PMCLootGenerator
* @param item Item to check size of
* @returns true if it fits
*/
protected itemFitsInto1By2Slot(item: ITemplateItem): boolean
{
switch (`${item._props.Width}x${item._props.Height}`)
{
protected itemFitsInto1By2Slot(item: ITemplateItem): boolean {
switch (`${item._props.Width}x${item._props.Height}`) {
case "1x1":
case "1x2":
case "2x1":
@ -189,15 +169,12 @@ export class PMCLootGenerator
* Create an array of loot items a PMC can have in their backpack
* @returns string array of tpls
*/
public generatePMCBackpackLootPool(botRole: string): Record<string, number>
{
public generatePMCBackpackLootPool(botRole: string): Record<string, number> {
// Hydrate loot dictionary if empty
if (Object.keys(this.backpackLootPool).length === 0)
{
if (Object.keys(this.backpackLootPool).length === 0) {
const items = this.databaseService.getItems();
const pmcPriceOverrides
= this.databaseService.getBots().types[botRole === "pmcBEAR" ? "bear" : "usec"].inventory.items
.Backpack;
const pmcPriceOverrides =
this.databaseService.getBots().types[botRole === "pmcBEAR" ? "bear" : "usec"].inventory.items.Backpack;
const allowedItemTypes = this.pmcConfig.backpackLoot.whitelist;
const pmcItemBlacklist = this.pmcConfig.backpackLoot.blacklist;
@ -208,21 +185,17 @@ export class PMCLootGenerator
const itemsToAdd = Object.values(items).filter(
(item) =>
allowedItemTypes.includes(item._parent)
&& this.itemHelper.isValidItem(item._id)
&& !pmcItemBlacklist.includes(item._id)
&& !itemBlacklist.includes(item._id),
allowedItemTypes.includes(item._parent) &&
this.itemHelper.isValidItem(item._id) &&
!pmcItemBlacklist.includes(item._id) &&
!itemBlacklist.includes(item._id),
);
for (const itemToAdd of itemsToAdd)
{
for (const itemToAdd of itemsToAdd) {
// If pmc has price override, use that. Otherwise use flea price
if (pmcPriceOverrides[itemToAdd._id])
{
if (pmcPriceOverrides[itemToAdd._id]) {
this.backpackLootPool[itemToAdd._id] = pmcPriceOverrides[itemToAdd._id];
}
else
{
} else {
// Set price of item as its weight
const price = this.ragfairPriceService.getDynamicItemPrice(itemToAdd._id, Money.ROUBLES);
this.backpackLootPool[itemToAdd._id] = price;
@ -230,8 +203,7 @@ export class PMCLootGenerator
}
const highestPrice = Math.max(...Object.values(this.backpackLootPool));
for (const key of Object.keys(this.backpackLootPool))
{
for (const key of Object.keys(this.backpackLootPool)) {
// Invert price so cheapest has a larger weight
// Times by highest price so most expensive item has weight of 1
this.backpackLootPool[key] = Math.round((1 / this.backpackLootPool[key]) * highestPrice);

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { BotGenerator } from "@spt/generators/BotGenerator";
import { BotGeneratorHelper } from "@spt/helpers/BotGeneratorHelper";
import { BotHelper } from "@spt/helpers/BotHelper";
@ -22,13 +21,13 @@ import { BotLootCacheService } from "@spt/services/BotLootCacheService";
import { DatabaseService } from "@spt/services/DatabaseService";
import { FenceService } from "@spt/services/FenceService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HashUtil } from "@spt/utils/HashUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class PlayerScavGenerator
{
export class PlayerScavGenerator {
protected playerScavConfig: IPlayerScavConfig;
constructor(
@ -47,8 +46,7 @@ export class PlayerScavGenerator
@inject("BotGenerator") protected botGenerator: BotGenerator,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
) {
this.playerScavConfig = this.configServer.getConfig(ConfigTypes.PLAYERSCAV);
}
@ -57,8 +55,7 @@ export class PlayerScavGenerator
* @param sessionID session id to specify what profile is updated
* @returns profile object
*/
public generate(sessionID: string): IPmcData
{
public generate(sessionID: string): IPmcData {
// get karma level from profile
const profile = this.saveServer.getProfile(sessionID);
const pmcDataClone = this.cloner.clone(profile.characters.pmc);
@ -68,8 +65,7 @@ export class PlayerScavGenerator
// use karma level to get correct karmaSettings
const playerScavKarmaSettings = this.playerScavConfig.karmaLevel[scavKarmaLevel];
if (!playerScavKarmaSettings)
{
if (!playerScavKarmaSettings) {
this.logger.error(this.localisationService.getText("scav-missing_karma_settings", scavKarmaLevel));
}
@ -144,19 +140,15 @@ export class PlayerScavGenerator
possibleItemsToAdd: Record<string, number>,
scavData: IBotBase,
containersToAddTo: string[],
): void
{
for (const tpl in possibleItemsToAdd)
{
): void {
for (const tpl in possibleItemsToAdd) {
const shouldAdd = this.randomUtil.getChance100(possibleItemsToAdd[tpl]);
if (!shouldAdd)
{
if (!shouldAdd) {
continue;
}
const itemResult = this.itemHelper.getItem(tpl);
if (!itemResult[0])
{
if (!itemResult[0]) {
this.logger.warning(this.localisationService.getText("scav-unable_to_add_item_to_player_scav", tpl));
continue;
@ -179,8 +171,7 @@ export class PlayerScavGenerator
scavData.Inventory,
);
if (result !== ItemAddedResult.SUCCESS)
{
if (result !== ItemAddedResult.SUCCESS) {
this.logger.debug(`Unable to add keycard to bot. Reason: ${ItemAddedResult[result]}`);
}
}
@ -192,20 +183,17 @@ export class PlayerScavGenerator
* @param pmcData pmc profile
* @returns karma level
*/
protected getScavKarmaLevel(pmcData: IPmcData): number
{
protected getScavKarmaLevel(pmcData: IPmcData): number {
const fenceInfo = pmcData.TradersInfo[Traders.FENCE];
// Can be empty during profile creation
if (!fenceInfo)
{
if (!fenceInfo) {
this.logger.warning(this.localisationService.getText("scav-missing_karma_level_getting_default"));
return 0;
}
if (fenceInfo.standing > 6)
{
if (fenceInfo.standing > 6) {
return 6;
}
@ -219,14 +207,12 @@ export class PlayerScavGenerator
* @param botTypeForLoot bot type to use for inventory/chances
* @returns IBotType object
*/
protected constructBotBaseTemplate(botTypeForLoot: string): IBotType
{
protected constructBotBaseTemplate(botTypeForLoot: string): IBotType {
const baseScavType = "assault";
const assaultBase = this.cloner.clone(this.botHelper.getBotTemplate(baseScavType));
// Loot bot is same as base bot, return base with no modification
if (botTypeForLoot === baseScavType)
{
if (botTypeForLoot === baseScavType) {
return assaultBase;
}
@ -243,13 +229,10 @@ export class PlayerScavGenerator
* @param karmaSettings Values to modify the bot template with
* @param baseBotNode bot template to modify according to karama level settings
*/
protected adjustBotTemplateWithKarmaSpecificSettings(karmaSettings: KarmaLevel, baseBotNode: IBotType): void
{
protected adjustBotTemplateWithKarmaSpecificSettings(karmaSettings: KarmaLevel, baseBotNode: IBotType): void {
// Adjust equipment chance values
for (const equipmentKey in karmaSettings.modifiers.equipment)
{
if (karmaSettings.modifiers.equipment[equipmentKey] === 0)
{
for (const equipmentKey in karmaSettings.modifiers.equipment) {
if (karmaSettings.modifiers.equipment[equipmentKey] === 0) {
continue;
}
@ -257,10 +240,8 @@ export class PlayerScavGenerator
}
// Adjust mod chance values
for (const modKey in karmaSettings.modifiers.mod)
{
if (karmaSettings.modifiers.mod[modKey] === 0)
{
for (const modKey in karmaSettings.modifiers.mod) {
if (karmaSettings.modifiers.mod[modKey] === 0) {
continue;
}
@ -268,63 +249,51 @@ export class PlayerScavGenerator
}
// Adjust item spawn quantity values
for (const itemLimitkey in karmaSettings.itemLimits)
{
for (const itemLimitkey in karmaSettings.itemLimits) {
baseBotNode.generation.items[itemLimitkey] = karmaSettings.itemLimits[itemLimitkey];
}
// Blacklist equipment
for (const equipmentKey in karmaSettings.equipmentBlacklist)
{
for (const equipmentKey in karmaSettings.equipmentBlacklist) {
const blacklistedItemTpls = karmaSettings.equipmentBlacklist[equipmentKey];
for (const itemToRemove of blacklistedItemTpls)
{
for (const itemToRemove of blacklistedItemTpls) {
delete baseBotNode.inventory.equipment[equipmentKey][itemToRemove];
}
}
}
protected getScavSkills(scavProfile: IPmcData): Skills
{
if (scavProfile.Skills)
{
protected getScavSkills(scavProfile: IPmcData): Skills {
if (scavProfile.Skills) {
return scavProfile.Skills;
}
return this.getDefaultScavSkills();
}
protected getDefaultScavSkills(): Skills
{
protected getDefaultScavSkills(): Skills {
return { Common: [], Mastering: [], Points: 0 };
}
protected getScavStats(scavProfile: IPmcData): Stats
{
if (scavProfile.Stats)
{
protected getScavStats(scavProfile: IPmcData): Stats {
if (scavProfile.Stats) {
return scavProfile.Stats;
}
return this.profileHelper.getDefaultCounters();
}
protected getScavLevel(scavProfile: IPmcData): number
{
protected getScavLevel(scavProfile: IPmcData): number {
// Info can be undefined on initial account creation
if (!scavProfile.Info?.Level)
{
if (!scavProfile.Info?.Level) {
return 1;
}
return scavProfile.Info.Level;
}
protected getScavExperience(scavProfile: IPmcData): number
{
protected getScavExperience(scavProfile: IPmcData): number {
// Info can be undefined on initial account creation
if (!scavProfile.Info?.Experience)
{
if (!scavProfile.Info?.Experience) {
return 0;
}
@ -338,17 +307,14 @@ export class PlayerScavGenerator
* @param pmcData pmc profile
* @returns
*/
protected setScavCooldownTimer(scavData: IPmcData, pmcData: IPmcData): IPmcData
{
protected setScavCooldownTimer(scavData: IPmcData, pmcData: IPmcData): IPmcData {
// Set cooldown time.
// Make sure to apply ScavCooldownTimer bonus from Hideout if the player has it.
let scavLockDuration = this.databaseService.getGlobals().config.SavagePlayCooldown;
let modifier = 1;
for (const bonus of pmcData.Bonuses)
{
if (bonus.type === BonusType.SCAV_COOLDOWN_TIMER)
{
for (const bonus of pmcData.Bonuses) {
if (bonus.type === BonusType.SCAV_COOLDOWN_TIMER) {
// Value is negative, so add.
// Also note that for scav cooldown, multiple bonuses stack additively.
modifier += bonus.value / 100;
@ -360,8 +326,7 @@ export class PlayerScavGenerator
scavLockDuration *= modifier;
const fullProfile = this.profileHelper.getFullProfile(pmcData?.sessionId);
if (fullProfile?.info?.edition?.toLowerCase?.().startsWith?.(AccountTypes.SPT_DEVELOPER))
{
if (fullProfile?.info?.edition?.toLowerCase?.().startsWith?.(AccountTypes.SPT_DEVELOPER)) {
// Set scav cooldown timer to 10 seconds for spt developer account
scavLockDuration = 10;
}

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { PresetHelper } from "@spt/helpers/PresetHelper";
import { IPreset } from "@spt/models/eft/common/IGlobals";
@ -10,10 +9,10 @@ import { ConfigServer } from "@spt/servers/ConfigServer";
import { DatabaseServer } from "@spt/servers/DatabaseServer";
import { SeasonalEventService } from "@spt/services/SeasonalEventService";
import { HashUtil } from "@spt/utils/HashUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class RagfairAssortGenerator
{
export class RagfairAssortGenerator {
protected generatedAssortItems: Item[][] = [];
protected ragfairConfig: IRagfairConfig;
@ -34,8 +33,7 @@ export class RagfairAssortGenerator
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
) {
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
}
@ -44,10 +42,8 @@ export class RagfairAssortGenerator
* Each sub array contains item + children (if any)
* @returns array of arrays
*/
public getAssortItems(): Item[][]
{
if (!this.assortsAreGenerated())
{
public getAssortItems(): Item[][] {
if (!this.assortsAreGenerated()) {
this.generatedAssortItems = this.generateRagfairAssortItems();
}
@ -58,8 +54,7 @@ export class RagfairAssortGenerator
* Check internal generatedAssortItems array has objects
* @returns true if array has objects
*/
protected assortsAreGenerated(): boolean
{
protected assortsAreGenerated(): boolean {
return this.generatedAssortItems.length > 0;
}
@ -67,8 +62,7 @@ export class RagfairAssortGenerator
* Generate an array of arrays (item + children) the flea can sell
* @returns array of arrays (item + children)
*/
protected generateRagfairAssortItems(): Item[][]
{
protected generateRagfairAssortItems(): Item[][] {
const results: Item[][] = [];
/** Get cloned items from db */
@ -80,8 +74,7 @@ export class RagfairAssortGenerator
const seasonalItemTplBlacklist = this.seasonalEventService.getInactiveSeasonalEventItems();
const presets = this.getPresetsToAdd();
for (const preset of presets)
{
for (const preset of presets) {
// Update Ids and clone
const presetAndMods: Item[] = this.itemHelper.replaceIDs(preset._items);
this.itemHelper.remapRootItemId(presetAndMods);
@ -96,25 +89,21 @@ export class RagfairAssortGenerator
results.push(presetAndMods);
}
for (const item of dbItemsClone)
{
if (!this.itemHelper.isValidItem(item._id, this.ragfairItemInvalidBaseTypes))
{
for (const item of dbItemsClone) {
if (!this.itemHelper.isValidItem(item._id, this.ragfairItemInvalidBaseTypes)) {
continue;
}
// Skip seasonal items when not in-season
if (
this.ragfairConfig.dynamic.removeSeasonalItemsWhenNotInEvent
&& !seasonalEventActive
&& seasonalItemTplBlacklist.includes(item._id)
)
{
this.ragfairConfig.dynamic.removeSeasonalItemsWhenNotInEvent &&
!seasonalEventActive &&
seasonalItemTplBlacklist.includes(item._id)
) {
continue;
}
if (processedArmorItems.includes(item._id))
{
if (processedArmorItems.includes(item._id)) {
// Already processed
continue;
}
@ -132,8 +121,7 @@ export class RagfairAssortGenerator
* ragfairConfig.dynamic.showDefaultPresetsOnly decides if its all presets or just defaults
* @returns IPreset array
*/
protected getPresetsToAdd(): IPreset[]
{
protected getPresetsToAdd(): IPreset[] {
return this.ragfairConfig.dynamic.showDefaultPresetsOnly
? Object.values(this.presetHelper.getDefaultPresets())
: this.presetHelper.getAllPresets();
@ -145,8 +133,7 @@ export class RagfairAssortGenerator
* @param id id to add to item
* @returns Hydrated Item object
*/
protected createRagfairAssortRootItem(tplId: string, id = this.hashUtil.generate()): Item
{
protected createRagfairAssortRootItem(tplId: string, id = this.hashUtil.generate()): Item {
return {
_id: id,
_tpl: tplId,

View File

@ -1,4 +1,3 @@
import { inject, injectable } from "tsyringe";
import { RagfairAssortGenerator } from "@spt/generators/RagfairAssortGenerator";
import { BotHelper } from "@spt/helpers/BotHelper";
import { HandbookHelper } from "@spt/helpers/HandbookHelper";
@ -29,16 +28,16 @@ import { FenceService } from "@spt/services/FenceService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { RagfairOfferService } from "@spt/services/RagfairOfferService";
import { RagfairPriceService } from "@spt/services/RagfairPriceService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HashUtil } from "@spt/utils/HashUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
@injectable()
export class RagfairOfferGenerator
{
export class RagfairOfferGenerator {
protected ragfairConfig: IRagfairConfig;
protected allowedFleaPriceItemsForBarter: { tpl: string, price: number }[];
protected allowedFleaPriceItemsForBarter: { tpl: string; price: number }[];
/** Internal counter to ensure each offer created has a unique value for its intId property */
protected offerCounter = 0;
@ -64,8 +63,7 @@ export class RagfairOfferGenerator
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
) {
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
}
@ -86,8 +84,7 @@ export class RagfairOfferGenerator
barterScheme: IBarterScheme[],
loyalLevel: number,
sellInOnePiece = false,
): IRagfairOffer
{
): IRagfairOffer {
const offer = this.createOffer(userID, time, items, barterScheme, loyalLevel, sellInOnePiece);
this.ragfairOfferService.addOffer(offer);
@ -111,12 +108,10 @@ export class RagfairOfferGenerator
barterScheme: IBarterScheme[],
loyalLevel: number,
isPackOffer = false,
): IRagfairOffer
{
): IRagfairOffer {
const isTrader = this.ragfairServerHelper.isTrader(userID);
const offerRequirements = barterScheme.map((barter) =>
{
const offerRequirements = barterScheme.map((barter) => {
const offerRequirement: OfferRequirement = {
_tpl: barter._tpl,
count: +barter.count.toFixed(2),
@ -124,8 +119,7 @@ export class RagfairOfferGenerator
};
// Dogtags define level and side
if (barter.level !== undefined)
{
if (barter.level !== undefined) {
offerRequirement.level = barter.level;
offerRequirement.side = barter.side;
}
@ -139,9 +133,7 @@ export class RagfairOfferGenerator
// Hydrate ammo boxes with cartridges + ensure only 1 item is present (ammo box)
// On offer refresh dont re-add cartridges to ammo box that already has cartridges
if (this.itemHelper.isOfBaseclass(itemsClone[0]._tpl, BaseClasses.AMMO_BOX)
&& itemsClone.length === 1)
{
if (this.itemHelper.isOfBaseclass(itemsClone[0]._tpl, BaseClasses.AMMO_BOX) && itemsClone.length === 1) {
this.itemHelper.addCartridgesToAmmoBox(itemsClone, this.itemHelper.getItem(items[0]._tpl)[1]);
}
@ -176,11 +168,9 @@ export class RagfairOfferGenerator
* @param isTrader Is the user creating the offer a trader
* @returns IRagfairOfferUser
*/
createUserDataForFleaOffer(userID: string, isTrader: boolean): IRagfairOfferUser
{
createUserDataForFleaOffer(userID: string, isTrader: boolean): IRagfairOfferUser {
// Trader offer
if (isTrader)
{
if (isTrader) {
return {
id: userID,
memberType: MemberCategory.TRADER,
@ -188,8 +178,7 @@ export class RagfairOfferGenerator
}
const isPlayerOffer = this.profileHelper.isPlayer(userID);
if (isPlayerOffer)
{
if (isPlayerOffer) {
const playerProfile = this.profileHelper.getPmcProfile(userID)!;
return {
id: playerProfile._id,
@ -210,7 +199,8 @@ export class RagfairOfferGenerator
nickname: this.botHelper.getPmcNicknameOfMaxLength(userID, 50),
rating: this.randomUtil.getFloat(
this.ragfairConfig.dynamic.rating.min,
this.ragfairConfig.dynamic.rating.max),
this.ragfairConfig.dynamic.rating.max,
),
isRatingGrowing: this.randomUtil.getBool(),
avatar: undefined,
aid: this.hashUtil.generateAccountId(),
@ -222,11 +212,9 @@ export class RagfairOfferGenerator
* @param offerRequirements barter requirements for offer
* @returns rouble cost of offer
*/
protected convertOfferRequirementsIntoRoubles(offerRequirements: OfferRequirement[]): number
{
protected convertOfferRequirementsIntoRoubles(offerRequirements: OfferRequirement[]): number {
let roublePrice = 0;
for (const requirement of offerRequirements)
{
for (const requirement of offerRequirements) {
roublePrice += this.paymentHelper.isMoneyTpl(requirement._tpl)
? Math.round(this.calculateRoublePrice(requirement.count, requirement._tpl))
: this.ragfairPriceService.getFleaPriceForItem(requirement._tpl) * requirement.count; // get flea price for barter offer items
@ -241,10 +229,8 @@ export class RagfairOfferGenerator
* @param userId persons id to get avatar of
* @returns url of avatar
*/
protected getAvatarUrl(isTrader: boolean, userId: string): string
{
if (isTrader)
{
protected getAvatarUrl(isTrader: boolean, userId: string): string {
if (isTrader) {
return this.databaseService.getTrader(userId).base.avatar;
}
@ -257,10 +243,8 @@ export class RagfairOfferGenerator
* @param currencyType Type of currency (euro/dollar/rouble)
* @returns count of roubles
*/
protected calculateRoublePrice(currencyCount: number, currencyType: string): number
{
if (currencyType === Money.ROUBLES)
{
protected calculateRoublePrice(currencyCount: number, currencyType: string): number {
if (currencyType === Money.ROUBLES) {
return currencyCount;
}
@ -272,10 +256,8 @@ export class RagfairOfferGenerator
* @param userId Users Id to check
* @returns Users Id
*/
protected getTraderId(userId: string): string
{
if (this.profileHelper.isPlayer(userId))
{
protected getTraderId(userId: string): string {
if (this.profileHelper.isPlayer(userId)) {
return this.saveServer.getProfile(userId).characters.pmc._id;
}
@ -287,16 +269,13 @@ export class RagfairOfferGenerator
* @param userId User to get flea rating of
* @returns Flea rating value
*/
protected getRating(userId: string): number
{
if (this.profileHelper.isPlayer(userId))
{
protected getRating(userId: string): number {
if (this.profileHelper.isPlayer(userId)) {
// Player offer
return this.saveServer.getProfile(userId).characters.pmc.RagfairInfo.rating;
}
if (this.ragfairServerHelper.isTrader(userId))
{
if (this.ragfairServerHelper.isTrader(userId)) {
// Trader offer
return 1;
}
@ -310,16 +289,13 @@ export class RagfairOfferGenerator
* @param userID user to check rating of
* @returns true if its growing
*/
protected getRatingGrowing(userID: string): boolean
{
if (this.profileHelper.isPlayer(userID))
{
protected getRatingGrowing(userID: string): boolean {
if (this.profileHelper.isPlayer(userID)) {
// player offer
return this.saveServer.getProfile(userID).characters.pmc.RagfairInfo.isRatingGrowing;
}
if (this.ragfairServerHelper.isTrader(userID))
{
if (this.ragfairServerHelper.isTrader(userID)) {
// trader offer
return true;
}
@ -335,26 +311,22 @@ export class RagfairOfferGenerator
* @param time Time the offer is posted
* @returns number of seconds until offer expires
*/
protected getOfferEndTime(userID: string, time: number): number
{
if (this.profileHelper.isPlayer(userID))
{
protected getOfferEndTime(userID: string, time: number): number {
if (this.profileHelper.isPlayer(userID)) {
// Player offer = current time + offerDurationTimeInHour;
const offerDurationTimeHours
= this.databaseService.getGlobals().config.RagFair.offerDurationTimeInHour;
const offerDurationTimeHours = this.databaseService.getGlobals().config.RagFair.offerDurationTimeInHour;
return this.timeUtil.getTimestamp() + Math.round(offerDurationTimeHours * TimeUtil.ONE_HOUR_AS_SECONDS);
}
if (this.ragfairServerHelper.isTrader(userID))
{
if (this.ragfairServerHelper.isTrader(userID)) {
// Trader offer
return this.databaseService.getTrader(userID).base.nextResupply;
}
// Generated fake-player offer
return Math.round(
time
+ this.randomUtil.getInt(
time +
this.randomUtil.getInt(
this.ragfairConfig.dynamic.endTimeSeconds.min,
this.ragfairConfig.dynamic.endTimeSeconds.max,
),
@ -365,8 +337,7 @@ export class RagfairOfferGenerator
* Create multiple offers for items by using a unique list of items we've generated previously
* @param expiredOffers optional, expired offers to regenerate
*/
public async generateDynamicOffers(expiredOffers?: Item[][]): Promise<void>
{
public async generateDynamicOffers(expiredOffers?: Item[][]): Promise<void> {
const replacingExpiredOffers = Boolean(expiredOffers?.length);
// get assort items from param if they exist, otherwise grab freshly generated assorts
@ -375,9 +346,11 @@ export class RagfairOfferGenerator
: this.ragfairAssortGenerator.getAssortItems();
// Create offers for each item set concurrently
await Promise.all(assortItemsToProcess.map((assortItemWithChildren) =>
await Promise.all(
assortItemsToProcess.map((assortItemWithChildren) =>
this.createOffersFromAssort(assortItemWithChildren, replacingExpiredOffers, this.ragfairConfig.dynamic),
));
),
);
}
/**
@ -389,20 +362,17 @@ export class RagfairOfferGenerator
assortItemWithChildren: Item[],
isExpiredOffer: boolean,
config: Dynamic,
): Promise<void>
{
): Promise<void> {
const itemDetails = this.itemHelper.getItem(assortItemWithChildren[0]._tpl);
const isPreset = this.presetHelper.isPreset(assortItemWithChildren[0].upd.sptPresetId);
// Only perform checks on newly generated items, skip expired items being refreshed
if (!(isExpiredOffer || this.ragfairServerHelper.isItemValidRagfairItem(itemDetails)))
{
if (!(isExpiredOffer || this.ragfairServerHelper.isItemValidRagfairItem(itemDetails))) {
return;
}
// Armor presets can hold plates above the allowed flea level, remove if necessary
if (isPreset && this.ragfairConfig.dynamic.blacklist.enableBsgList)
{
if (isPreset && this.ragfairConfig.dynamic.blacklist.enableBsgList) {
this.removeBannedPlatesFromPreset(assortItemWithChildren, this.ragfairConfig.dynamic.blacklist.armorPlate);
}
@ -414,8 +384,7 @@ export class RagfairOfferGenerator
// Store all functions to create offers for this item and pass into Promise.all to run async
const assortSingleOfferProcesses = [];
for (let index = 0; index < offerCount; index++)
{
for (let index = 0; index < offerCount; index++) {
// Clone the item so we don't have shared references and generate new item IDs
const clonedAssort = this.cloner.clone(assortItemWithChildren);
this.itemHelper.reparentItemAndChildren(clonedAssort[0], clonedAssort);
@ -439,10 +408,8 @@ export class RagfairOfferGenerator
protected removeBannedPlatesFromPreset(
presetWithChildren: Item[],
plateSettings: IArmorPlateBlacklistSettings,
): boolean
{
if (!this.itemHelper.armorItemCanHoldMods(presetWithChildren[0]._tpl))
{
): boolean {
if (!this.itemHelper.armorItemCanHoldMods(presetWithChildren[0]._tpl)) {
// Cant hold armor inserts, skip
return false;
}
@ -450,24 +417,20 @@ export class RagfairOfferGenerator
const plateSlots = presetWithChildren.filter((item) =>
this.itemHelper.getRemovablePlateSlotIds().includes(item.slotId?.toLowerCase()),
);
if (plateSlots.length === 0)
{
if (plateSlots.length === 0) {
// Has no plate slots e.g. "front_plate", exit
return false;
}
let removedPlate = false;
for (const plateSlot of plateSlots)
{
for (const plateSlot of plateSlots) {
const plateDetails = this.itemHelper.getItem(plateSlot._tpl)[1];
if (plateSettings.ignoreSlots.includes(plateSlot.slotId.toLowerCase()))
{
if (plateSettings.ignoreSlots.includes(plateSlot.slotId.toLowerCase())) {
continue;
}
const plateArmorLevel = Number.parseInt(<string>plateDetails._props.armorClass) ?? 0;
if (plateArmorLevel > plateSettings.maxProtectionLevel)
{
if (plateArmorLevel > plateSettings.maxProtectionLevel) {
presetWithChildren.splice(presetWithChildren.indexOf(plateSlot), 1);
removedPlate = true;
}
@ -487,8 +450,7 @@ export class RagfairOfferGenerator
itemWithChildren: Item[],
isPreset: boolean,
itemDetails: [boolean, ITemplateItem],
): Promise<void>
{
): Promise<void> {
// Set stack size to random value
itemWithChildren[0].upd.StackObjectsCount = this.ragfairServerHelper.calculateDynamicStackCount(
itemWithChildren[0]._tpl,
@ -496,11 +458,11 @@ export class RagfairOfferGenerator
);
const isBarterOffer = this.randomUtil.getChance100(this.ragfairConfig.dynamic.barter.chancePercent);
const isPackOffer
= this.randomUtil.getChance100(this.ragfairConfig.dynamic.pack.chancePercent)
&& !isBarterOffer
&& itemWithChildren.length === 1
&& this.itemHelper.isOfBaseclasses(
const isPackOffer =
this.randomUtil.getChance100(this.ragfairConfig.dynamic.pack.chancePercent) &&
!isBarterOffer &&
itemWithChildren.length === 1 &&
this.itemHelper.isOfBaseclasses(
itemWithChildren[0]._tpl,
this.ragfairConfig.dynamic.pack.itemTypeWhitelist,
);
@ -508,27 +470,23 @@ export class RagfairOfferGenerator
const randomUserId = this.hashUtil.generate();
// Remove removable plates if % check passes
if (this.itemHelper.armorItemCanHoldMods(itemWithChildren[0]._tpl))
{
if (this.itemHelper.armorItemCanHoldMods(itemWithChildren[0]._tpl)) {
const armorConfig = this.ragfairConfig.dynamic.armor;
const shouldRemovePlates = this.randomUtil.getChance100(armorConfig.removeRemovablePlateChance);
if (shouldRemovePlates && this.itemHelper.armorItemHasRemovablePlateSlots(itemWithChildren[0]._tpl))
{
if (shouldRemovePlates && this.itemHelper.armorItemHasRemovablePlateSlots(itemWithChildren[0]._tpl)) {
const offerItemPlatesToRemove = itemWithChildren.filter((item) =>
armorConfig.plateSlotIdToRemovePool.includes(item.slotId?.toLowerCase()),
);
for (const plateItem of offerItemPlatesToRemove)
{
for (const plateItem of offerItemPlatesToRemove) {
itemWithChildren.splice(itemWithChildren.indexOf(plateItem), 1);
}
}
}
let barterScheme: IBarterScheme[];
if (isPackOffer)
{
if (isPackOffer) {
// Set pack size
const stackSize = this.randomUtil.getInt(
this.ragfairConfig.dynamic.pack.itemCountMin,
@ -538,15 +496,11 @@ export class RagfairOfferGenerator
// Don't randomise pack items
barterScheme = this.createCurrencyBarterScheme(itemWithChildren, isPackOffer, stackSize);
}
else if (isBarterOffer)
{
} else if (isBarterOffer) {
// Apply randomised properties
this.randomiseOfferItemUpdProperties(randomUserId, itemWithChildren, itemDetails[1]);
barterScheme = this.createBarterBarterScheme(itemWithChildren);
}
else
{
} else {
// Apply randomised properties
this.randomiseOfferItemUpdProperties(randomUserId, itemWithChildren, itemDetails[1]);
barterScheme = this.createCurrencyBarterScheme(itemWithChildren, isPackOffer);
@ -566,8 +520,7 @@ export class RagfairOfferGenerator
* Generate trader offers on flea using the traders assort data
* @param traderID Trader to generate offers for
*/
public generateFleaOffersForTrader(traderID: string): void
{
public generateFleaOffersForTrader(traderID: string): void {
// Purge
this.ragfairOfferService.removeAllOffersByTrader(traderID);
@ -576,8 +529,7 @@ export class RagfairOfferGenerator
const assorts = trader.assort;
// Trader assorts / assort items are missing
if (!assorts?.items?.length)
{
if (!assorts?.items?.length) {
this.logger.error(
this.localisationService.getText(
"ragfair-no_trader_assorts_cant_generate_flea_offers",
@ -588,28 +540,23 @@ export class RagfairOfferGenerator
}
const blacklist = this.ragfairConfig.dynamic.blacklist;
for (const item of assorts.items)
{
for (const item of assorts.items) {
// We only want to process 'base/root' items, no children
if (item.slotId !== "hideout")
{
if (item.slotId !== "hideout") {
// skip mod items
continue;
}
// Run blacklist check on trader offers
if (blacklist.traderItems)
{
if (blacklist.traderItems) {
const itemDetails = this.itemHelper.getItem(item._tpl);
if (!itemDetails[0])
{
if (!itemDetails[0]) {
this.logger.warning(this.localisationService.getText("ragfair-tpl_not_a_valid_item", item._tpl));
continue;
}
// Don't include items that BSG has blacklisted from flea
if (blacklist.enableBsgList && !itemDetails[1]._props.CanSellOnRagfair)
{
if (blacklist.enableBsgList && !itemDetails[1]._props.CanSellOnRagfair) {
continue;
}
}
@ -620,8 +567,7 @@ export class RagfairOfferGenerator
: [...[item], ...this.itemHelper.findAndReturnChildrenByAssort(item._id, assorts.items)];
const barterScheme = assorts.barter_scheme[item._id];
if (!barterScheme)
{
if (!barterScheme) {
this.logger.warning(
this.localisationService.getText("ragfair-missing_barter_scheme", {
itemId: item._id,
@ -649,23 +595,19 @@ export class RagfairOfferGenerator
* @param itemWithMods Item and mods, get condition of first item (only first array item is modified)
* @param itemDetails db details of first item
*/
protected randomiseOfferItemUpdProperties(userID: string, itemWithMods: Item[], itemDetails: ITemplateItem): void
{
protected randomiseOfferItemUpdProperties(userID: string, itemWithMods: Item[], itemDetails: ITemplateItem): void {
// Add any missing properties to first item in array
this.addMissingConditions(itemWithMods[0]);
if (!(this.profileHelper.isPlayer(userID) || this.ragfairServerHelper.isTrader(userID)))
{
if (!(this.profileHelper.isPlayer(userID) || this.ragfairServerHelper.isTrader(userID))) {
const parentId = this.getDynamicConditionIdForTpl(itemDetails._id);
if (!parentId)
{
if (!parentId) {
// No condition details found, don't proceed with modifying item conditions
return;
}
// Roll random chance to randomise item condition
if (this.randomUtil.getChance100(this.ragfairConfig.dynamic.condition[parentId].conditionChance * 100))
{
if (this.randomUtil.getChance100(this.ragfairConfig.dynamic.condition[parentId].conditionChance * 100)) {
this.randomiseItemCondition(parentId, itemWithMods, itemDetails);
}
}
@ -676,14 +618,11 @@ export class RagfairOfferGenerator
* @param tpl Item to look for matching condition object
* @returns condition id
*/
protected getDynamicConditionIdForTpl(tpl: string): string | undefined
{
protected getDynamicConditionIdForTpl(tpl: string): string | undefined {
// Get keys from condition config dictionary
const configConditions = Object.keys(this.ragfairConfig.dynamic.condition);
for (const baseClass of configConditions)
{
if (this.itemHelper.isOfBaseclass(tpl, baseClass))
{
for (const baseClass of configConditions) {
if (this.itemHelper.isOfBaseclass(tpl, baseClass)) {
return baseClass;
}
}
@ -701,8 +640,7 @@ export class RagfairOfferGenerator
conditionSettingsId: string,
itemWithMods: Item[],
itemDetails: ITemplateItem,
): void
{
): void {
const rootItem = itemWithMods[0];
const itemConditionValues: Condition = this.ragfairConfig.dynamic.condition[conditionSettingsId];
@ -714,18 +652,16 @@ export class RagfairOfferGenerator
// Randomise armor + plates + armor related things
if (
this.itemHelper.armorItemCanHoldMods(rootItem._tpl)
|| this.itemHelper.isOfBaseclasses(rootItem._tpl, [BaseClasses.ARMOR_PLATE, BaseClasses.ARMORED_EQUIPMENT])
)
{
this.itemHelper.armorItemCanHoldMods(rootItem._tpl) ||
this.itemHelper.isOfBaseclasses(rootItem._tpl, [BaseClasses.ARMOR_PLATE, BaseClasses.ARMORED_EQUIPMENT])
) {
this.randomiseArmorDurabilityValues(itemWithMods, currentMultiplier, maxMultiplier);
// Add hits to visor
const visorMod = itemWithMods.find(
(item) => item.parentId === BaseClasses.ARMORED_EQUIPMENT && item.slotId === "mod_equipment_000",
);
if (this.randomUtil.getChance100(25) && visorMod)
{
if (this.randomUtil.getChance100(25) && visorMod) {
this.itemHelper.addUpdObjectToItem(visorMod);
visorMod.upd.FaceShield = { Hits: this.randomUtil.getInt(1, 3) };
@ -735,48 +671,42 @@ export class RagfairOfferGenerator
}
// Randomise Weapons
if (this.itemHelper.isOfBaseclass(itemDetails._id, BaseClasses.WEAPON))
{
if (this.itemHelper.isOfBaseclass(itemDetails._id, BaseClasses.WEAPON)) {
this.randomiseWeaponDurability(itemWithMods[0], itemDetails, maxMultiplier, currentMultiplier);
return;
}
if (rootItem.upd.MedKit)
{
if (rootItem.upd.MedKit) {
// Randomize health
rootItem.upd.MedKit.HpResource = Math.round(rootItem.upd.MedKit.HpResource * maxMultiplier) || 1;
return;
}
if (rootItem.upd.Key && itemDetails._props.MaximumNumberOfUsage > 1)
{
if (rootItem.upd.Key && itemDetails._props.MaximumNumberOfUsage > 1) {
// Randomize key uses
rootItem.upd.Key.NumberOfUsages
= Math.round(itemDetails._props.MaximumNumberOfUsage * (1 - maxMultiplier)) || 0;
rootItem.upd.Key.NumberOfUsages =
Math.round(itemDetails._props.MaximumNumberOfUsage * (1 - maxMultiplier)) || 0;
return;
}
if (rootItem.upd.FoodDrink)
{
if (rootItem.upd.FoodDrink) {
// randomize food/drink value
rootItem.upd.FoodDrink.HpPercent = Math.round(itemDetails._props.MaxResource * maxMultiplier) || 1;
return;
}
if (rootItem.upd.RepairKit)
{
if (rootItem.upd.RepairKit) {
// randomize repair kit (armor/weapon) uses
rootItem.upd.RepairKit.Resource = Math.round(itemDetails._props.MaxRepairResource * maxMultiplier) || 1;
return;
}
if (this.itemHelper.isOfBaseclass(itemDetails._id, BaseClasses.FUEL))
{
if (this.itemHelper.isOfBaseclass(itemDetails._id, BaseClasses.FUEL)) {
const totalCapacity = itemDetails._props.MaxResource;
const remainingFuel = Math.round(totalCapacity * maxMultiplier);
rootItem.upd.Resource = { UnitsConsumed: totalCapacity - remainingFuel, Value: remainingFuel };
@ -795,14 +725,11 @@ export class RagfairOfferGenerator
itemDbDetails: ITemplateItem,
maxMultiplier: number,
currentMultiplier: number,
): void
{
): void {
// Max
const baseMaxDurability = itemDbDetails._props.MaxDurability;
const lowestMaxDurability = this.randomUtil.getFloat(maxMultiplier, 1) * baseMaxDurability;
const chosenMaxDurability = Math.round(
this.randomUtil.getFloat(lowestMaxDurability, baseMaxDurability),
);
const chosenMaxDurability = Math.round(this.randomUtil.getFloat(lowestMaxDurability, baseMaxDurability));
// Current
const lowestCurrentDurability = this.randomUtil.getFloat(currentMultiplier, 1) * chosenMaxDurability;
@ -824,18 +751,14 @@ export class RagfairOfferGenerator
armorWithMods: Item[],
currentMultiplier: number,
maxMultiplier: number,
): void
{
for (const armorItem of armorWithMods)
{
): void {
for (const armorItem of armorWithMods) {
const itemDbDetails = this.itemHelper.getItem(armorItem._tpl)[1];
if (Number.parseInt(<string>itemDbDetails._props.armorClass) > 1)
{
if (Number.parseInt(<string>itemDbDetails._props.armorClass) > 1) {
this.itemHelper.addUpdObjectToItem(armorItem);
const baseMaxDurability = itemDbDetails._props.MaxDurability;
const lowestMaxDurability
= this.randomUtil.getFloat(maxMultiplier, 1) * baseMaxDurability;
const lowestMaxDurability = this.randomUtil.getFloat(maxMultiplier, 1) * baseMaxDurability;
const chosenMaxDurability = Math.round(
this.randomUtil.getFloat(lowestMaxDurability, baseMaxDurability),
);
@ -859,8 +782,7 @@ export class RagfairOfferGenerator
* HpResource for medical items
* @param item item to add conditions to
*/
protected addMissingConditions(item: Item): void
{
protected addMissingConditions(item: Item): void {
const props = this.itemHelper.getItem(item._tpl)[1]._props;
const isRepairable = "Durability" in props;
const isMedkit = "MaxHpResource" in props;
@ -868,37 +790,32 @@ export class RagfairOfferGenerator
const isConsumable = props.MaxResource > 1 && "foodUseTime" in props;
const isRepairKit = "MaxRepairResource" in props;
if (isRepairable && props.Durability > 0)
{
if (isRepairable && props.Durability > 0) {
item.upd.Repairable = { Durability: props.Durability, MaxDurability: props.Durability };
return;
}
if (isMedkit && props.MaxHpResource > 0)
{
if (isMedkit && props.MaxHpResource > 0) {
item.upd.MedKit = { HpResource: props.MaxHpResource };
return;
}
if (isKey)
{
if (isKey) {
item.upd.Key = { NumberOfUsages: 0 };
return;
}
// Food/drink
if (isConsumable)
{
if (isConsumable) {
item.upd.FoodDrink = { HpPercent: props.MaxResource };
return;
}
if (isRepairKit)
{
if (isRepairKit) {
item.upd.RepairKit = { Resource: props.MaxRepairResource };
}
}
@ -908,8 +825,7 @@ export class RagfairOfferGenerator
* @param offerItems Items for sale in offer
* @returns Barter scheme
*/
protected createBarterBarterScheme(offerItems: Item[]): IBarterScheme[]
{
protected createBarterBarterScheme(offerItems: Item[]): IBarterScheme[] {
// get flea price of item being sold
const priceOfItemOffer = this.ragfairPriceService.getDynamicOfferPriceForOffer(
offerItems,
@ -918,8 +834,7 @@ export class RagfairOfferGenerator
);
// Dont make items under a designated rouble value into barter offers
if (priceOfItemOffer < this.ragfairConfig.dynamic.barter.minRoubleCostToBecomeBarter)
{
if (priceOfItemOffer < this.ragfairConfig.dynamic.barter.minRoubleCostToBecomeBarter) {
return this.createCurrencyBarterScheme(offerItems, false);
}
@ -940,14 +855,13 @@ export class RagfairOfferGenerator
// Filter possible barters to items that match the price range + not itself
const filtered = fleaPrices.filter(
(x) =>
x.price >= desiredItemCost - offerCostVariance
&& x.price <= desiredItemCost + offerCostVariance
&& x.tpl !== offerItems[0]._tpl,
x.price >= desiredItemCost - offerCostVariance &&
x.price <= desiredItemCost + offerCostVariance &&
x.tpl !== offerItems[0]._tpl,
);
// No items on flea have a matching price, fall back to currency
if (filtered.length === 0)
{
if (filtered.length === 0) {
return this.createCurrencyBarterScheme(offerItems, false);
}
@ -961,11 +875,9 @@ export class RagfairOfferGenerator
* Get an array of flea prices + item tpl, cached in generator class inside `allowedFleaPriceItemsForBarter`
* @returns array with tpl/price values
*/
protected getFleaPricesAsArray(): { tpl: string, price: number }[]
{
protected getFleaPricesAsArray(): { tpl: string; price: number }[] {
// Generate if needed
if (!this.allowedFleaPriceItemsForBarter)
{
if (!this.allowedFleaPriceItemsForBarter) {
const fleaPrices = this.databaseService.getPrices();
// Only get prices for items that also exist in items.json
@ -993,11 +905,10 @@ export class RagfairOfferGenerator
offerWithChildren: Item[],
isPackOffer: boolean,
multipler = 1,
): IBarterScheme[]
{
): IBarterScheme[] {
const currency = this.ragfairServerHelper.getDynamicOfferCurrency();
const price = this.ragfairPriceService.getDynamicOfferPriceForOffer(offerWithChildren, currency, isPackOffer)
* multipler;
const price =
this.ragfairPriceService.getDynamicOfferPriceForOffer(offerWithChildren, currency, isPackOffer) * multipler;
return [{ count: price, _tpl: currency }];
}

Some files were not shown because too many files have changed in this diff Show More