update 3.9.0 with 3.8.1 changes (!289)

Co-authored-by: Refringe <me@refringe.com>
Co-authored-by: Dev <dev@dev.sp-tarkov.com>
Co-authored-by: Terkoiz <terkoiz@spt.dev>
Co-authored-by: Refringe <refringe@noreply.dev.sp-tarkov.com>
Co-authored-by: DrakiaXYZ <565558+TheDgtl@users.noreply.github.com>
Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/289
This commit is contained in:
chomp 2024-04-15 07:59:33 +00:00
parent dedb47eb14
commit 687436ab8b
118 changed files with 10185 additions and 8595 deletions

View File

@ -0,0 +1,58 @@
name: Run Code Linter
on:
push:
branches: '*'
pull_request:
branches: '*'
jobs:
biome:
runs-on: ubuntu-latest
container:
image: refringe/spt-build-node:1.0.7
steps:
- name: Clone
run: |
rm -rf /workspace/SPT-AKI/Build/server
git clone https://dev.sp-tarkov.com/SPT-AKI/Server.git --branch master /workspace/SPT-AKI/Build/server
cd /workspace/SPT-AKI/Build/server
git checkout ${GITHUB_SHA}
shell: bash
- name: Pull LFS Files
run: |
cd /workspace/SPT-AKI/Build/server
git lfs pull
git lfs ls-files
shell: bash
- name: Cache NPM Dependencies
id: cache-npm-dependencies
uses: actions/cache@v4
with:
path: /workspace/SPT-AKI/Build/server/project/node_modules
key: npm-dependencies-${{ hashFiles('/workspace/SPT-AKI/Build/server/project/package.json') }}
- name: Install NPM Dependencies
if: steps.cache-npm-dependencies.outputs.cache-hit != 'true'
run: |
cd /workspace/SPT-AKI/Build/server/project
rm -rf node_modules
npm install
shell: bash
- name: Run Linter
id: run-tests
run: |
cd /workspace/SPT-AKI/Build/server/project
npm run lint
shell: bash
- name: Fix Instructions
if: failure() && steps.run-tests.outcome == 'failure'
run: |
echo -e "Code linting has failed. The linter has been configured to look for coding errors, defects, and questionable patterns. Please look into resolving these errors. The linter may be able to resolve some of these issues automatically. You can launch the automatic fixer by running the following command from within the 'project' directory. Anything not resolved by running this command must be resolved manually.\n\nnpm run lint:fix\n"
echo -e "Consistency is professionalism.™"
shell: bash

View File

@ -0,0 +1,59 @@
name: Check Code Style
on:
push:
branches: '*'
pull_request:
branches: '*'
jobs:
dprint:
runs-on: ubuntu-latest
container:
image: refringe/spt-build-node:1.0.7
steps:
- name: Clone
run: |
rm -rf /workspace/SPT-AKI/Build/server
git clone https://dev.sp-tarkov.com/SPT-AKI/Server.git --branch master /workspace/SPT-AKI/Build/server
cd /workspace/SPT-AKI/Build/server
git checkout ${GITHUB_SHA}
shell: bash
- name: Pull LFS Files
run: |
cd /workspace/SPT-AKI/Build/server
git lfs pull
git lfs ls-files
shell: bash
- name: Cache NPM Dependencies
id: cache-npm-dependencies
uses: actions/cache@v4
with:
path: /workspace/SPT-AKI/Build/server/project/node_modules
key: npm-dependencies-${{ hashFiles('/workspace/SPT-AKI/Build/server/project/package.json') }}
- name: Install NPM Dependencies
if: steps.cache-npm-dependencies.outputs.cache-hit != 'true'
run: |
cd /workspace/SPT-AKI/Build/server/project
rm -rf node_modules
npm install
shell: bash
- name: Check Code Style
id: check-code-style
run: |
cd /workspace/SPT-AKI/Build/server/project
npm run style
shell: bash
- name: Fix Instructions
if: failure() && steps.check-code-style.outcome == 'failure'
run: |
echo -e "The code style check has failed. To fix this, please ensure your code adheres to the project's style guidelines. You can automatically format the project code by running the following command from within the 'project' directory.\n\nnpm run style:fix\n"
echo -e "To automatically format code on-save in your IDE, please install the recommended VSCode plugins listed within the 'project/Server.code-workspace' file.\n"
echo -e "Thank you for keeping our house clean. ♥"
shell: bash

View File

@ -0,0 +1,58 @@
name: Run Tests
on:
push:
branches: '*'
pull_request:
branches: '*'
jobs:
vitest:
runs-on: ubuntu-latest
container:
image: refringe/spt-build-node:1.0.7
steps:
- name: Clone
run: |
rm -rf /workspace/SPT-AKI/Build/server
git clone https://dev.sp-tarkov.com/SPT-AKI/Server.git --branch master /workspace/SPT-AKI/Build/server
cd /workspace/SPT-AKI/Build/server
git checkout ${GITHUB_SHA}
shell: bash
- name: Pull LFS Files
run: |
cd /workspace/SPT-AKI/Build/server
git lfs pull
git lfs ls-files
shell: bash
- name: Cache NPM Dependencies
id: cache-npm-dependencies
uses: actions/cache@v4
with:
path: /workspace/SPT-AKI/Build/server/project/node_modules
key: npm-dependencies-${{ hashFiles('/workspace/SPT-AKI/Build/server/project/package.json') }}
- name: Install NPM Dependencies
if: steps.cache-npm-dependencies.outputs.cache-hit != 'true'
run: |
cd /workspace/SPT-AKI/Build/server/project
rm -rf node_modules
npm install
shell: bash
- name: Run Tests
id: run-tests
run: |
cd /workspace/SPT-AKI/Build/server/project
npm run test
shell: bash
- name: Fix Instructions
if: failure() && steps.run-tests.outcome == 'failure'
run: |
echo -e "Automated tests have failed. This could point to an issue with the commited code, or an updated test that has yet to be updated. Please look into resolving these test failures. The testing suite has a GUI to aid in writing tests. You can launch this by running the following command from within the 'project' directory.\n\nnpm run test:ui\n"
echo -e "A test written today is a bug prevented tomorrow.™"
shell: bash

View File

@ -324,7 +324,9 @@
"5d08d21286f774736e7c94c3": 1, "5d08d21286f774736e7c94c3": 1,
"5c94bbff86f7747ee735c08f": 1 "5c94bbff86f7747ee735c08f": 1
}, },
"bosssanitar": {}, "bosssanitar": {
"5efde6b4f5448336730dbd61": 1
},
"bosstagilla": {}, "bosstagilla": {},
"bossknight": {}, "bossknight": {},
"bosszryachiy": {}, "bosszryachiy": {},
@ -387,8 +389,12 @@
"pmcbot": { "pmcbot": {
"60098ad7c2240c0fe85c570a": 2 "60098ad7c2240c0fe85c570a": 2
}, },
"arenafighterevent": {}, "arenafighterevent": {
"arenafighter": {}, "5734758f24597738025ee253": 1
},
"arenafighter": {
"5734758f24597738025ee253": 1
},
"crazyassaultevent": {}, "crazyassaultevent": {},
"assaultgroup": {}, "assaultgroup": {},
"gifter": {}, "gifter": {},

View File

@ -5,6 +5,7 @@
"serverName": "SPT Server", "serverName": "SPT Server",
"profileSaveIntervalSeconds": 15, "profileSaveIntervalSeconds": 15,
"sptFriendNickname": "SPT", "sptFriendNickname": "SPT",
"allowProfileWipe": true,
"bsgLogging": { "bsgLogging": {
"verbosity": 6, "verbosity": 6,
"sendToServer": false "sendToServer": false

View File

@ -5,5 +5,8 @@
"inRaid": 60, "inRaid": 60,
"outOfRaid": 10 "outOfRaid": 10
}, },
"expCraftAmount": 10 "expCraftAmount": 10,
"overrideCraftTimeSeconds": -1,
"overrideBuildTimeSeconds": -1,
"updateProfileHideoutWhenActiveWithinMinutes": 90
} }

View File

@ -1,6 +1,8 @@
{ {
"ip": "127.0.0.1", "ip": "127.0.0.1",
"port": 6969, "port": 6969,
"backendIp": "127.0.0.1",
"backendPort": 6969,
"webSocketPingDelayMs": 90000, "webSocketPingDelayMs": 90000,
"logRequests": true, "logRequests": true,
"serverImagePathOverride": {} "serverImagePathOverride": {}

View File

@ -11,8 +11,7 @@
"randomTime": false "randomTime": false
}, },
"save": { "save": {
"loot": true, "loot": true
"durability": true
}, },
"carExtracts": [ "carExtracts": [
"Dorms V-Ex", "Dorms V-Ex",

View File

@ -50,6 +50,7 @@
"5580239d4bdc2de7118b4583" "5580239d4bdc2de7118b4583"
], ],
"rewardItemBlacklist": [ "rewardItemBlacklist": [
"58ac60eb86f77401897560ff",
"5e997f0b86f7741ac73993e2", "5e997f0b86f7741ac73993e2",
"5b44abe986f774283e2e3512", "5b44abe986f774283e2e3512",
"5e99711486f7744bfc4af328", "5e99711486f7744bfc4af328",

View File

@ -1146,6 +1146,7 @@
"minFillStaticMagazinePercent": 50, "minFillStaticMagazinePercent": 50,
"allowDuplicateItemsInStaticContainers": true, "allowDuplicateItemsInStaticContainers": true,
"magazineLootHasAmmoChancePercent": 50, "magazineLootHasAmmoChancePercent": 50,
"staticMagazineLootHasAmmoChancePercent": 0,
"looseLootBlacklist": {}, "looseLootBlacklist": {},
"scavRaidTimeSettings": { "scavRaidTimeSettings": {
"settings": { "settings": {

View File

@ -157,7 +157,7 @@
"543be6564bdc2df4348b4568": 0, "543be6564bdc2df4348b4568": 0,
"5448ecbe4bdc2d60728b4568": 0, "5448ecbe4bdc2d60728b4568": 0,
"5671435f4bdc2d96058b4569": 0, "5671435f4bdc2d96058b4569": 0,
"543be5cb4bdc2deb348b4568": 3, "543be5cb4bdc2deb348b4568": 5,
"5448e53e4bdc2d60728b4567": 7 "5448e53e4bdc2d60728b4567": 7
}, },
"preventDuplicateOffersOfCategory": [ "preventDuplicateOffersOfCategory": [
@ -319,6 +319,7 @@
"left_side_plate": 75, "left_side_plate": 75,
"right_side_plate": 75 "right_side_plate": 75
}, },
"ammoMaxPenLimit": 20,
"blacklistSeasonalItems": true, "blacklistSeasonalItems": true,
"blacklist": [ "blacklist": [
"5c164d2286f774194c5e69fa", "5c164d2286f774194c5e69fa",
@ -342,7 +343,8 @@
"5a341c4086f77401f2541505", "5a341c4086f77401f2541505",
"5422acb9af1c889c16000029", "5422acb9af1c889c16000029",
"64d0b40fbe2eed70e254e2d4", "64d0b40fbe2eed70e254e2d4",
"5fc22d7c187fea44d52eda44" "5fc22d7c187fea44d52eda44",
"646372518610c40fc20204e8"
], ],
"coopExtractGift": { "coopExtractGift": {
"sendGift": true, "sendGift": true,
@ -351,7 +353,11 @@
"5da89b3a86f7742f9026cb83 0" "5da89b3a86f7742f9026cb83 0"
], ],
"giftExpiryHours": 168, "giftExpiryHours": 168,
"presetCount": { "weaponPresetCount": {
"min": 0,
"max": 0
},
"armorPresetCount": {
"min": 0, "min": 0,
"max": 0 "max": 0
}, },

View File

@ -2110,6 +2110,22 @@
"2": 0 "2": 0
}, },
"whitelist": [] "whitelist": []
},
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
}, },
"grenades": { "grenades": {
"weights": { "weights": {

View File

@ -2347,6 +2347,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 30, "0": 30,
@ -2355,6 +2363,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 8, "0": 8,

View File

@ -2188,7 +2188,7 @@
"Basuro", "Basuro",
"Bepis", "Bepis",
"Baliston", "Baliston",
"Pessin", "Crow",
"Aki-chan", "Aki-chan",
"Fin", "Fin",
"Gatsu66", "Gatsu66",
@ -2481,9 +2481,11 @@
"Brin", "Brin",
"Belette", "Belette",
"Agnotology", "Agnotology",
"All_Heil_Lord_Ppepe", "All_Heil_Lord_Pepe",
"ixcetotis", "ixcetotis",
"btdc00" "btdc00",
"Bnuy",
"Choccy"
], ],
"generation": { "generation": {
"items": { "items": {
@ -2508,11 +2510,27 @@
"2": 1 "2": 1
}, },
"whitelist": [] "whitelist": []
},
"food": {
"weights": {
"0": 6,
"1": 5,
"2": 2
},
"whitelist": []
},
"drink": {
"weights": {
"0": 6,
"1": 5,
"2": 1
},
"whitelist": []
}, },
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 2,
"1": 4, "1": 6,
"2": 5, "2": 5,
"3": 2, "3": 2,
"4": 1 "4": 1
@ -2988,9 +3006,9 @@
"5ac66d9b5acfc4001633997a": 5, "5ac66d9b5acfc4001633997a": 5,
"5ae08f0a5acfc408fb1398a1": 4, "5ae08f0a5acfc408fb1398a1": 4,
"5b0bbe4e5acfc40dc528a72d": 4, "5b0bbe4e5acfc40dc528a72d": 4,
"5ba26383d4351e00334c93d9": 5, "5ba26383d4351e00334c93d9": 4,
"5bb2475ed4351e00853264e3": 4, "5bb2475ed4351e00853264e3": 4,
"5bd70322209c4d00d7167b8f": 5, "5bd70322209c4d00d7167b8f": 4,
"5beed0f50db834001c062b12": 3, "5beed0f50db834001c062b12": 3,
"5bf3e03b0db834001d2c4a9c": 5, "5bf3e03b0db834001d2c4a9c": 5,
"5bf3e0490db83400196199af": 5, "5bf3e0490db83400196199af": 5,

View File

@ -2302,6 +2302,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2310,6 +2318,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2038,6 +2038,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2046,6 +2054,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2029,6 +2029,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2036,6 +2044,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2317,6 +2317,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2324,6 +2332,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2019,6 +2019,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2026,6 +2034,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2167,6 +2167,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2175,6 +2183,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2070,6 +2070,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2078,6 +2086,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2322,6 +2322,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2330,6 +2338,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2114,6 +2114,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2122,6 +2130,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2113,6 +2113,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2121,6 +2129,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -1946,6 +1946,22 @@
"healing": { "healing": {
"max": 2, "max": 2,
"min": 1 "min": 1
},
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
}, },
"looseLoot": { "looseLoot": {
"max": 3, "max": 3,

View File

@ -2024,6 +2024,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2032,6 +2040,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2365,6 +2365,22 @@
"2": 0 "2": 0
}, },
"whitelist": [] "whitelist": []
},
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
}, },
"grenades": { "grenades": {
"weights": { "weights": {

View File

@ -2391,6 +2391,22 @@
"2": 0 "2": 0
}, },
"whitelist": [] "whitelist": []
},
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
}, },
"grenades": { "grenades": {
"weights": { "weights": {

View File

@ -2254,6 +2254,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2261,6 +2269,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2053,6 +2053,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2061,6 +2069,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2085,6 +2085,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2093,6 +2101,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2253,6 +2253,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 9, "0": 9,
@ -2263,6 +2271,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2173,6 +2173,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2181,6 +2189,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2173,6 +2173,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2181,6 +2189,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2044,6 +2044,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2052,6 +2060,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2212,6 +2212,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2220,6 +2228,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2214,6 +2214,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2222,6 +2230,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2217,6 +2217,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2225,6 +2233,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2099,6 +2099,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2107,6 +2115,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2232,6 +2232,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2240,6 +2248,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2245,6 +2245,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2253,6 +2261,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2144,6 +2144,22 @@
"2": 0 "2": 0
}, },
"whitelist": [] "whitelist": []
},
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
}, },
"grenades": { "grenades": {
"weights": { "weights": {

View File

@ -2003,6 +2003,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2011,6 +2019,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2064,6 +2064,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2072,6 +2080,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2302,6 +2302,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2310,6 +2318,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2341,10 +2357,10 @@
}, },
"pocketLoot": { "pocketLoot": {
"weights": { "weights": {
"0": 3, "0": 10,
"1": 10, "1": 35,
"2": 3, "2": 3,
"3": 1, "3": 2,
"4": 1 "4": 1
}, },
"whitelist": [] "whitelist": []

View File

@ -2004,33 +2004,108 @@
], ],
"generation": { "generation": {
"items": { "items": {
"backpackLoot": {
"weights": {
"0": 1,
"1": 1,
"2": 2,
"3": 1,
"4": 1,
"5": 1,
"6": 1,
"7": 0
},
"whitelist": []
},
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"max": 1, "weights": {
"min": 0 "0": 1,
"1": 2,
"2": 0
},
"whitelist": []
},
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
}, },
"grenades": { "grenades": {
"max": 5, "weights": {
"min": 0 "0": 1,
"1": 2,
"2": 1,
"3": 1,
"4": 0,
"5": 0
},
"whitelist": []
}, },
"healing": { "healing": {
"max": 2, "weights": {
"min": 1 "0": 1,
"1": 2,
"2": 1
}, },
"looseLoot": { "whitelist": []
"max": 3,
"min": 0
}, },
"magazines": { "magazines": {
"max": 4, "weights": {
"min": 2 "0": 0,
"1": 0,
"2": 1,
"3": 3,
"4": 1
},
"whitelist": []
},
"pocketLoot": {
"weights": {
"0": 1,
"1": 6,
"2": 3,
"3": 1,
"4": 1
},
"whitelist": []
}, },
"specialItems": { "specialItems": {
"max": 0, "weights": {
"min": 0 "0": 1,
"1": 0
},
"whitelist": []
}, },
"stims": { "stims": {
"max": 1, "weights": {
"min": 0 "0": 2,
"1": 1,
"2": 0
},
"whitelist": []
},
"vestLoot": {
"weights": {
"0": 1,
"1": 1,
"2": 2,
"3": 1,
"4": 0,
"5": 0,
"6": 0
},
"whitelist": []
} }
} }
}, },

View File

@ -2119,6 +2119,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2127,6 +2135,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2085,6 +2085,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2093,6 +2101,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2101,6 +2101,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2109,6 +2117,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2112,6 +2112,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2120,6 +2128,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -2146,6 +2146,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": { "drugs": {
"weights": { "weights": {
"0": 1, "0": 1,
@ -2154,6 +2162,14 @@
}, },
"whitelist": [] "whitelist": []
}, },
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 1,

View File

@ -1938,6 +1938,22 @@
"drugs": { "drugs": {
"max": 1, "max": 1,
"min": 0 "min": 0
},
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
}, },
"grenades": { "grenades": {
"max": 5, "max": 5,

View File

@ -2185,7 +2185,7 @@
"Basuro", "Basuro",
"Bepis", "Bepis",
"Baliston", "Baliston",
"Pessin", "Crow",
"Aki-chan", "Aki-chan",
"Fin", "Fin",
"Gatsu66", "Gatsu66",
@ -2478,9 +2478,11 @@
"Brin", "Brin",
"Belette", "Belette",
"Agnotology", "Agnotology",
"All_Heil_Lord_Ppepe", "All_Heil_Lord_Pepe",
"ixcetotis", "ixcetotis",
"btdc00" "btdc00",
"Bnuy",
"Choccy"
], ],
"generation": { "generation": {
"items": { "items": {
@ -2505,11 +2507,27 @@
"2": 1 "2": 1
}, },
"whitelist": [] "whitelist": []
},
"food": {
"weights": {
"0": 6,
"1": 5,
"2": 2
},
"whitelist": []
},
"drink": {
"weights": {
"0": 6,
"1": 5,
"2": 1
},
"whitelist": []
}, },
"grenades": { "grenades": {
"weights": { "weights": {
"0": 1, "0": 2,
"1": 4, "1": 6,
"2": 5, "2": 5,
"3": 2, "3": 2,
"4": 1 "4": 1
@ -2985,9 +3003,9 @@
"5ac66d9b5acfc4001633997a": 5, "5ac66d9b5acfc4001633997a": 5,
"5ae08f0a5acfc408fb1398a1": 4, "5ae08f0a5acfc408fb1398a1": 4,
"5b0bbe4e5acfc40dc528a72d": 4, "5b0bbe4e5acfc40dc528a72d": 4,
"5ba26383d4351e00334c93d9": 5, "5ba26383d4351e00334c93d9": 4,
"5bb2475ed4351e00853264e3": 4, "5bb2475ed4351e00853264e3": 4,
"5bd70322209c4d00d7167b8f": 5, "5bd70322209c4d00d7167b8f": 4,
"5beed0f50db834001c062b12": 3, "5beed0f50db834001c062b12": 3,
"5bf3e03b0db834001d2c4a9c": 5, "5bf3e03b0db834001d2c4a9c": 5,
"5bf3e0490db83400196199af": 5, "5bf3e0490db83400196199af": 5,

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@
"dependencies": { "dependencies": {
"atomically": "~1.7", "atomically": "~1.7",
"buffer-crc32": "^1.0.0", "buffer-crc32": "^1.0.0",
"closest-match": "~1.3",
"date-fns": "~2.30", "date-fns": "~2.30",
"date-fns-tz": "~2.0", "date-fns-tz": "~2.0",
"i18n": "~0.15", "i18n": "~0.15",

View File

@ -4,6 +4,7 @@ import { BotController } from "@spt-aki/controllers/BotController";
import { IGenerateBotsRequestData } from "@spt-aki/models/eft/bot/IGenerateBotsRequestData"; import { IGenerateBotsRequestData } from "@spt-aki/models/eft/bot/IGenerateBotsRequestData";
import { IEmptyRequestData } from "@spt-aki/models/eft/common/IEmptyRequestData"; import { IEmptyRequestData } from "@spt-aki/models/eft/common/IEmptyRequestData";
import { IBotBase } from "@spt-aki/models/eft/common/tables/IBotBase"; import { IBotBase } from "@spt-aki/models/eft/common/tables/IBotBase";
import { Difficulties } from "@spt-aki/models/eft/common/tables/IBotType";
import { IGetBodyResponseData } from "@spt-aki/models/eft/httpResponse/IGetBodyResponseData"; import { IGetBodyResponseData } from "@spt-aki/models/eft/httpResponse/IGetBodyResponseData";
import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil"; import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil";
@ -44,6 +45,15 @@ export class BotCallbacks
return this.httpResponse.noBody(this.botController.getBotDifficulty(type, difficulty)); return this.httpResponse.noBody(this.botController.getBotDifficulty(type, difficulty));
} }
/**
* Handle singleplayer/settings/bot/difficulties
* @returns dictionary of every bot and its diffiulty settings
*/
public getAllBotDifficulties(url: string, info: IEmptyRequestData, sessionID: string): Record<string, Difficulties>
{
return this.httpResponse.noBody(this.botController.getAllBotDifficulties());
}
/** /**
* Handle client/game/bot/generate * Handle client/game/bot/generate
* @returns IGetBodyResponseData * @returns IGetBodyResponseData

View File

@ -65,15 +65,6 @@ export class InraidCallbacks
return this.httpResponse.noBody(this.inraidController.getInraidConfig().raidMenuSettings); return this.httpResponse.noBody(this.inraidController.getInraidConfig().raidMenuSettings);
} }
/**
* Handle singleplayer/settings/weapon/durability
* @returns
*/
public getWeaponDurability(): string
{
return this.httpResponse.noBody(this.inraidController.getInraidConfig().save.durability);
}
/** /**
* Handle singleplayer/airdrop/config * Handle singleplayer/airdrop/config
* @returns JSON as string * @returns JSON as string

View File

@ -83,7 +83,7 @@ export class BotController
/** /**
* Get bot difficulty settings * Get bot difficulty settings
* adjust PMC settings to ensure they engage the correct bot types * Adjust PMC settings to ensure they engage the correct bot types
* @param type what bot the server is requesting settings for * @param type what bot the server is requesting settings for
* @param diffLevel difficulty level server requested settings for * @param diffLevel difficulty level server requested settings for
* @returns Difficulty object * @returns Difficulty object
@ -104,7 +104,7 @@ export class BotController
// Check value chosen in pre-raid difficulty dropdown // Check value chosen in pre-raid difficulty dropdown
// If value is not 'asonline', change requested difficulty to be what was chosen in dropdown // If value is not 'asonline', change requested difficulty to be what was chosen in dropdown
const botDifficultyDropDownValue = raidConfig.wavesSettings.botDifficulty.toLowerCase(); const botDifficultyDropDownValue = raidConfig?.wavesSettings.botDifficulty.toLowerCase() ?? "asonline";
if (botDifficultyDropDownValue !== "asonline") if (botDifficultyDropDownValue !== "asonline")
{ {
difficulty = this.botDifficultyHelper.convertBotDifficultyDropdownToBotDifficulty( difficulty = this.botDifficultyHelper.convertBotDifficultyDropdownToBotDifficulty(
@ -140,6 +140,31 @@ export class BotController
return difficultySettings; return difficultySettings;
} }
public getAllBotDifficulties(): Record<string, any>
{
const result = {};
const botDb = this.databaseServer.getTables().bots.types;
const botTypes = Object.keys(botDb);
for (const botType of botTypes)
{
const botDetails = botDb[botType];
if (!botDetails.difficulty)
{
continue;
}
const botDifficulties = Object.keys(botDetails.difficulty);
result[botType] = {};
for (const difficulty of botDifficulties)
{
result[botType][difficulty] = this.getBotDifficulty(botType, difficulty);
}
}
return result;
}
/** /**
* Generate bot profiles and store in cache * Generate bot profiles and store in cache
* @param sessionId Session id * @param sessionId Session id

View File

@ -147,7 +147,7 @@ export class BuildController
this.removePlayerBuild(request.id, sessionID); this.removePlayerBuild(request.id, sessionID);
} }
protected removePlayerBuild(id: string, sessionID: string): void protected removePlayerBuild(idToRemove: string, sessionID: string): void
{ {
const profile = this.saveServer.getProfile(sessionID); const profile = this.saveServer.getProfile(sessionID);
const weaponBuilds = profile.userbuilds.weaponBuilds; const weaponBuilds = profile.userbuilds.weaponBuilds;
@ -155,7 +155,7 @@ export class BuildController
const magazineBuilds = profile.userbuilds.magazineBuilds; const magazineBuilds = profile.userbuilds.magazineBuilds;
// Check for id in weapon array first // Check for id in weapon array first
const matchingWeaponBuild = weaponBuilds.find((x) => x.Id === id); const matchingWeaponBuild = weaponBuilds.find((weaponBuild) => weaponBuild.Id === idToRemove);
if (matchingWeaponBuild) if (matchingWeaponBuild)
{ {
weaponBuilds.splice(weaponBuilds.indexOf(matchingWeaponBuild), 1); weaponBuilds.splice(weaponBuilds.indexOf(matchingWeaponBuild), 1);
@ -164,7 +164,7 @@ export class BuildController
} }
// Id not found in weapons, try equipment // Id not found in weapons, try equipment
const matchingEquipmentBuild = equipmentBuilds.find((x) => x.Id === id); const matchingEquipmentBuild = equipmentBuilds.find((equipmentBuild) => equipmentBuild.Id === idToRemove);
if (matchingEquipmentBuild) if (matchingEquipmentBuild)
{ {
equipmentBuilds.splice(equipmentBuilds.indexOf(matchingEquipmentBuild), 1); equipmentBuilds.splice(equipmentBuilds.indexOf(matchingEquipmentBuild), 1);
@ -173,7 +173,7 @@ export class BuildController
} }
// Id not found in weapons/equipment, try mags // Id not found in weapons/equipment, try mags
const matchingMagazineBuild = magazineBuilds.find((x) => x.Id === id); const matchingMagazineBuild = magazineBuilds.find((magBuild) => magBuild.Id === idToRemove);
if (matchingMagazineBuild) if (matchingMagazineBuild)
{ {
magazineBuilds.splice(magazineBuilds.indexOf(matchingMagazineBuild), 1); magazineBuilds.splice(magazineBuilds.indexOf(matchingMagazineBuild), 1);
@ -182,7 +182,9 @@ export class BuildController
} }
// Not found in weapons,equipment or magazines, not good // Not found in weapons,equipment or magazines, not good
this.logger.error(`Unable to delete preset, cannot find ${id} in weapon, equipment or magazine presets`); this.logger.error(
`Unable to delete preset, cannot find ${idToRemove} in weapon, equipment or magazine presets`,
);
} }
/** /**

View File

@ -39,6 +39,7 @@ import { GiftService } from "@spt-aki/services/GiftService";
import { ItemBaseClassService } from "@spt-aki/services/ItemBaseClassService"; import { ItemBaseClassService } from "@spt-aki/services/ItemBaseClassService";
import { LocalisationService } from "@spt-aki/services/LocalisationService"; import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { OpenZoneService } from "@spt-aki/services/OpenZoneService"; import { OpenZoneService } from "@spt-aki/services/OpenZoneService";
import { ProfileActivityService } from "@spt-aki/services/ProfileActivityService";
import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService"; import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService";
import { RaidTimeAdjustmentService } from "@spt-aki/services/RaidTimeAdjustmentService"; import { RaidTimeAdjustmentService } from "@spt-aki/services/RaidTimeAdjustmentService";
import { SeasonalEventService } from "@spt-aki/services/SeasonalEventService"; import { SeasonalEventService } from "@spt-aki/services/SeasonalEventService";
@ -78,6 +79,7 @@ export class GameController
@inject("ItemBaseClassService") protected itemBaseClassService: ItemBaseClassService, @inject("ItemBaseClassService") protected itemBaseClassService: ItemBaseClassService,
@inject("GiftService") protected giftService: GiftService, @inject("GiftService") protected giftService: GiftService,
@inject("RaidTimeAdjustmentService") protected raidTimeAdjustmentService: RaidTimeAdjustmentService, @inject("RaidTimeAdjustmentService") protected raidTimeAdjustmentService: RaidTimeAdjustmentService,
@inject("ProfileActivityService") protected profileActivityService: ProfileActivityService,
@inject("ApplicationContext") protected applicationContext: ApplicationContext, @inject("ApplicationContext") protected applicationContext: ApplicationContext,
@inject("ConfigServer") protected configServer: ConfigServer, @inject("ConfigServer") protected configServer: ConfigServer,
) )
@ -109,6 +111,8 @@ export class GameController
// Store client start time in app context // Store client start time in app context
this.applicationContext.addValue(ContextVariableType.CLIENT_START_TIMESTAMP, startTimeStampMS); this.applicationContext.addValue(ContextVariableType.CLIENT_START_TIMESTAMP, startTimeStampMS);
this.profileActivityService.setActivityTimestamp(sessionID);
if (this.coreConfig.fixes.fixShotgunDispersion) if (this.coreConfig.fixes.fixShotgunDispersion)
{ {
this.fixShotgunDispersions(); this.fixShotgunDispersions();
@ -203,12 +207,16 @@ export class GameController
this.hideoutHelper.setHideoutImprovementsToCompleted(pmcProfile); this.hideoutHelper.setHideoutImprovementsToCompleted(pmcProfile);
this.hideoutHelper.unlockHideoutWallInProfile(pmcProfile); this.hideoutHelper.unlockHideoutWallInProfile(pmcProfile);
this.profileFixerService.addMissingIdsToBonuses(pmcProfile); this.profileFixerService.addMissingIdsToBonuses(pmcProfile);
this.profileFixerService.fixBitcoinProductionTime(pmcProfile);
} }
this.logProfileDetails(fullProfile); this.logProfileDetails(fullProfile);
this.adjustLabsRaiderSpawnRate(); this.adjustLabsRaiderSpawnRate();
this.adjustHideoutCraftTimes();
this.adjustHideoutBuildTimes();
this.removePraporTestMessage(); this.removePraporTestMessage();
this.saveActiveModsToProfile(fullProfile); this.saveActiveModsToProfile(fullProfile);
@ -240,6 +248,46 @@ export class GameController
} }
} }
protected adjustHideoutCraftTimes(): void
{
const craftTimeOverrideSeconds = this.hideoutConfig.overrideCraftTimeSeconds;
if (craftTimeOverrideSeconds === -1)
{
return;
}
for (const craft of this.databaseServer.getTables().hideout.production)
{
// Only adjust crafts ABOVE the override
if (craft.productionTime > craftTimeOverrideSeconds)
{
craft.productionTime = craftTimeOverrideSeconds;
}
}
}
protected adjustHideoutBuildTimes(): void
{
const craftTimeOverrideSeconds = this.hideoutConfig.overrideBuildTimeSeconds;
if (craftTimeOverrideSeconds === -1)
{
return;
}
for (const area of this.databaseServer.getTables().hideout.areas)
{
for (const stageKey of Object.keys(area.stages))
{
const stage = area.stages[stageKey];
// Only adjust crafts ABOVE the override
if (stage.constructionTime > craftTimeOverrideSeconds)
{
stage.constructionTime = craftTimeOverrideSeconds;
}
}
}
}
protected adjustLocationBotValues(): void protected adjustLocationBotValues(): void
{ {
const mapsDb = this.databaseServer.getTables().locations; const mapsDb = this.databaseServer.getTables().locations;
@ -460,6 +508,7 @@ export class GameController
*/ */
public getKeepAlive(sessionId: string): IGameKeepAliveResponse public getKeepAlive(sessionId: string): IGameKeepAliveResponse
{ {
this.profileActivityService.setActivityTimestamp(sessionId);
return { msg: "OK", utc_time: new Date().getTime() / 1000 }; return { msg: "OK", utc_time: new Date().getTime() / 1000 };
} }

View File

@ -48,6 +48,7 @@ import { SaveServer } from "@spt-aki/servers/SaveServer";
import { FenceService } from "@spt-aki/services/FenceService"; import { FenceService } from "@spt-aki/services/FenceService";
import { LocalisationService } from "@spt-aki/services/LocalisationService"; import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { PlayerService } from "@spt-aki/services/PlayerService"; import { PlayerService } from "@spt-aki/services/PlayerService";
import { ProfileActivityService } from "@spt-aki/services/ProfileActivityService";
import { HashUtil } from "@spt-aki/utils/HashUtil"; import { HashUtil } from "@spt-aki/utils/HashUtil";
import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil"; import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil";
import { JsonUtil } from "@spt-aki/utils/JsonUtil"; import { JsonUtil } from "@spt-aki/utils/JsonUtil";
@ -79,6 +80,7 @@ export class HideoutController
@inject("HideoutHelper") protected hideoutHelper: HideoutHelper, @inject("HideoutHelper") protected hideoutHelper: HideoutHelper,
@inject("ScavCaseRewardGenerator") protected scavCaseRewardGenerator: ScavCaseRewardGenerator, @inject("ScavCaseRewardGenerator") protected scavCaseRewardGenerator: ScavCaseRewardGenerator,
@inject("LocalisationService") protected localisationService: LocalisationService, @inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ProfileActivityService") protected profileActivityService: ProfileActivityService,
@inject("ConfigServer") protected configServer: ConfigServer, @inject("ConfigServer") protected configServer: ConfigServer,
@inject("JsonUtil") protected jsonUtil: JsonUtil, @inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("FenceService") protected fenceService: FenceService, @inject("FenceService") protected fenceService: FenceService,
@ -1323,7 +1325,13 @@ export class HideoutController
{ {
for (const sessionID in this.saveServer.getProfiles()) for (const sessionID in this.saveServer.getProfiles())
{ {
if ("Hideout" in this.saveServer.getProfile(sessionID).characters.pmc) if (
"Hideout" in this.saveServer.getProfile(sessionID).characters.pmc
&& this.profileActivityService.activeWithinLastMinutes(
sessionID,
this.hideoutConfig.updateProfileHideoutWhenActiveWithinMinutes,
)
)
{ {
this.hideoutHelper.updatePlayerHideout(sessionID); this.hideoutHelper.updatePlayerHideout(sessionID);
} }

View File

@ -196,7 +196,7 @@ export class InsuranceController
!this.itemHelper.isAttachmentAttached(item) !this.itemHelper.isAttachmentAttached(item)
); );
// Process all items that are not attached, attachments. Those are handled separately, by value. // Process all items that are not attached, attachments; those are handled separately, by value.
if (hasRegularItems) if (hasRegularItems)
{ {
this.processRegularItems(insured, toDelete, parentAttachmentsMap); this.processRegularItems(insured, toDelete, parentAttachmentsMap);

View File

@ -172,8 +172,18 @@ export class LauncherController
return sessionID; return sessionID;
} }
/**
* Handle launcher requesting profile be wiped
* @param info IRegisterData
* @returns Session id
*/
public wipe(info: IRegisterData): string public wipe(info: IRegisterData): string
{ {
if (!this.coreConfig.allowProfileWipe)
{
return;
}
const sessionID = this.login(info); const sessionID = this.login(info);
if (sessionID) if (sessionID)

View File

@ -213,6 +213,7 @@ import { OpenZoneService } from "@spt-aki/services/OpenZoneService";
import { PaymentService } from "@spt-aki/services/PaymentService"; import { PaymentService } from "@spt-aki/services/PaymentService";
import { PlayerService } from "@spt-aki/services/PlayerService"; import { PlayerService } from "@spt-aki/services/PlayerService";
import { PmcChatResponseService } from "@spt-aki/services/PmcChatResponseService"; import { PmcChatResponseService } from "@spt-aki/services/PmcChatResponseService";
import { ProfileActivityService } from "@spt-aki/services/ProfileActivityService";
import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService"; import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService";
import { ProfileSnapshotService } from "@spt-aki/services/ProfileSnapshotService"; import { ProfileSnapshotService } from "@spt-aki/services/ProfileSnapshotService";
import { RagfairCategoriesService } from "@spt-aki/services/RagfairCategoriesService"; import { RagfairCategoriesService } from "@spt-aki/services/RagfairCategoriesService";
@ -596,7 +597,7 @@ export class Container
lifecycle: Lifecycle.Singleton, lifecycle: Lifecycle.Singleton,
}); });
// SptCommands // SptCommands
depContainer.register<GiveSptCommand>("GiveSptCommand", GiveSptCommand); depContainer.register<GiveSptCommand>("GiveSptCommand", GiveSptCommand, { lifecycle: Lifecycle.Singleton });
} }
private static registerLoaders(depContainer: DependencyContainer): void private static registerLoaders(depContainer: DependencyContainer): void
@ -747,6 +748,10 @@ export class Container
depContainer.register<GiftService>("GiftService", GiftService); depContainer.register<GiftService>("GiftService", GiftService);
depContainer.register<MailSendService>("MailSendService", MailSendService); depContainer.register<MailSendService>("MailSendService", MailSendService);
depContainer.register<RaidTimeAdjustmentService>("RaidTimeAdjustmentService", RaidTimeAdjustmentService); depContainer.register<RaidTimeAdjustmentService>("RaidTimeAdjustmentService", RaidTimeAdjustmentService);
depContainer.register<ProfileActivityService>("ProfileActivityService", ProfileActivityService, {
lifecycle: Lifecycle.Singleton,
});
} }
private static registerServers(depContainer: DependencyContainer): void private static registerServers(depContainer: DependencyContainer): void

View File

@ -258,13 +258,15 @@ export class BotGenerator
* @param botJsonTemplate x.json from database * @param botJsonTemplate x.json from database
* @param botGenerationDetails * @param botGenerationDetails
* @param botRole role of bot e.g. assault * @param botRole role of bot e.g. assault
* @param sessionId profile session id
* @returns Nickname for bot * @returns Nickname for bot
*/ */
// TODO: Remove sessionId parameter from this function in v3.9.0
protected generateBotNickname( protected generateBotNickname(
botJsonTemplate: IBotType, botJsonTemplate: IBotType,
botGenerationDetails: BotGenerationDetails, botGenerationDetails: BotGenerationDetails,
botRole: string, botRole: string,
sessionId: string, sessionId?: string, // @deprecated as of v3.8.1
): string ): string
{ {
const isPlayerScav = botGenerationDetails.isPlayerScav; const isPlayerScav = botGenerationDetails.isPlayerScav;
@ -273,9 +275,9 @@ export class BotGenerator
this.randomUtil.getArrayValue(botJsonTemplate.lastName) || "" this.randomUtil.getArrayValue(botJsonTemplate.lastName) || ""
}`; }`;
name = name.trim(); name = name.trim();
const playerProfile = this.profileHelper.getPmcProfile(sessionId);
// Simulate bot looking like a Player scav with the pmc name in brackets // Simulate bot looking like a player scav with the PMC name in brackets.
// E.g. "ScavName (PMCName)"
if (botRole === "assault" && this.randomUtil.getChance100(this.botConfig.chanceAssaultScavHasPlayerScavName)) if (botRole === "assault" && this.randomUtil.getChance100(this.botConfig.chanceAssaultScavHasPlayerScavName))
{ {
if (isPlayerScav) if (isPlayerScav)
@ -300,7 +302,7 @@ export class BotGenerator
if (botGenerationDetails.isPmc && botGenerationDetails.allPmcsHaveSameNameAsPlayer) if (botGenerationDetails.isPmc && botGenerationDetails.allPmcsHaveSameNameAsPlayer)
{ {
const prefix = this.localisationService.getRandomTextThatMatchesPartialKey("pmc-name_prefix_"); const prefix = this.localisationService.getRandomTextThatMatchesPartialKey("pmc-name_prefix_");
name = `${prefix} ${botGenerationDetails.playerName}`; name = `${prefix} ${name}`;
} }
return name; return name;

View File

@ -98,6 +98,10 @@ export class BotLootGenerator
); );
const healingItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.healing.weights)); const healingItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.healing.weights));
const drugItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.drugs.weights)); const drugItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.drugs.weights));
const foodItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.food.weights));
const drinkItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.drink.weights));
const stimItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.stims.weights)); const stimItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.stims.weights));
const grenadeCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.grenades.weights)); const grenadeCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.grenades.weights));
@ -145,6 +149,30 @@ export class BotLootGenerator
isPmc, isPmc,
); );
// Food
this.addLootFromPool(
this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.FOOD_ITEMS, botJsonTemplate),
containersBotHasAvailable,
foodItemCount,
botInventory,
botRole,
null,
0,
isPmc,
);
// Drink
this.addLootFromPool(
this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.DRINK_ITEMS, botJsonTemplate),
containersBotHasAvailable,
drinkItemCount,
botInventory,
botRole,
null,
0,
isPmc,
);
// Stims // Stims
this.addLootFromPool( this.addLootFromPool(
this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.STIM_ITEMS, botJsonTemplate), this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.STIM_ITEMS, botJsonTemplate),
@ -282,19 +310,6 @@ export class BotLootGenerator
true, true,
); );
// eTG regen stim
this.addLootFromPool(
// eslint-disable-next-line @typescript-eslint/naming-convention
{ "5c0e534186f7747fa1419867": 1 },
[EquipmentSlots.SECURED_CONTAINER],
2,
botInventory,
botRole,
null,
0,
true,
);
// AFAK // AFAK
this.addLootFromPool( this.addLootFromPool(
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention

View File

@ -95,6 +95,15 @@ export class FenceBaseAssortGenerator
upd: { StackObjectsCount: 9999999 }, upd: { StackObjectsCount: 9999999 },
}]; }];
// Ensure ammo is not above penetration limit value
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))
{ {
this.itemHelper.addCartridgesToAmmoBox(itemWithChildrenToAdd, rootItemDb); this.itemHelper.addCartridgesToAmmoBox(itemWithChildrenToAdd, rootItemDb);
@ -175,6 +184,53 @@ export class FenceBaseAssortGenerator
} }
} }
/**
* Check ammo in boxes + loose ammos has a penetration value above the configured value in trader.json / ammoMaxPenLimit
* @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
{
const ammoPenetrationPower = this.getAmmoPenetrationPower(rootItemDb);
if (ammoPenetrationPower === null)
{
this.logger.warning(`Ammo: ${rootItemDb._id} has no penetration value, skipping`);
return false;
}
return ammoPenetrationPower > this.traderConfig.fence.ammoMaxPenLimit;
}
/**
* Get the penetration power value of an ammo, works with ammo boxes and raw ammos
* @param rootItemDb Ammo box or ammo item from items.db
* @returns Penetration power of passed in item, null if it doesnt have a power
*/
protected getAmmoPenetrationPower(rootItemDb: ITemplateItem): number
{
if (this.itemHelper.isOfBaseclass(rootItemDb._id, BaseClasses.AMMO_BOX))
{
const ammoTplInBox = rootItemDb._props.StackSlots[0]._props.filters[0].Filter[0];
const ammoItemDb = this.itemHelper.getItem(ammoTplInBox);
if (!ammoItemDb[0])
{
this.logger.warning(`Ammo: ${ammoTplInBox} not an item, skipping`);
return null;
}
return ammoItemDb[1]._props.PenetrationPower;
}
// Plain old ammo, get its pen property
if (this.itemHelper.isOfBaseclass(rootItemDb._id, BaseClasses.AMMO))
{
return rootItemDb._props.PenetrationPower;
}
// Not an ammobox or ammo
return null;
}
protected getItemPrice(itemTpl: string, items: Item[]): number protected getItemPrice(itemTpl: string, items: Item[]): number
{ {
return this.itemHelper.isOfBaseclass(itemTpl, BaseClasses.AMMO_BOX) return this.itemHelper.isOfBaseclass(itemTpl, BaseClasses.AMMO_BOX)

View File

@ -871,7 +871,7 @@ export class LocationGenerator
// Create array with just magazine // Create array with just magazine
const magazineItem: Item[] = [{ _id: this.objectId.generate(), _tpl: chosenTpl }]; const magazineItem: Item[] = [{ _id: this.objectId.generate(), _tpl: chosenTpl }];
if (this.randomUtil.getChance100(this.locationConfig.magazineLootHasAmmoChancePercent)) if (this.randomUtil.getChance100(this.locationConfig.staticMagazineLootHasAmmoChancePercent))
{ {
// Add randomised amount of cartridges // Add randomised amount of cartridges
this.itemHelper.fillMagazineWithRandomCartridge( this.itemHelper.fillMagazineWithRandomCartridge(

View File

@ -99,6 +99,8 @@ export class LootGenerator
&& options.itemTypeWhitelist.includes(x[1]._parent) && options.itemTypeWhitelist.includes(x[1]._parent)
); );
if (items.length > 0)
{
const randomisedItemCount = this.randomUtil.getInt(options.itemCount.min, options.itemCount.max); const randomisedItemCount = this.randomUtil.getInt(options.itemCount.min, options.itemCount.max);
for (let index = 0; index < randomisedItemCount; index++) for (let index = 0; index < randomisedItemCount; index++)
{ {
@ -107,6 +109,7 @@ export class LootGenerator
index--; index--;
} }
} }
}
const globalDefaultPresets = Object.values(this.presetHelper.getDefaultPresets()); const globalDefaultPresets = Object.values(this.presetHelper.getDefaultPresets());
const itemBlacklistArray = Array.from(itemBlacklist); const itemBlacklistArray = Array.from(itemBlacklist);
@ -122,16 +125,24 @@ export class LootGenerator
this.itemHelper.isOfBaseclass(preset._encyclopedia, BaseClasses.WEAPON) this.itemHelper.isOfBaseclass(preset._encyclopedia, BaseClasses.WEAPON)
); );
if (weaponDefaultPresets.length > 0)
{
for (let index = 0; index < randomisedWeaponPresetCount; index++) for (let index = 0; index < randomisedWeaponPresetCount; index++)
{ {
if ( if (
!this.findAndAddRandomPresetToLoot(weaponDefaultPresets, itemTypeCounts, itemBlacklistArray, result) !this.findAndAddRandomPresetToLoot(
weaponDefaultPresets,
itemTypeCounts,
itemBlacklistArray,
result,
)
) )
{ {
index--; index--;
} }
} }
} }
}
// Filter default presets to just armors and then filter again by protection level // Filter default presets to just armors and then filter again by protection level
const randomisedArmorPresetCount = this.randomUtil.getInt( const randomisedArmorPresetCount = this.randomUtil.getInt(
@ -146,6 +157,9 @@ export class LootGenerator
const levelFilteredArmorPresets = armorDefaultPresets.filter((armor) => const levelFilteredArmorPresets = armorDefaultPresets.filter((armor) =>
this.armorIsDesiredProtectionLevel(armor, options) this.armorIsDesiredProtectionLevel(armor, options)
); );
if (levelFilteredArmorPresets.length > 0)
{
for (let index = 0; index < randomisedArmorPresetCount; index++) for (let index = 0; index < randomisedArmorPresetCount; index++)
{ {
if ( if (
@ -161,6 +175,7 @@ export class LootGenerator
} }
} }
} }
}
return result; return result;
} }
@ -307,7 +322,7 @@ export class LootGenerator
const randomPreset = this.randomUtil.getArrayValue(globalDefaultPresets); const randomPreset = this.randomUtil.getArrayValue(globalDefaultPresets);
if (!randomPreset?._encyclopedia) if (!randomPreset?._encyclopedia)
{ {
this.logger.debug(`Airdrop - preset with id: ${randomPreset._id} lacks encyclopedia property, skipping`); this.logger.debug(`Airdrop - preset with id: ${randomPreset?._id} lacks encyclopedia property, skipping`);
return false; return false;
} }

View File

@ -192,7 +192,7 @@ export class PMCLootGenerator
for (const itemToAdd of itemsToAdd) for (const itemToAdd of itemsToAdd)
{ {
// If pmc has override, use that. Otherwise use flea price // 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]; this.backpackLootPool[itemToAdd._id] = pmcPriceOverrides[itemToAdd._id];

View File

@ -688,19 +688,19 @@ export class BotGeneratorHelper
const itemDetails = this.itemHelper.getItem(itemTpl)[1]; const itemDetails = this.itemHelper.getItem(itemTpl)[1];
// if item to add is found in exclude filter, not allowed // if item to add is found in exclude filter, not allowed
if (excludedFilter.includes(itemDetails._parent)) if (excludedFilter?.includes(itemDetails._parent))
{ {
return false; return false;
} }
// If Filter array only contains 1 filter and its for basetype 'item', allow it // If Filter array only contains 1 filter and its for basetype 'item', allow it
if (filter.length === 1 && filter.includes(BaseClasses.ITEM)) if (filter?.length === 1 && filter.includes(BaseClasses.ITEM))
{ {
return true; return true;
} }
// If allowed filter has something in it + filter doesnt have basetype 'item', not allowed // If allowed filter has something in it + filter doesnt have basetype 'item', not allowed
if (filter.length > 0 && !filter.includes(itemDetails._parent)) if (filter?.length > 0 && !filter.includes(itemDetails._parent))
{ {
return false; return false;
} }

View File

@ -0,0 +1,75 @@
import { IChatCommand, ICommandoCommand } from "@spt-aki/helpers/Dialogue/Commando/IChatCommand";
import { IDialogueChatBot } from "@spt-aki/helpers/Dialogue/IDialogueChatBot";
import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest";
import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { MailSendService } from "@spt-aki/services/MailSendService";
export abstract class AbstractDialogueChatBot implements IDialogueChatBot
{
public constructor(
protected logger: ILogger,
protected mailSendService: MailSendService,
protected chatCommands: IChatCommand[] | ICommandoCommand[],
)
{
}
/**
* @deprecated As of v3.7.6. Use registerChatCommand.
*/
// TODO: v3.9.0 - Remove registerCommandoCommand method.
public registerCommandoCommand(chatCommand: IChatCommand | ICommandoCommand): void
{
this.registerChatCommand(chatCommand);
}
public registerChatCommand(chatCommand: IChatCommand | ICommandoCommand): void
{
if (this.chatCommands.some((cc) => cc.getCommandPrefix() === chatCommand.getCommandPrefix()))
{
throw new Error(
`The command "${chatCommand.getCommandPrefix()}" attempting to be registered already exists.`,
);
}
this.chatCommands.push(chatCommand);
}
public abstract getChatBot(): IUserDialogInfo;
protected abstract getUnrecognizedCommandMessage(): string;
public handleMessage(sessionId: string, request: ISendMessageRequest): string
{
if ((request.text ?? "").length === 0)
{
this.logger.error("Command came in as empty text! Invalid data!");
return request.dialogId;
}
const splitCommand = request.text.split(" ");
const commandos = this.chatCommands.filter((c) => c.getCommandPrefix() === splitCommand[0]);
if (commandos[0]?.getCommands().has(splitCommand[1]))
{
return commandos[0].handle(splitCommand[1], this.getChatBot(), sessionId, request);
}
if (splitCommand[0].toLowerCase() === "help")
{
const helpMessage = this.chatCommands.map((c) =>
`Available commands:\n\n${c.getCommandPrefix()}:\n\n${
Array.from(c.getCommands()).map((command) => c.getCommandHelp(command)).join("\n")
}`
).join("\n");
this.mailSendService.sendUserMessageToPlayer(sessionId, this.getChatBot(), helpMessage);
return request.dialogId;
}
this.mailSendService.sendUserMessageToPlayer(
sessionId,
this.getChatBot(),
this.getUnrecognizedCommandMessage(),
);
}
}

View File

@ -1,7 +1,12 @@
import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest";
import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile";
export interface ICommandoCommand /**
* @deprecated As of v3.7.6. Use IChatCommand. Will be removed in v3.9.0.
*/
// TODO: v3.9.0 - Remove ICommandoCommand.
export type ICommandoCommand = IChatCommand;
export interface IChatCommand
{ {
getCommandPrefix(): string; getCommandPrefix(): string;
getCommandHelp(command: string): string; getCommandHelp(command: string): string;

View File

@ -1,4 +1,4 @@
import { ICommandoCommand } from "@spt-aki/helpers/Dialogue/Commando/ICommandoCommand"; import { IChatCommand } from "@spt-aki/helpers/Dialogue/Commando/IChatCommand";
import { ISptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/ISptCommand"; import { ISptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/ISptCommand";
import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest";
import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile";
@ -8,7 +8,7 @@ import { ConfigServer } from "@spt-aki/servers/ConfigServer";
import { inject, injectAll, injectable } from "tsyringe"; import { inject, injectAll, injectable } from "tsyringe";
@injectable() @injectable()
export class SptCommandoCommands implements ICommandoCommand export class SptCommandoCommands implements IChatCommand
{ {
constructor( constructor(
@inject("ConfigServer") protected configServer: ConfigServer, @inject("ConfigServer") protected configServer: ConfigServer,
@ -31,7 +31,7 @@ export class SptCommandoCommands implements ICommandoCommand
{ {
if (this.sptCommands.some((c) => c.getCommand() === command.getCommand())) if (this.sptCommands.some((c) => c.getCommand() === command.getCommand()))
{ {
throw new Error(`The command ${command.getCommand()} being registered for SPT Commands already exists!`); throw new Error(`The command "${command.getCommand()}" attempting to be registered already exists.`);
} }
this.sptCommands.push(command); this.sptCommands.push(command);
} }

View File

@ -1,4 +1,5 @@
import { ISptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/ISptCommand"; import { ISptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/ISptCommand";
import { SavedCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/SavedCommand";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
import { PresetHelper } from "@spt-aki/helpers/PresetHelper"; import { PresetHelper } from "@spt-aki/helpers/PresetHelper";
import { Item } from "@spt-aki/models/eft/common/tables/IItem"; import { Item } from "@spt-aki/models/eft/common/tables/IItem";
@ -6,14 +7,30 @@ import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequ
import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile";
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses"; import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { LocaleService } from "@spt-aki/services/LocaleService";
import { MailSendService } from "@spt-aki/services/MailSendService"; import { MailSendService } from "@spt-aki/services/MailSendService";
import { HashUtil } from "@spt-aki/utils/HashUtil"; import { HashUtil } from "@spt-aki/utils/HashUtil";
import { JsonUtil } from "@spt-aki/utils/JsonUtil"; import { JsonUtil } from "@spt-aki/utils/JsonUtil";
import { closestMatch, distance } from "closest-match";
import { inject, injectable } from "tsyringe"; import { inject, injectable } from "tsyringe";
@injectable() @injectable()
export class GiveSptCommand implements ISptCommand export class GiveSptCommand implements ISptCommand
{ {
/**
* Regex to account for all these cases:
* spt give "item name" 5
* spt give templateId 5
* spt give en "item name in english" 5
* spt give es "nombre en español" 5
* spt give 5 <== this is the reply when the algo isn't sure about an item
*/
private static commandRegex = /^spt give (((([a-z]{2,5}) )?"(.+)"|\w+) )?([0-9]+)$/;
private static maxAllowedDistance = 1.5;
protected savedCommand: SavedCommand;
public constructor( public constructor(
@inject("WinstonLogger") protected logger: ILogger, @inject("WinstonLogger") protected logger: ILogger,
@inject("ItemHelper") protected itemHelper: ItemHelper, @inject("ItemHelper") protected itemHelper: ItemHelper,
@ -21,6 +38,8 @@ export class GiveSptCommand implements ISptCommand
@inject("JsonUtil") protected jsonUtil: JsonUtil, @inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("PresetHelper") protected presetHelper: PresetHelper, @inject("PresetHelper") protected presetHelper: PresetHelper,
@inject("MailSendService") protected mailSendService: MailSendService, @inject("MailSendService") protected mailSendService: MailSendService,
@inject("LocaleService") protected localeService: LocaleService,
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
) )
{ {
} }
@ -32,49 +51,135 @@ export class GiveSptCommand implements ISptCommand
public getCommandHelp(): string public getCommandHelp(): string
{ {
return "Usage: spt give tplId quantity"; return "spt give\n========\nSends items to the player through the message system.\n\n\tspt give [template ID] [quantity]\n\t\tEx: spt give 544fb25a4bdc2dfb738b4567 2\n\n\tspt give [\"item name\"] [quantity]\n\t\tEx: spt give \"pack of sugar\" 10\n\n\tspt give [locale] [\"item name\"] [quantity]\n\t\tEx: spt give fr \"figurine de chat\" 3";
} }
public performAction(commandHandler: IUserDialogInfo, sessionId: string, request: ISendMessageRequest): string public performAction(commandHandler: IUserDialogInfo, sessionId: string, request: ISendMessageRequest): string
{ {
const giveCommand = request.text.split(" "); if (!GiveSptCommand.commandRegex.test(request.text))
if (giveCommand[1] !== "give")
{
this.logger.error("Invalid action received for give command!");
return request.dialogId;
}
if (!giveCommand[2])
{ {
this.mailSendService.sendUserMessageToPlayer( this.mailSendService.sendUserMessageToPlayer(
sessionId, sessionId,
commandHandler, commandHandler,
"Invalid use of give command! Template ID is missing. Use \"Help\" for more info", "Invalid use of give command. Use \"help\" for more information.",
); );
return request.dialogId; return request.dialogId;
} }
const tplId = giveCommand[2];
if (!giveCommand[3]) const result = GiveSptCommand.commandRegex.exec(request.text);
let item: string;
let quantity: number;
let isItemName: boolean;
let locale: string;
// This is a reply to a give request previously made pending a reply
if (result[1] === undefined)
{
if (this.savedCommand === undefined)
{ {
this.mailSendService.sendUserMessageToPlayer( this.mailSendService.sendUserMessageToPlayer(
sessionId, sessionId,
commandHandler, commandHandler,
"Invalid use of give command! Quantity is missing. Use \"Help\" for more info", "Invalid use of give command. Use \"help\" for more information.",
); );
return request.dialogId; return request.dialogId;
} }
const quantity = giveCommand[3]; if (+result[6] > this.savedCommand.potentialItemNames.length)
if (Number.isNaN(+quantity))
{ {
this.mailSendService.sendUserMessageToPlayer( this.mailSendService.sendUserMessageToPlayer(
sessionId, sessionId,
commandHandler, commandHandler,
"Invalid use of give command! Quantity is not a valid integer. Use \"Help\" for more info", "Invalid selection. Outside of bounds! Use \"help\" for more information.",
); );
return request.dialogId; return request.dialogId;
} }
item = this.savedCommand.potentialItemNames[+result[6] - 1];
quantity = this.savedCommand.quantity;
locale = this.savedCommand.locale;
isItemName = true;
this.savedCommand = undefined;
}
else
{
// A new give request was entered, we need to ignore the old saved command
this.savedCommand = undefined;
isItemName = result[5] !== undefined;
item = result[5] ? result[5] : result[2];
quantity = +result[6];
if (isItemName)
{
locale = result[4] ? result[4] : this.localeService.getDesiredGameLocale();
if (!this.localeService.getServerSupportedLocales().includes(locale))
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
`Unknown locale "${locale}". Use \"help\" for more information.`,
);
return request.dialogId;
}
const localizedGlobal = this.databaseServer.getTables().locales.global[locale];
const closestItemsMatchedByName = closestMatch(
item.toLowerCase(),
this.itemHelper.getItems().filter((i) => i._type !== "Node").map((i) =>
localizedGlobal[`${i?._id} Name`]?.toLowerCase()
).filter((i) => i !== undefined),
true,
) as string[];
if (closestItemsMatchedByName === undefined || closestItemsMatchedByName.length === 0)
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
"That item could not be found. Please refine your request and try again.",
);
return request.dialogId;
}
if (closestItemsMatchedByName.length > 1)
{
let i = 1;
const slicedItems = closestItemsMatchedByName.slice(0, 10);
// max 10 item names and map them
const itemList = slicedItems.map((itemName) => `${i++}. ${itemName}`).join("\n");
this.savedCommand = new SavedCommand(quantity, slicedItems, locale);
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
`Could not find exact match. Closest matches are:\n\n${itemList}\n\nUse "spt give [number]" to select one.`,
);
return request.dialogId;
}
const dist = distance(item, closestItemsMatchedByName[0]);
if (dist > GiveSptCommand.maxAllowedDistance)
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
`Found a possible match for "${item}" but uncertain. Match: "${
closestItemsMatchedByName[0]
}". Please refine your request and try again.`,
);
return request.dialogId;
}
// Only one available so we get that entry and use it
item = closestItemsMatchedByName[0];
}
}
// If item is an item name, we need to search using that item name and the locale which one we want otherwise
// item is just the tplId.
const tplId = isItemName
? this.itemHelper.getItems().find((i) =>
this.databaseServer.getTables().locales.global[locale][`${i?._id} Name`]?.toLowerCase() === item
)._id
: item;
const checkedItem = this.itemHelper.getItem(tplId); const checkedItem = this.itemHelper.getItem(tplId);
if (!checkedItem[0]) if (!checkedItem[0])
@ -82,21 +187,25 @@ export class GiveSptCommand implements ISptCommand
this.mailSendService.sendUserMessageToPlayer( this.mailSendService.sendUserMessageToPlayer(
sessionId, sessionId,
commandHandler, commandHandler,
"Invalid template ID requested for give command. The item doesn't exist in the DB.", "That item could not be found. Please refine your request and try again.",
); );
return request.dialogId; return request.dialogId;
} }
const itemsToSend: Item[] = []; const itemsToSend: Item[] = [];
if (this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.WEAPON))
{
const preset = this.presetHelper.getDefaultPreset(checkedItem[1]._id); const preset = this.presetHelper.getDefaultPreset(checkedItem[1]._id);
if (preset) if (!preset)
{ {
for (let i = 0; i < +quantity; i++) this.mailSendService.sendUserMessageToPlayer(
{ sessionId,
// Make sure IDs are unique before adding to array - prevent collisions commandHandler,
const presetToSend = this.itemHelper.replaceIDs(preset._items); "That weapon template ID could not be found. Please refine your request and try again.",
itemsToSend.push(...presetToSend); );
return request.dialogId;
} }
itemsToSend.push(...this.jsonUtil.clone(preset._items));
} }
else if (this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.AMMO_BOX)) else if (this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.AMMO_BOX))
{ {
@ -115,13 +224,25 @@ export class GiveSptCommand implements ISptCommand
_tpl: checkedItem[1]._id, _tpl: checkedItem[1]._id,
upd: { StackObjectsCount: +quantity, SpawnedInSession: true }, upd: { StackObjectsCount: +quantity, SpawnedInSession: true },
}; };
try
{
itemsToSend.push(...this.itemHelper.splitStack(item)); itemsToSend.push(...this.itemHelper.splitStack(item));
} }
catch
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
"Too many items requested. Please lower the amount and try again.",
);
return request.dialogId;
}
}
// Flag the items as FiR // Flag the items as FiR
this.itemHelper.setFoundInRaid(itemsToSend); this.itemHelper.setFoundInRaid(itemsToSend);
this.mailSendService.sendSystemMessageToPlayer(sessionId, "Give command!", itemsToSend); this.mailSendService.sendSystemMessageToPlayer(sessionId, "SPT GIVE", itemsToSend);
return request.dialogId; return request.dialogId;
} }
} }

View File

@ -0,0 +1,6 @@
export class SavedCommand
{
public constructor(public quantity: number, public potentialItemNames: string[], public locale: string)
{
}
}

View File

@ -1,33 +1,22 @@
import { inject, injectAll, injectable } from "tsyringe"; import { inject, injectAll, injectable } from "tsyringe";
import { ICommandoCommand } from "@spt-aki/helpers/Dialogue/Commando/ICommandoCommand"; import { AbstractDialogueChatBot } from "@spt-aki/helpers/Dialogue/AbstractDialogueChatBot";
import { IDialogueChatBot } from "@spt-aki/helpers/Dialogue/IDialogueChatBot"; import { IChatCommand } from "@spt-aki/helpers/Dialogue/Commando/IChatCommand";
import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest";
import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile";
import { MemberCategory } from "@spt-aki/models/enums/MemberCategory"; import { MemberCategory } from "@spt-aki/models/enums/MemberCategory";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { MailSendService } from "@spt-aki/services/MailSendService"; import { MailSendService } from "@spt-aki/services/MailSendService";
@injectable() @injectable()
export class CommandoDialogueChatBot implements IDialogueChatBot export class CommandoDialogueChatBot extends AbstractDialogueChatBot
{ {
public constructor( public constructor(
@inject("WinstonLogger") protected logger: ILogger, @inject("WinstonLogger") logger: ILogger,
@inject("MailSendService") protected mailSendService: MailSendService, @inject("MailSendService") mailSendService: MailSendService,
@injectAll("CommandoCommand") protected commandoCommands: ICommandoCommand[], @injectAll("CommandoCommand") chatCommands: IChatCommand[],
) )
{ {
} super(logger, mailSendService, chatCommands);
public registerCommandoCommand(commandoCommand: ICommandoCommand): void
{
if (this.commandoCommands.some((cc) => cc.getCommandPrefix() === commandoCommand.getCommandPrefix()))
{
throw new Error(
`The commando command ${commandoCommand.getCommandPrefix()} being registered already exists!`,
);
}
this.commandoCommands.push(commandoCommand);
} }
public getChatBot(): IUserDialogInfo public getChatBot(): IUserDialogInfo
@ -39,37 +28,8 @@ export class CommandoDialogueChatBot implements IDialogueChatBot
}; };
} }
public handleMessage(sessionId: string, request: ISendMessageRequest): string protected getUnrecognizedCommandMessage(): string
{ {
if ((request.text ?? "").length === 0) return `I'm sorry soldier, I don't recognize the command you are trying to use! Type "help" to see available commands.`;
{
this.logger.error("Commando command came in as empty text! Invalid data!");
return request.dialogId;
}
const splitCommand = request.text.split(" ");
const commandos = this.commandoCommands.filter((c) => c.getCommandPrefix() === splitCommand[0]);
if (commandos[0]?.getCommands().has(splitCommand[1]))
{
return commandos[0].handle(splitCommand[1], this.getChatBot(), sessionId, request);
}
if (splitCommand[0].toLowerCase() === "help")
{
const helpMessage = this.commandoCommands.map((c) =>
`Help for ${c.getCommandPrefix()}:\n${
Array.from(c.getCommands()).map((command) => c.getCommandHelp(command)).join("\n")
}`
).join("\n");
this.mailSendService.sendUserMessageToPlayer(sessionId, this.getChatBot(), helpMessage);
return request.dialogId;
}
this.mailSendService.sendUserMessageToPlayer(
sessionId,
this.getChatBot(),
`Im sorry soldier, I dont recognize the command you are trying to use! Type "help" to see available commands.`,
);
} }
} }

View File

@ -595,7 +595,7 @@ export class HideoutHelper
* @param applyHideoutManagementBonus should the hideout mgmt bonus be appled to the calculation * @param applyHideoutManagementBonus should the hideout mgmt bonus be appled to the calculation
* @returns Items craft time with bonuses subtracted * @returns Items craft time with bonuses subtracted
*/ */
protected getAdjustedCraftTimeWithSkills( public getAdjustedCraftTimeWithSkills(
pmcData: IPmcData, pmcData: IPmcData,
recipeId: string, recipeId: string,
applyHideoutManagementBonus = false, applyHideoutManagementBonus = false,
@ -613,13 +613,19 @@ export class HideoutHelper
return undefined; return undefined;
} }
let timeReductionSeconds = 0;
// Bitcoin farm is excluded from crafting skill cooldown reduction
if (recipeId !== HideoutHelper.bitcoinFarm)
{
// Seconds to deduct from crafts total time // Seconds to deduct from crafts total time
let timeReductionSeconds = this.getSkillProductionTimeReduction( timeReductionSeconds += this.getSkillProductionTimeReduction(
pmcData, pmcData,
recipe.productionTime, recipe.productionTime,
SkillTypes.CRAFTING, SkillTypes.CRAFTING,
globalSkillsDb.Crafting.ProductionTimeReductionPerLevel, globalSkillsDb.Crafting.ProductionTimeReductionPerLevel,
); );
}
// Some crafts take into account hideout management, e.g. fuel, water/air filters // Some crafts take into account hideout management, e.g. fuel, water/air filters
if (applyHideoutManagementBonus) if (applyHideoutManagementBonus)

View File

@ -32,12 +32,12 @@ export class HttpServerHelper
} }
/** /**
* Combine ip and port into url * Combine ip and port into address
* @returns url * @returns url
*/ */
public buildUrl(): string public buildUrl(): string
{ {
return `${this.httpConfig.ip}:${this.httpConfig.port}`; return `${this.httpConfig.backendIp}:${this.httpConfig.backendPort}`;
} }
/** /**

View File

@ -429,19 +429,12 @@ export class ItemHelper
if (repairable.Durability > repairable.MaxDurability) if (repairable.Durability > repairable.MaxDurability)
{ {
this.logger.warning( this.logger.warning(
`Max durability: ${repairable.MaxDurability} for item id: ${item._id} was below Durability: ${repairable.Durability}, adjusting values to match`, `Max durability: ${repairable.MaxDurability} for item id: ${item._id} was below durability: ${repairable.Durability}, adjusting values to match`,
); );
repairable.MaxDurability = repairable.Durability; repairable.MaxDurability = repairable.Durability;
} }
// Armor // Attempt to get the max durability from _props. If not available, use Repairable max durability value instead.
if (itemDetails._props.armorClass)
{
return repairable.MaxDurability / itemDetails._props.MaxDurability;
}
// Weapon
// Get max dura from props, if it isnt there use repairable max dura value
const maxDurability = (itemDetails._props.MaxDurability) const maxDurability = (itemDetails._props.MaxDurability)
? itemDetails._props.MaxDurability ? itemDetails._props.MaxDurability
: repairable.MaxDurability; : repairable.MaxDurability;

View File

@ -85,6 +85,17 @@ export class PresetHelper
return id in this.databaseServer.getTables().globals.ItemPresets; return id in this.databaseServer.getTables().globals.ItemPresets;
} }
/**
* Checks to see if the preset is of the given base class.
* @param id The id of the preset
* @param baseClass The BaseClasses enum to check against
* @returns True if the preset is of the given base class, false otherwise
*/
public isPresetBaseClass(id: string, baseClass: BaseClasses): boolean
{
return this.isPreset(id) && this.itemHelper.isOfBaseclass(this.getPreset(id)._encyclopedia, baseClass);
}
public hasPreset(templateId: string): boolean public hasPreset(templateId: string): boolean
{ {
return templateId in this.lookup; return templateId in this.lookup;

View File

@ -731,7 +731,10 @@ export class QuestHelper
repeatableType.activeQuests repeatableType.activeQuests
).find((activeQuest) => activeQuest._id === failRequest.qid); ).find((activeQuest) => activeQuest._id === failRequest.qid);
if (matchingRepeatableQuest || quest) // Quest found and no repeatable found
if (quest && !matchingRepeatableQuest)
{
if (quest.failMessageText.trim().length > 0)
{ {
this.mailSendService.sendLocalisedNpcMessageToPlayer( this.mailSendService.sendLocalisedNpcMessageToPlayer(
sessionID, sessionID,
@ -742,6 +745,7 @@ export class QuestHelper
this.timeUtil.getHoursAsSeconds(this.questConfig.redeemTime), this.timeUtil.getHoursAsSeconds(this.questConfig.redeemTime),
); );
} }
}
output.profileChanges[sessionID].quests.push(...this.failedUnlocked(failRequest.qid, sessionID)); output.profileChanges[sessionID].quests.push(...this.failedUnlocked(failRequest.qid, sessionID));
} }

View File

@ -158,6 +158,14 @@ export class TradeHelper
); );
} }
// Check if trader has enough stock
if (itemPurchased.upd.StackObjectsCount < buyCount)
{
throw new Error(
`Unable to purchase ${buyCount} items, this would exceed the remaining stock left ${itemPurchased.upd.StackObjectsCount} from the traders assort: ${buyRequestData.tid} this refresh`,
);
}
// Decrement trader item count // Decrement trader item count
itemPurchased.upd.StackObjectsCount -= buyCount; itemPurchased.upd.StackObjectsCount -= buyCount;

View File

@ -130,6 +130,8 @@ export interface GenerationWeightingItems
grenades: GenerationData; grenades: GenerationData;
healing: GenerationData; healing: GenerationData;
drugs: GenerationData; drugs: GenerationData;
food: GenerationData;
drink: GenerationData;
stims: GenerationData; stims: GenerationData;
backpackLoot: GenerationData; backpackLoot: GenerationData;
pocketLoot: GenerationData; pocketLoot: GenerationData;

View File

@ -169,6 +169,7 @@ export interface IQuestReward
target?: string; target?: string;
items?: Item[]; items?: Item[];
loyaltyLevel?: number; loyaltyLevel?: number;
/** Hideout area id */
traderId?: string; traderId?: string;
unknown?: boolean; unknown?: boolean;
findInRaid?: boolean; findInRaid?: boolean;

View File

@ -1,5 +1,3 @@
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
export interface IBotLootCache export interface IBotLootCache
{ {
backpackLoot: Record<string, number>; backpackLoot: Record<string, number>;
@ -11,6 +9,8 @@ export interface IBotLootCache
specialItems: Record<string, number>; specialItems: Record<string, number>;
healingItems: Record<string, number>; healingItems: Record<string, number>;
drugItems: Record<string, number>; drugItems: Record<string, number>;
foodItems: Record<string, number>;
drinkItems: Record<string, number>;
stimItems: Record<string, number>; stimItems: Record<string, number>;
grenadeItems: Record<string, number>; grenadeItems: Record<string, number>;
} }
@ -27,4 +27,6 @@ export enum LootCacheType
DRUG_ITEMS = "DrugItems", DRUG_ITEMS = "DrugItems",
STIM_ITEMS = "StimItems", STIM_ITEMS = "StimItems",
GRENADE_ITEMS = "GrenadeItems", GRENADE_ITEMS = "GrenadeItems",
FOOD_ITEMS = "FoodItems",
DRINK_ITEMS = "DrinkItems",
} }

View File

@ -186,12 +186,9 @@ export interface IAdjustmentDetails
edit: Record<string, Record<string, number>>; edit: Record<string, Record<string, number>>;
} }
export interface IArmorPlateWeights export interface IArmorPlateWeights extends Record<string, any>
{ {
levelRange: MinMax; levelRange: MinMax;
frontPlateWeights: Record<string, number>;
backPlateWeights: Record<string, number>;
sidePlateWeights: Record<string, number>;
} }
export interface IRandomisedResourceDetails export interface IRandomisedResourceDetails

View File

@ -9,6 +9,7 @@ export interface ICoreConfig extends IBaseConfig
serverName: string; serverName: string;
profileSaveIntervalSeconds: number; profileSaveIntervalSeconds: number;
sptFriendNickname: string; sptFriendNickname: string;
allowProfileWipe: boolean;
bsgLogging: IBsgLogging; bsgLogging: IBsgLogging;
release: IRelease; release: IRelease;
fixes: IGameFixes; fixes: IGameFixes;

View File

@ -9,4 +9,8 @@ export interface IHideoutConfig extends IBaseConfig
runIntervalValues: IRunIntervalValues; runIntervalValues: IRunIntervalValues;
hoursForSkillCrafting: number; hoursForSkillCrafting: number;
expCraftAmount: number; expCraftAmount: number;
overrideCraftTimeSeconds: number;
overrideBuildTimeSeconds: number;
/** Only process a profiles hideout crafts when it has been active in the last x minutes */
updateProfileHideoutWhenActiveWithinMinutes: number;
} }

View File

@ -2,10 +2,14 @@ import { IBaseConfig } from "@spt-aki/models/spt/config/IBaseConfig";
export interface IHttpConfig extends IBaseConfig export interface IHttpConfig extends IBaseConfig
{ {
webSocketPingDelayMs: number;
kind: "aki-http"; kind: "aki-http";
/** Address used by webserver */
ip: string; ip: string;
port: number; port: number;
/** Address used by game client to connect to */
backendIp: string;
backendPort: string;
webSocketPingDelayMs: number;
logRequests: boolean; logRequests: boolean;
/** e.g. "Aki_Data/Server/images/traders/579dc571d53a0658a154fbec.png": "Aki_Data/Server/images/traders/NewTraderImage.png" */ /** e.g. "Aki_Data/Server/images/traders/579dc571d53a0658a154fbec.png": "Aki_Data/Server/images/traders/NewTraderImage.png" */
serverImagePathOverride: Record<string, string>; serverImagePathOverride: Record<string, string>;

View File

@ -42,5 +42,4 @@ export interface Save
{ {
/** Should loot gained from raid be saved */ /** Should loot gained from raid be saved */
loot: boolean; loot: boolean;
durability: boolean;
} }

View File

@ -36,8 +36,10 @@ export interface ILocationConfig extends IBaseConfig
/** How full must a random static magazine be %*/ /** How full must a random static magazine be %*/
minFillStaticMagazinePercent: number; minFillStaticMagazinePercent: number;
allowDuplicateItemsInStaticContainers: boolean; allowDuplicateItemsInStaticContainers: boolean;
/** Chance loose/static magazines have ammo in them */ /** Chance loose magazines have ammo in them TODO - rename to dynamicMagazineLootHasAmmoChancePercent */
magazineLootHasAmmoChancePercent: number; magazineLootHasAmmoChancePercent: number;
/** Chance static magazines have ammo in them */
staticMagazineLootHasAmmoChancePercent: number;
/** Key: map, value: loose loot ids to ignore */ /** Key: map, value: loose loot ids to ignore */
looseLootBlacklist: Record<string, string[]>; looseLootBlacklist: Record<string, string[]>;
/** Key: map, value: settings to control how long scav raids are*/ /** Key: map, value: settings to control how long scav raids are*/

View File

@ -46,6 +46,8 @@ export interface FenceConfig
presetSlotsToRemoveChancePercent: Record<string, number>; presetSlotsToRemoveChancePercent: Record<string, number>;
/** Block seasonal items from appearing when season is inactive */ /** Block seasonal items from appearing when season is inactive */
blacklistSeasonalItems: boolean; blacklistSeasonalItems: boolean;
/** Max pen value allowed to be listed on flea - affects ammo + ammo boxes */
ammoMaxPenLimit: number;
blacklist: string[]; blacklist: string[];
coopExtractGift: CoopExtractReward; coopExtractGift: CoopExtractReward;
btrDeliveryExpireHours: number; btrDeliveryExpireHours: number;

View File

@ -0,0 +1,9 @@
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { IBarterScheme } from "@spt-aki/models/eft/common/tables/ITrader";
export interface ICreateFenceAssortsResult
{
sptItems: Item[][];
barter_scheme: Record<string, IBarterScheme[][]>;
loyal_level_items: Record<string, number>;
}

View File

@ -23,6 +23,13 @@ export class BotDynamicRouter extends DynamicRouter
return this.botCallbacks.getBotDifficulty(url, info, sessionID); return this.botCallbacks.getBotDifficulty(url, info, sessionID);
}, },
), ),
new RouteAction(
"/singleplayer/settings/bot/difficulties/",
(url: string, info: any, sessionID: string, output: string): any =>
{
return this.botCallbacks.getAllBotDifficulties(url, info, sessionID);
},
),
new RouteAction( new RouteAction(
"/singleplayer/settings/bot/maxCap", "/singleplayer/settings/bot/maxCap",
(url: string, info: any, sessionID: string, output: string): any => (url: string, info: any, sessionID: string, output: string): any =>

View File

@ -20,13 +20,6 @@ export class InraidStaticRouter extends StaticRouter
return this.inraidCallbacks.getRaidEndState(); return this.inraidCallbacks.getRaidEndState();
}, },
), ),
new RouteAction(
"/singleplayer/settings/weapon/durability",
(url: string, info: any, sessionID: string, output: string): any =>
{
return this.inraidCallbacks.getWeaponDurability();
},
),
new RouteAction( new RouteAction(
"/singleplayer/settings/raid/menu", "/singleplayer/settings/raid/menu",
(url: string, info: any, sessionID: string, output: string): any => (url: string, info: any, sessionID: string, output: string): any =>

View File

@ -45,9 +45,6 @@ export class HttpServer
this.handleRequest(req, res); this.handleRequest(req, res);
}); });
this.databaseServer.getTables().server.ip = this.httpConfig.ip;
this.databaseServer.getTables().server.port = this.httpConfig.port;
/* Config server to listen on a port */ /* Config server to listen on a port */
httpServer.listen(this.httpConfig.port, this.httpConfig.ip, () => httpServer.listen(this.httpConfig.port, this.httpConfig.ip, () =>
{ {
@ -82,7 +79,9 @@ export class HttpServer
if (this.httpConfig.logRequests) if (this.httpConfig.logRequests)
{ {
// TODO: Extend to include 192.168 / 10.10 ranges or check subnet // TODO: Extend to include 192.168 / 10.10 ranges or check subnet
const isLocalRequest = req.socket.remoteAddress.startsWith("127.0.0"); const isLocalRequest = req.socket.remoteAddress?.startsWith("127.0.0");
if (typeof isLocalRequest !== "undefined")
{
if (isLocalRequest) if (isLocalRequest)
{ {
this.logger.info(this.localisationService.getText("client_request", req.url)); this.logger.info(this.localisationService.getText("client_request", req.url));
@ -95,6 +94,7 @@ export class HttpServer
})); }));
} }
} }
}
for (const listener of this.httpListeners) for (const listener of this.httpListeners)
{ {

View File

@ -89,6 +89,12 @@ export class BotLootCacheService
case LootCacheType.DRUG_ITEMS: case LootCacheType.DRUG_ITEMS:
result = this.lootCache[botRole].drugItems; result = this.lootCache[botRole].drugItems;
break; break;
case LootCacheType.FOOD_ITEMS:
result = this.lootCache[botRole].foodItems;
break;
case LootCacheType.DRINK_ITEMS:
result = this.lootCache[botRole].drinkItems;
break;
case LootCacheType.STIM_ITEMS: case LootCacheType.STIM_ITEMS:
result = this.lootCache[botRole].stimItems; result = this.lootCache[botRole].stimItems;
break; break;
@ -219,7 +225,7 @@ export class BotLootCacheService
? botJsonTemplate.generation.items.drugs.whitelist ? botJsonTemplate.generation.items.drugs.whitelist
: {}; : {};
// no whitelist, find and assign from combined item pool // no drugs whitelist, find and assign from combined item pool
if (Object.keys(drugItems).length === 0) if (Object.keys(drugItems).length === 0)
{ {
for (const [tpl, weight] of Object.entries(combinedLootPool)) for (const [tpl, weight] of Object.entries(combinedLootPool))
@ -232,6 +238,44 @@ export class BotLootCacheService
} }
} }
// Assign whitelisted food to bot if any exist
const foodItems: Record<string, number> =
(Object.keys(botJsonTemplate.generation.items.food.whitelist)?.length > 0)
? botJsonTemplate.generation.items.food.whitelist
: {};
// No food whitelist, find and assign from combined item pool
if (Object.keys(foodItems).length === 0)
{
for (const [tpl, weight] of Object.entries(combinedLootPool))
{
const itemTemplate = this.itemHelper.getItem(tpl)[1];
if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.FOOD))
{
foodItems[tpl] = weight;
}
}
}
// Assign whitelisted drink to bot if any exist
const drinkItems: Record<string, number> =
(Object.keys(botJsonTemplate.generation.items.food.whitelist)?.length > 0)
? botJsonTemplate.generation.items.food.whitelist
: {};
// No drink whitelist, find and assign from combined item pool
if (Object.keys(drinkItems).length === 0)
{
for (const [tpl, weight] of Object.entries(combinedLootPool))
{
const itemTemplate = this.itemHelper.getItem(tpl)[1];
if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.DRINK))
{
drinkItems[tpl] = weight;
}
}
}
// Assign whitelisted stims to bot if any exist // Assign whitelisted stims to bot if any exist
const stimItems: Record<string, number> = const stimItems: Record<string, number> =
(Object.keys(botJsonTemplate.generation.items.stims.whitelist)?.length > 0) (Object.keys(botJsonTemplate.generation.items.stims.whitelist)?.length > 0)
@ -270,7 +314,7 @@ export class BotLootCacheService
} }
} }
// Get backpack loot (excluding magazines, bullets, grenades and healing items) // Get backpack loot (excluding magazines, bullets, grenades, drink, food and healing/stim items)
const filteredBackpackItems = {}; const filteredBackpackItems = {};
for (const itemKey of Object.keys(backpackLootPool)) for (const itemKey of Object.keys(backpackLootPool))
{ {
@ -285,6 +329,8 @@ export class BotLootCacheService
|| this.isMagazine(itemTemplate._props) || this.isMagazine(itemTemplate._props)
|| this.isMedicalItem(itemTemplate._props) || this.isMedicalItem(itemTemplate._props)
|| this.isGrenade(itemTemplate._props) || this.isGrenade(itemTemplate._props)
|| this.isFood(itemTemplate._id)
|| this.isDrink(itemTemplate._id)
) )
{ {
// Is type we dont want as backpack loot, skip // Is type we dont want as backpack loot, skip
@ -294,7 +340,7 @@ export class BotLootCacheService
filteredBackpackItems[itemKey] = backpackLootPool[itemKey]; filteredBackpackItems[itemKey] = backpackLootPool[itemKey];
} }
// Get pocket loot (excluding magazines, bullets, grenades, medical and healing items) // Get pocket loot (excluding magazines, bullets, grenades, drink, food medical and healing/stim items)
const filteredPocketItems = {}; const filteredPocketItems = {};
for (const itemKey of Object.keys(pocketLootPool)) for (const itemKey of Object.keys(pocketLootPool))
{ {
@ -309,6 +355,8 @@ export class BotLootCacheService
|| this.isMagazine(itemTemplate._props) || this.isMagazine(itemTemplate._props)
|| this.isMedicalItem(itemTemplate._props) || this.isMedicalItem(itemTemplate._props)
|| this.isGrenade(itemTemplate._props) || this.isGrenade(itemTemplate._props)
|| this.isFood(itemTemplate._id)
|| this.isDrink(itemTemplate._id)
|| !("Height" in itemTemplate._props) // lacks height || !("Height" in itemTemplate._props) // lacks height
|| !("Width" in itemTemplate._props) // lacks width || !("Width" in itemTemplate._props) // lacks width
) )
@ -319,7 +367,7 @@ export class BotLootCacheService
filteredPocketItems[itemKey] = pocketLootPool[itemKey]; filteredPocketItems[itemKey] = pocketLootPool[itemKey];
} }
// Get vest loot (excluding magazines, bullets, grenades, medical and healing items) // Get vest loot (excluding magazines, bullets, grenades, medical and healing/stim items)
const filteredVestItems = {}; const filteredVestItems = {};
for (const itemKey of Object.keys(vestLootPool)) for (const itemKey of Object.keys(vestLootPool))
{ {
@ -334,6 +382,8 @@ export class BotLootCacheService
|| this.isMagazine(itemTemplate._props) || this.isMagazine(itemTemplate._props)
|| this.isMedicalItem(itemTemplate._props) || this.isMedicalItem(itemTemplate._props)
|| this.isGrenade(itemTemplate._props) || this.isGrenade(itemTemplate._props)
|| this.isFood(itemTemplate._id)
|| this.isDrink(itemTemplate._id)
) )
{ {
continue; continue;
@ -344,6 +394,8 @@ export class BotLootCacheService
this.lootCache[botRole].healingItems = healingItems; this.lootCache[botRole].healingItems = healingItems;
this.lootCache[botRole].drugItems = drugItems; this.lootCache[botRole].drugItems = drugItems;
this.lootCache[botRole].foodItems = foodItems;
this.lootCache[botRole].drinkItems = drinkItems;
this.lootCache[botRole].stimItems = stimItems; this.lootCache[botRole].stimItems = stimItems;
this.lootCache[botRole].grenadeItems = grenadeItems; this.lootCache[botRole].grenadeItems = grenadeItems;
@ -429,6 +481,16 @@ export class BotLootCacheService
return ("ThrowType" in props); return ("ThrowType" in props);
} }
protected isFood(tpl: string): boolean
{
return this.itemHelper.isOfBaseclass(tpl, BaseClasses.FOOD);
}
protected isDrink(tpl: string): boolean
{
return this.itemHelper.isOfBaseclass(tpl, BaseClasses.DRINK);
}
/** /**
* Check if a bot type exists inside the loot cache * Check if a bot type exists inside the loot cache
* @param botRole role to check for * @param botRole role to check for
@ -455,6 +517,8 @@ export class BotLootCacheService
specialItems: {}, specialItems: {},
grenadeItems: {}, grenadeItems: {},
drugItems: {}, drugItems: {},
foodItems: {},
drinkItems: {},
healingItems: {}, healingItems: {},
stimItems: {}, stimItems: {},
}; };

View File

@ -3,16 +3,16 @@ import { inject, injectable } from "tsyringe";
import { HandbookHelper } from "@spt-aki/helpers/HandbookHelper"; import { HandbookHelper } from "@spt-aki/helpers/HandbookHelper";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
import { PresetHelper } from "@spt-aki/helpers/PresetHelper"; import { PresetHelper } from "@spt-aki/helpers/PresetHelper";
import { MinMax } from "@spt-aki/models/common/MinMax";
import { IFenceLevel } from "@spt-aki/models/eft/common/IGlobals"; import { IFenceLevel } from "@spt-aki/models/eft/common/IGlobals";
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData"; import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
import { Item, Repairable, Upd } from "@spt-aki/models/eft/common/tables/IItem"; import { Item, Repairable } from "@spt-aki/models/eft/common/tables/IItem";
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem"; import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
import { IBarterScheme, ITraderAssort } from "@spt-aki/models/eft/common/tables/ITrader"; import { IBarterScheme, ITraderAssort } from "@spt-aki/models/eft/common/tables/ITrader";
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses"; import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { Traders } from "@spt-aki/models/enums/Traders"; import { Traders } from "@spt-aki/models/enums/Traders";
import { IItemDurabilityCurrentMax, ITraderConfig } from "@spt-aki/models/spt/config/ITraderConfig"; import { IItemDurabilityCurrentMax, ITraderConfig } from "@spt-aki/models/spt/config/ITraderConfig";
import { ICreateFenceAssortsResult } from "@spt-aki/models/spt/fence/ICreateFenceAssortsResult";
import { import {
IFenceAssortGenerationValues, IFenceAssortGenerationValues,
IGenerationAssortValues, IGenerationAssortValues,
@ -43,7 +43,7 @@ export class FenceService
/** Assorts shown on a separate tab when you max out fence rep */ /** Assorts shown on a separate tab when you max out fence rep */
protected fenceDiscountAssort: ITraderAssort = undefined; protected fenceDiscountAssort: ITraderAssort = undefined;
/** Hydrated on initial assort generation as part of generateFenceAssorts() */ /** Desired baseline counts - Hydrated on initial assort generation as part of generateFenceAssorts() */
protected desiredAssortCounts: IFenceAssortGenerationValues; protected desiredAssortCounts: IFenceAssortGenerationValues;
constructor( constructor(
@ -231,16 +231,23 @@ export class FenceService
this.deleteRandomAssorts(itemCountToReplace, this.fenceAssort); this.deleteRandomAssorts(itemCountToReplace, this.fenceAssort);
this.deleteRandomAssorts(discountItemCountToReplace, this.fenceDiscountAssort); this.deleteRandomAssorts(discountItemCountToReplace, this.fenceDiscountAssort);
// Get count of what item pools need new items (item/weapon/equipment) const normalItemCountsToGenerate = this.getItemCountsToGenerate(
const itemCountsToReplace = this.getCountOfItemsToGenerate(); this.fenceAssort.items,
this.desiredAssortCounts.normal,
);
const newItems = this.createAssorts(normalItemCountsToGenerate, 1);
const newItems = this.createFenceAssortSkeleton(); // Push newly generated assorts into existing data
this.createAssorts(itemCountsToReplace.normal, newItems, 1); this.updateFenceAssorts(newItems, this.fenceAssort);
this.fenceAssort.items.push(...newItems.items);
const newDiscountItems = this.createFenceAssortSkeleton(); const discountItemCountsToGenerate = this.getItemCountsToGenerate(
this.createAssorts(itemCountsToReplace.discount, newDiscountItems, 2); this.fenceDiscountAssort.items,
this.fenceDiscountAssort.items.push(...newDiscountItems.items); this.desiredAssortCounts.discount,
);
const newDiscountItems = this.createAssorts(discountItemCountsToGenerate, 2);
// Push newly generated discount assorts into existing data
this.updateFenceAssorts(newDiscountItems, this.fenceDiscountAssort);
// Add new barter items to fence barter scheme // Add new barter items to fence barter scheme
for (const barterItemKey in newItems.barter_scheme) for (const barterItemKey in newItems.barter_scheme)
@ -271,6 +278,46 @@ export class FenceService
this.incrementPartialRefreshTime(); this.incrementPartialRefreshTime();
} }
/**
* Handle the process of folding new assorts into existing assorts, when a new assort exists already, increment its StackObjectsCount instead
* @param newFenceAssorts Assorts to fold into existing fence assorts
* @param existingFenceAssorts Current fence assorts new assorts will be added to
*/
protected updateFenceAssorts(newFenceAssorts: ICreateFenceAssortsResult, existingFenceAssorts: ITraderAssort): void
{
for (const itemWithChildren of newFenceAssorts.sptItems)
{
// Find the root item
const newRootItem = itemWithChildren.find((item) => item.slotId === "hideout");
// Find a matching root item with same tpl in existing assort
const existingRootItem = existingFenceAssorts.items.find((item) =>
item._tpl === newRootItem._tpl && item.slotId === "hideout"
);
// Check if same type of item exists + its on list of item types to always stack
if (existingRootItem && this.itemInPreventDupeCategoryList(newRootItem._tpl))
{
// Guard against a missing stack count
if (!existingRootItem.upd.StackObjectsCount)
{
existingRootItem.upd.StackObjectsCount = 1;
}
// Merge new items count into existing, dont add new loyalty/barter data as it already exists
existingRootItem.upd.StackObjectsCount += newRootItem.upd.StackObjectsCount;
continue;
}
// New assort to be added to existing assorts
existingFenceAssorts.items.push(...itemWithChildren);
existingFenceAssorts.barter_scheme[newRootItem._id] = newFenceAssorts.barter_scheme[newRootItem._id];
existingFenceAssorts.loyal_level_items[newRootItem._id] =
newFenceAssorts.loyal_level_items[newRootItem._id];
}
}
/** /**
* Increment fence next refresh timestamp by current timestamp + partialRefreshTimeSeconds from config * Increment fence next refresh timestamp by current timestamp + partialRefreshTimeSeconds from config
*/ */
@ -281,18 +328,18 @@ export class FenceService
} }
/** /**
* Compare the current fence offer count to what the config wants it to be, * Get values that will hydrate the passed in assorts back to the desired counts
* If value is lower add extra count to value to generate more items to fill gap * @param assortItems Current assorts after items have been removed
* @param existingItemCountToReplace count of items to generate * @param generationValues Base counts assorts should be adjusted to
* @returns number of items to generate * @returns IGenerationAssortValues object with adjustments needed to reach desired state
*/ */
protected getCountOfItemsToGenerate(): IFenceAssortGenerationValues protected getItemCountsToGenerate(
assortItems: Item[],
generationValues: IGenerationAssortValues,
): IGenerationAssortValues
{ {
const currentItemAssortCount = Object.keys(this.fenceAssort.loyal_level_items).length; const allRootItems = assortItems.filter((item) => item.slotId === "hideout");
const rootPresetItems = allRootItems.filter((item) => item.upd.sptPresetId);
const rootPresetItems = this.fenceAssort.items.filter((item) =>
item.slotId === "hideout" && item.upd.sptPresetId
);
// Get count of weapons // Get count of weapons
const currentWeaponPresetCount = rootPresetItems.reduce((count, item) => const currentWeaponPresetCount = rootPresetItems.reduce((count, item) =>
@ -306,60 +353,19 @@ export class FenceService
return this.itemHelper.armorItemCanHoldMods(item._tpl) ? count + 1 : count; return this.itemHelper.armorItemCanHoldMods(item._tpl) ? count + 1 : count;
}, 0); }, 0);
const itemCountToGenerate = Math.max(this.desiredAssortCounts.normal.item - currentItemAssortCount, 0); // Normal item count is total count minus weapon + armor count
const weaponCountToGenerate = Math.max( const nonPresetItemAssortCount = allRootItems.length - (currentWeaponPresetCount + currentEquipmentPresetCount);
this.desiredAssortCounts.normal.weaponPreset - currentWeaponPresetCount,
0,
);
const equipmentCountToGenerate = Math.max(
this.desiredAssortCounts.normal.equipmentPreset - currentEquipmentPresetCount,
0,
);
const normalValues: IGenerationAssortValues = { // Get counts of items to generate, never let values fall below 0
const itemCountToGenerate = Math.max(generationValues.item - nonPresetItemAssortCount, 0);
const weaponCountToGenerate = Math.max(generationValues.weaponPreset - currentWeaponPresetCount, 0);
const equipmentCountToGenerate = Math.max(generationValues.equipmentPreset - currentEquipmentPresetCount, 0);
return {
item: itemCountToGenerate, item: itemCountToGenerate,
weaponPreset: weaponCountToGenerate, weaponPreset: weaponCountToGenerate,
equipmentPreset: equipmentCountToGenerate, equipmentPreset: equipmentCountToGenerate,
}; };
// Discount tab handling
const currentDiscountItemAssortCount = Object.keys(this.fenceDiscountAssort.loyal_level_items).length;
const rootDiscountPresetItems = this.fenceDiscountAssort.items.filter((item) =>
item.slotId === "hideout" && item.upd.sptPresetId
);
// Get count of weapons
const currentDiscountWeaponPresetCount = rootDiscountPresetItems.reduce((count, item) =>
{
return this.itemHelper.isOfBaseclass(item._tpl, BaseClasses.WEAPON) ? count + 1 : count;
}, 0);
// Get count of equipment
const currentDiscountEquipmentPresetCount = rootDiscountPresetItems.reduce((count, item) =>
{
return this.itemHelper.armorItemCanHoldMods(item._tpl) ? count + 1 : count;
}, 0);
const itemDiscountCountToGenerate = Math.max(
this.desiredAssortCounts.discount.item - currentDiscountItemAssortCount,
0,
);
const weaponDiscountCountToGenerate = Math.max(
this.desiredAssortCounts.discount.weaponPreset - currentDiscountWeaponPresetCount,
0,
);
const equipmentDiscountCountToGenerate = Math.max(
this.desiredAssortCounts.discount.equipmentPreset - currentDiscountEquipmentPresetCount,
0,
);
const discountValues: IGenerationAssortValues = {
item: itemDiscountCountToGenerate,
weaponPreset: weaponDiscountCountToGenerate,
equipmentPreset: equipmentDiscountCountToGenerate,
};
return { normal: normalValues, discount: discountValues };
} }
/** /**
@ -386,18 +392,26 @@ export class FenceService
*/ */
protected removeRandomItemFromAssorts(assort: ITraderAssort, rootItems: Item[]): void protected removeRandomItemFromAssorts(assort: ITraderAssort, rootItems: Item[]): void
{ {
const rootItemToRemove = this.randomUtil.getArrayValue(rootItems); const rootItemToAdjust = this.randomUtil.getArrayValue(rootItems);
const itemCountToRemove = this.randomUtil.getInt(1, rootItemToAdjust.upd.StackObjectsCount);
// Clean up any mods if item had them if (itemCountToRemove > 1 && itemCountToRemove < rootItemToAdjust.upd.StackObjectsCount)
const itemWithChildren = this.itemHelper.findAndReturnChildrenAsItems(assort.items, rootItemToRemove._id); { // More than 1 + less then full stack
// Reduce stack size but keep stack
rootItemToAdjust.upd.StackObjectsCount -= itemCountToRemove;
}
else
{
// Remove up item + any mods
const itemWithChildren = this.itemHelper.findAndReturnChildrenAsItems(assort.items, rootItemToAdjust._id);
for (const itemToDelete of itemWithChildren) for (const itemToDelete of itemWithChildren)
{ {
// Delete item from assort items array // Delete item from assort items array
assort.items.splice(assort.items.indexOf(itemToDelete), 1); assort.items.splice(assort.items.indexOf(itemToDelete), 1);
} }
delete assort.barter_scheme[rootItemToRemove._id]; delete assort.barter_scheme[rootItemToAdjust._id];
delete assort.loyal_level_items[rootItemToRemove._id]; delete assort.loyal_level_items[rootItemToAdjust._id];
}
} }
/** /**
@ -437,16 +451,35 @@ export class FenceService
this.createInitialFenceAssortGenerationValues(); this.createInitialFenceAssortGenerationValues();
// Create basic fence assort // Create basic fence assort
const assorts = this.createFenceAssortSkeleton(); const assorts = this.createAssorts(this.desiredAssortCounts.normal, 1);
this.createAssorts(this.desiredAssortCounts.normal, assorts, 1);
// Store in this.fenceAssort // Store in this.fenceAssort
this.setFenceAssort(assorts); this.setFenceAssort(this.convertIntoFenceAssort(assorts));
// Create level 2 assorts accessible at rep level 6 // Create level 2 assorts accessible at rep level 6
const discountAssorts = this.createFenceAssortSkeleton(); const discountAssorts = this.createAssorts(this.desiredAssortCounts.discount, 2);
this.createAssorts(this.desiredAssortCounts.discount, discountAssorts, 2);
// Store in this.fenceDiscountAssort // Store in this.fenceDiscountAssort
this.setFenceDiscountAssort(discountAssorts); this.setFenceDiscountAssort(this.convertIntoFenceAssort(discountAssorts));
}
/**
* Convert the intermediary assort data generated into format client can process
* @param intermediaryAssorts Generated assorts that will be converted
* @returns ITraderAssort
*/
protected convertIntoFenceAssort(intermediaryAssorts: ICreateFenceAssortsResult): ITraderAssort
{
const result = this.createFenceAssortSkeleton();
for (const itemWithChilden of intermediaryAssorts.sptItems)
{
result.items.push(...itemWithChilden);
}
result.barter_scheme = intermediaryAssorts.barter_scheme;
result.loyal_level_items = intermediaryAssorts.loyal_level_items;
return result;
} }
/** /**
@ -506,14 +539,22 @@ export class FenceService
* @param assortCount Number of assorts to generate * @param assortCount Number of assorts to generate
* @param assorts object to add created assorts to * @param assorts object to add created assorts to
*/ */
protected createAssorts(itemCounts: IGenerationAssortValues, assorts: ITraderAssort, loyaltyLevel: number): void protected createAssorts(itemCounts: IGenerationAssortValues, loyaltyLevel: number): ICreateFenceAssortsResult
{ {
const result: ICreateFenceAssortsResult = { sptItems: [], barter_scheme: {}, loyal_level_items: {} };
const baseFenceAssortClone = this.jsonUtil.clone(this.databaseServer.getTables().traders[Traders.FENCE].assort); const baseFenceAssortClone = this.jsonUtil.clone(this.databaseServer.getTables().traders[Traders.FENCE].assort);
const itemTypeLimitCounts = this.initItemLimitCounter(this.traderConfig.fence.itemTypeLimits); const itemTypeLimitCounts = this.initItemLimitCounter(this.traderConfig.fence.itemTypeLimits);
if (itemCounts.item > 0) if (itemCounts.item > 0)
{ {
this.addItemAssorts(itemCounts.item, assorts, baseFenceAssortClone, itemTypeLimitCounts, loyaltyLevel); const itemResult = this.addItemAssorts(
itemCounts.item,
result,
baseFenceAssortClone,
itemTypeLimitCounts,
loyaltyLevel,
);
} }
if (itemCounts.weaponPreset > 0 || itemCounts.equipmentPreset > 0) if (itemCounts.weaponPreset > 0 || itemCounts.equipmentPreset > 0)
@ -522,11 +563,13 @@ export class FenceService
this.addPresetsToAssort( this.addPresetsToAssort(
itemCounts.weaponPreset, itemCounts.weaponPreset,
itemCounts.equipmentPreset, itemCounts.equipmentPreset,
assorts, result,
baseFenceAssortClone, baseFenceAssortClone,
loyaltyLevel, loyaltyLevel,
); );
} }
return result;
} }
/** /**
@ -539,15 +582,15 @@ export class FenceService
*/ */
protected addItemAssorts( protected addItemAssorts(
assortCount: number, assortCount: number,
assorts: ITraderAssort, assorts: ICreateFenceAssortsResult,
baseFenceAssortClone: ITraderAssort, baseFenceAssortClone: ITraderAssort,
itemTypeLimits: Record<string, { current: number; max: number; }>, itemTypeLimits: Record<string, { current: number; max: number; }>,
loyaltyLevel: number, loyaltyLevel: number,
): void ): void
{ {
const priceLimits = this.traderConfig.fence.itemCategoryRoublePriceLimit; const priceLimits = this.traderConfig.fence.itemCategoryRoublePriceLimit;
const assortRootItems = baseFenceAssortClone.items.filter((x) => const assortRootItems = baseFenceAssortClone.items.filter((item) =>
x.parentId === "hideout" && !x.upd?.sptPresetId item.parentId === "hideout" && !item.upd?.sptPresetId
); );
for (let i = 0; i < assortCount; i++) for (let i = 0; i < assortCount; i++)
@ -614,7 +657,7 @@ export class FenceService
} }
// Skip items already in the assort if it exists in the prevent duplicate list // Skip items already in the assort if it exists in the prevent duplicate list
const existingItemThatMatches = this.getMatchingItem(rootItemBeingAdded, itemDbDetails, assorts.items); const existingItemThatMatches = this.getMatchingItem(rootItemBeingAdded, itemDbDetails, assorts.sptItems);
const shouldBeStacked = this.itemShouldBeForceStacked(existingItemThatMatches, itemDbDetails); const shouldBeStacked = this.itemShouldBeForceStacked(existingItemThatMatches, itemDbDetails);
if (shouldBeStacked && existingItemThatMatches) if (shouldBeStacked && existingItemThatMatches)
{ // Decrement loop counter so another items gets added { // Decrement loop counter so another items gets added
@ -630,7 +673,7 @@ export class FenceService
this.randomiseArmorModDurability(desiredAssortItemAndChildrenClone, itemDbDetails); this.randomiseArmorModDurability(desiredAssortItemAndChildrenClone, itemDbDetails);
} }
assorts.items.push(...desiredAssortItemAndChildrenClone); assorts.sptItems.push(desiredAssortItemAndChildrenClone);
assorts.barter_scheme[rootItemBeingAdded._id] = this.jsonUtil.clone( assorts.barter_scheme[rootItemBeingAdded._id] = this.jsonUtil.clone(
baseFenceAssortClone.barter_scheme[chosenBaseAssortRoot._id], baseFenceAssortClone.barter_scheme[chosenBaseAssortRoot._id],
@ -651,15 +694,15 @@ export class FenceService
* e.g. salewa hp resource units left * e.g. salewa hp resource units left
* @param rootItemBeingAdded item to look for a match against * @param rootItemBeingAdded item to look for a match against
* @param itemDbDetails Db details of matching item * @param itemDbDetails Db details of matching item
* @param fenceItemAssorts Items to search through * @param itemsWithChildren Items to search through
* @returns Matching assort item * @returns Matching assort item
*/ */
protected getMatchingItem(rootItemBeingAdded: Item, itemDbDetails: ITemplateItem, fenceItemAssorts: Item[]): Item protected getMatchingItem(rootItemBeingAdded: Item, itemDbDetails: ITemplateItem, itemsWithChildren: Item[][]): Item
{ {
// Get matching root items // Get matching root items
const matchingItems = fenceItemAssorts.filter((item) => const matchingItems = itemsWithChildren.filter((itemWithChildren) =>
item._tpl === rootItemBeingAdded._tpl && item.parentId === "hideout" itemWithChildren.find((item) => item._tpl === rootItemBeingAdded._tpl && item.parentId === "hideout")
); ).flatMap((x) => x);
if (matchingItems.length === 0) if (matchingItems.length === 0)
{ {
// Nothing matches by tpl and is root item, exit early // Nothing matches by tpl and is root item, exit early
@ -726,11 +769,13 @@ export class FenceService
return false; return false;
} }
return this.itemInPreventDupeCategoryList(itemDbDetails._id);
}
protected itemInPreventDupeCategoryList(tpl: string): boolean
{
// Item type in config list // Item type in config list
return this.itemHelper.isOfBaseclasses( return this.itemHelper.isOfBaseclasses(tpl, this.traderConfig.fence.preventDuplicateOffersOfCategory);
itemDbDetails._id,
this.traderConfig.fence.preventDuplicateOffersOfCategory,
);
} }
/** /**
@ -799,7 +844,7 @@ export class FenceService
protected addPresetsToAssort( protected addPresetsToAssort(
desiredWeaponPresetsCount: number, desiredWeaponPresetsCount: number,
desiredEquipmentPresetsCount: number, desiredEquipmentPresetsCount: number,
assorts: ITraderAssort, assorts: ICreateFenceAssortsResult,
baseFenceAssort: ITraderAssort, baseFenceAssort: ITraderAssort,
loyaltyLevel: number, loyaltyLevel: number,
): void ): void
@ -848,7 +893,7 @@ export class FenceService
// Remapping IDs causes parentid to be altered // Remapping IDs causes parentid to be altered
presetWithChildrenClone[0].parentId = "hideout"; presetWithChildrenClone[0].parentId = "hideout";
assorts.items.push(...presetWithChildrenClone); assorts.sptItems.push(presetWithChildrenClone);
// Set assort price // Set assort price
// Must be careful to use correct id as the item has had its IDs regenerated // Must be careful to use correct id as the item has had its IDs regenerated
@ -908,7 +953,7 @@ export class FenceService
// Remapping IDs causes parentid to be altered // Remapping IDs causes parentid to be altered
presetWithChildrenClone[0].parentId = "hideout"; presetWithChildrenClone[0].parentId = "hideout";
assorts.items.push(...presetWithChildrenClone); assorts.sptItems.push(presetWithChildrenClone);
// Set assort price // Set assort price
// Must be careful to use correct id as the item has had its IDs regenerated // Must be careful to use correct id as the item has had its IDs regenerated

View File

@ -13,6 +13,7 @@ import { LocalisationService } from "@spt-aki/services/LocalisationService";
export class ItemBaseClassService export class ItemBaseClassService
{ {
protected itemBaseClassesCache: Record<string, string[]> = {}; protected itemBaseClassesCache: Record<string, string[]> = {};
protected items: Record<string, ITemplateItem>;
protected cacheGenerated = false; protected cacheGenerated = false;
constructor( constructor(
@ -31,15 +32,15 @@ export class ItemBaseClassService
// Clear existing cache // Clear existing cache
this.itemBaseClassesCache = {}; this.itemBaseClassesCache = {};
const allDbItems = this.databaseServer.getTables().templates.items; this.items = this.databaseServer.getTables().templates.items;
if (!allDbItems) if (!this.items)
{ {
this.logger.warning(this.localisationService.getText("baseclass-missing_db_no_cache")); this.logger.warning(this.localisationService.getText("baseclass-missing_db_no_cache"));
return; return;
} }
const filteredDbItems = Object.values(allDbItems).filter((x) => x._type === "Item"); const filteredDbItems = Object.values(this.items).filter((x) => x._type === "Item");
for (const item of filteredDbItems) for (const item of filteredDbItems)
{ {
const itemIdToUpdate = item._id; const itemIdToUpdate = item._id;
@ -48,7 +49,7 @@ export class ItemBaseClassService
this.itemBaseClassesCache[item._id] = []; this.itemBaseClassesCache[item._id] = [];
} }
this.addBaseItems(itemIdToUpdate, item, allDbItems); this.addBaseItems(itemIdToUpdate, item);
} }
this.cacheGenerated = true; this.cacheGenerated = true;
@ -58,16 +59,15 @@ export class ItemBaseClassService
* Helper method, recursivly iterate through items parent items, finding and adding ids to dictionary * Helper method, recursivly iterate through items parent items, finding and adding ids to dictionary
* @param itemIdToUpdate item tpl to store base ids against in dictionary * @param itemIdToUpdate item tpl to store base ids against in dictionary
* @param item item being checked * @param item item being checked
* @param allDbItems all items in db
*/ */
protected addBaseItems(itemIdToUpdate: string, item: ITemplateItem, allDbItems: Record<string, ITemplateItem>): void protected addBaseItems(itemIdToUpdate: string, item: ITemplateItem): void
{ {
this.itemBaseClassesCache[itemIdToUpdate].push(item._parent); this.itemBaseClassesCache[itemIdToUpdate].push(item._parent);
const parent = allDbItems[item._parent]; const parent = this.items[item._parent];
if (parent._parent !== "") if (parent._parent !== "")
{ {
this.addBaseItems(itemIdToUpdate, parent, allDbItems); this.addBaseItems(itemIdToUpdate, parent);
} }
} }
@ -91,8 +91,9 @@ export class ItemBaseClassService
return false; return false;
} }
// Edge case - this is the 'root' item that all other items inherit from // The cache is only generated for item templates with `_type === "Item"`, so return false for any other type,
if (itemTpl === BaseClasses.ITEM) // including item templates that simply don't exist.
if (!this.cachedItemIsOfItemType(itemTpl))
{ {
return false; return false;
} }
@ -114,6 +115,16 @@ export class ItemBaseClassService
return this.itemBaseClassesCache[itemTpl].some((x) => baseClasses.includes(x)); return this.itemBaseClassesCache[itemTpl].some((x) => baseClasses.includes(x));
} }
/**
* Check if cached item template is of type Item
* @param itemTemplateId item to check
* @returns true if item is of type Item
*/
private cachedItemIsOfItemType(itemTemplateId: string): boolean
{
return this.items[itemTemplateId]?._type === "Item";
}
/** /**
* Get base classes item inherits from * Get base classes item inherits from
* @param itemTpl item to get base classes for * @param itemTpl item to get base classes for

View File

@ -114,8 +114,8 @@ export class ModCompilerService
if (output.sourceMapText) if (output.sourceMapText)
{ {
output.outputText = output.outputText.replace( output.outputText = output.outputText.replace(
"//# sourceMappingURL=module.js.map", "//# sourceMappingURL\=module.js.map",
`//# sourceMappingURL=${parsedDestPath.base}.map`, `//# sourceMappingURL\=${parsedDestPath.base}.map`,
); );
const sourceMap = JSON.parse(output.sourceMapText); const sourceMap = JSON.parse(output.sourceMapText);

View File

@ -315,7 +315,7 @@ export class PaymentService
} }
/** /**
* Get all money stacks in inventory and prioritse items in stash * Get all money stacks in inventory and prioritise items in stash
* @param pmcData * @param pmcData
* @param currencyTpl * @param currencyTpl
* @param playerStashId Players stash id * @param playerStashId Players stash id

View File

@ -0,0 +1,64 @@
import { injectable } from "tsyringe";
@injectable()
export class ProfileActivityService
{
protected profileActivityTimestamps: Record<string, number> = {};
/**
* Was the requested profile active in the last requested minutes
* @param sessionId Profile to check
* @param minutes Minutes to check for activity in
* @returns True when profile was active within past x minutes
*/
public activeWithinLastMinutes(sessionId: string, minutes: number): boolean
{
const currentTimestamp = new Date().getTime() / 1000;
const storedActivityTimestamp = this.profileActivityTimestamps[sessionId];
if (!storedActivityTimestamp)
{
// No value, no assumed activity (server offline?)
return false;
}
// True if difference since last timestamp to now is below desired amount
return (currentTimestamp - storedActivityTimestamp) < (minutes * 60); // convert minutes to seconds to compare
}
/**
* Get an array of profile ids that were active in the last x minutes
* @param minutes How many minutes from now to search for profiles
* @returns String array of profile ids
*/
public getActiveProfileIdsWithinMinutes(minutes: number): string[]
{
const currentTimestamp = new Date().getTime() / 1000;
const result: string[] = [];
for (const id of Object.keys(this.profileActivityTimestamps ?? {}))
{
const lastActiveTimestamp = this.profileActivityTimestamps[id];
if (!lastActiveTimestamp)
{
continue;
}
// Profile was active in last x minutes, add to return list
if ((currentTimestamp - lastActiveTimestamp) < (minutes * 60))
{
result.push(id);
}
}
return result;
}
/**
* Update the timestamp a profile was last observed active
* @param sessionId Profile to update
*/
public setActivityTimestamp(sessionId: string): void
{
this.profileActivityTimestamps[sessionId] = new Date().getTime() / 1000;
}
}

View File

@ -9,9 +9,9 @@ import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
import { Bonus, HideoutSlot, IQuestStatus } from "@spt-aki/models/eft/common/tables/IBotBase"; import { Bonus, HideoutSlot, IQuestStatus } from "@spt-aki/models/eft/common/tables/IBotBase";
import { IHideoutImprovement } from "@spt-aki/models/eft/common/tables/IBotBase"; import { IHideoutImprovement } from "@spt-aki/models/eft/common/tables/IBotBase";
import { IPmcDataRepeatableQuest, IRepeatableQuest } from "@spt-aki/models/eft/common/tables/IRepeatableQuests"; import { IPmcDataRepeatableQuest, IRepeatableQuest } from "@spt-aki/models/eft/common/tables/IRepeatableQuests";
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
import { StageBonus } from "@spt-aki/models/eft/hideout/IHideoutArea"; import { StageBonus } from "@spt-aki/models/eft/hideout/IHideoutArea";
import { IAkiProfile } from "@spt-aki/models/eft/profile/IAkiProfile"; import { IAkiProfile, IEquipmentBuild, IMagazineBuild, IWeaponBuild } from "@spt-aki/models/eft/profile/IAkiProfile";
import { AccountTypes } from "@spt-aki/models/enums/AccountTypes";
import { BonusType } from "@spt-aki/models/enums/BonusType"; import { BonusType } from "@spt-aki/models/enums/BonusType";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { HideoutAreas } from "@spt-aki/models/enums/HideoutAreas"; import { HideoutAreas } from "@spt-aki/models/enums/HideoutAreas";
@ -282,7 +282,11 @@ export class ProfileFixerService
} }
const db = this.databaseServer.getTables(); const db = this.databaseServer.getTables();
const placeOfFameAreaDb = db.hideout.areas.find((x) => x.type === HideoutAreas.PLACE_OF_FAME); const placeOfFameAreaDb = db.hideout.areas.find((area) => area.type === HideoutAreas.PLACE_OF_FAME);
if (!placeOfFameAreaDb)
{
return;
}
const stageCurrentlyAt = placeOfFameAreaDb.stages[placeOfFameArea.level]; const stageCurrentlyAt = placeOfFameAreaDb.stages[placeOfFameArea.level];
const placeOfFameStashId = pmcProfile.Inventory.hideoutAreaStashes[HideoutAreas.PLACE_OF_FAME]; const placeOfFameStashId = pmcProfile.Inventory.hideoutAreaStashes[HideoutAreas.PLACE_OF_FAME];
@ -870,11 +874,8 @@ export class ProfileFixerService
const inventoryItemsToCheck = pmcProfile.Inventory.items.filter((item) => const inventoryItemsToCheck = pmcProfile.Inventory.items.filter((item) =>
["hideout", "main"].includes(item.slotId) ["hideout", "main"].includes(item.slotId)
); );
if (!inventoryItemsToCheck) if (inventoryItemsToCheck)
{ {
return;
}
// Check each item in inventory to ensure item exists in itemdb // Check each item in inventory to ensure item exists in itemdb
for (const item of inventoryItemsToCheck) for (const item of inventoryItemsToCheck)
{ {
@ -893,29 +894,26 @@ export class ProfileFixerService
} }
} }
} }
// Iterate over player-made weapon builds, look for missing items and remove weapon preset if found
for (const buildId in fullProfile.userbuilds?.weaponBuilds)
{
for (const item of fullProfile.userbuilds.weaponBuilds[buildId].Items)
{
// Check item exists in itemsDb
if (!itemsDb[item._tpl])
{
this.logger.error(this.localisationService.getText("fixer-mod_item_found", item._tpl));
if (this.coreConfig.fixes.removeModItemsFromProfile)
{
delete fullProfile.userbuilds.weaponBuilds[buildId];
this.logger.warning(
`Item: ${item._tpl} has resulted in the deletion of weapon build: ${buildId}`,
);
} }
break; // Remove invalid builds from weapon, equipment and magazine build lists
} const weaponBuilds = fullProfile.userbuilds?.weaponBuilds || [];
} fullProfile.userbuilds.weaponBuilds = weaponBuilds.filter((weaponBuild) =>
} {
return !this.shouldRemoveWeaponEquipmentBuild("weapon", weaponBuild, itemsDb);
});
const equipmentBuilds = fullProfile.userbuilds?.equipmentBuilds || [];
fullProfile.userbuilds.equipmentBuilds = equipmentBuilds.filter((equipmentBuild) =>
{
return !this.shouldRemoveWeaponEquipmentBuild("equipment", equipmentBuild, itemsDb);
});
const magazineBuilds = fullProfile.userbuilds?.magazineBuilds || [];
fullProfile.userbuilds.magazineBuilds = magazineBuilds.filter((magazineBuild) =>
{
return !this.shouldRemoveMagazineBuild(magazineBuild, itemsDb);
});
// Iterate over dialogs, looking for messages with items not found in item db, remove message if item found // Iterate over dialogs, looking for messages with items not found in item db, remove message if item found
for (const dialogId in fullProfile.dialogues) for (const dialogId in fullProfile.dialogues)
@ -927,7 +925,7 @@ export class ProfileFixerService
} }
// Iterate over all messages in dialog // Iterate over all messages in dialog
for (const message of dialog.messages) for (const [_, message] of Object.entries(dialog.messages))
{ {
if (!message.items?.data) if (!message.items?.data)
{ {
@ -965,7 +963,7 @@ export class ProfileFixerService
} }
const clothing = this.databaseServer.getTables().templates.customization; const clothing = this.databaseServer.getTables().templates.customization;
for (const suitId of fullProfile.suits) for (const [_, suitId] of Object.entries(fullProfile.suits))
{ {
if (!clothing[suitId]) if (!clothing[suitId])
{ {
@ -980,7 +978,7 @@ export class ProfileFixerService
for (const repeatable of fullProfile.characters.pmc.RepeatableQuests ?? []) for (const repeatable of fullProfile.characters.pmc.RepeatableQuests ?? [])
{ {
for (const activeQuest of repeatable.activeQuests ?? []) for (const [_, activeQuest] of Object.entries(repeatable.activeQuests ?? []))
{ {
if (!this.traderHelper.traderEnumHasValue(activeQuest.traderId)) if (!this.traderHelper.traderEnumHasValue(activeQuest.traderId))
{ {
@ -1041,6 +1039,77 @@ export class ProfileFixerService
} }
} }
/**
* @param buildType The type of build, used for logging only
* @param build The build to check for invalid items
* @param itemsDb The items database to use for item lookup
* @returns True if the build should be removed from the build list, false otherwise
*/
protected shouldRemoveWeaponEquipmentBuild(
buildType: string,
build: IWeaponBuild | IEquipmentBuild,
itemsDb: Record<string, ITemplateItem>,
): boolean
{
for (const item of build.Items)
{
// Check item exists in itemsDb
if (!itemsDb[item._tpl])
{
this.logger.error(this.localisationService.getText("fixer-mod_item_found", item._tpl));
if (this.coreConfig.fixes.removeModItemsFromProfile)
{
this.logger.warning(
`Item: ${item._tpl} has resulted in the deletion of ${buildType} build: ${build.Name}`,
);
return true;
}
break;
}
}
return false;
}
/**
* @param magazineBuild The magazine build to check for validity
* @param itemsDb The items database to use for item lookup
* @returns True if the build should be removed from the build list, false otherwise
*/
protected shouldRemoveMagazineBuild(magazineBuild: IMagazineBuild, itemsDb: Record<string, ITemplateItem>): boolean
{
for (const item of magazineBuild.Items)
{
// Magazine builds can have null items in them, skip those
if (!item)
{
continue;
}
// Check item exists in itemsDb
if (!itemsDb[item.TemplateId])
{
this.logger.error(this.localisationService.getText("fixer-mod_item_found", item.TemplateId));
if (this.coreConfig.fixes.removeModItemsFromProfile)
{
this.logger.warning(
`Item: ${item.TemplateId} has resulted in the deletion of magazine build: ${magazineBuild.Name}`,
);
return true;
}
break;
}
}
return false;
}
/** /**
* Attempt to fix common item issues that corrupt profiles * Attempt to fix common item issues that corrupt profiles
* @param pmcProfile Profile to check items of * @param pmcProfile Profile to check items of
@ -1307,6 +1376,21 @@ export class ProfileFixerService
} }
} }
/**
* 3.8.0 utilized the wrong ProductionTime for bitcoin, fix it if it's found
*/
public fixBitcoinProductionTime(pmcProfile: IPmcData): void
{
const btcProd = pmcProfile.Hideout?.Production[HideoutHelper.bitcoinFarm];
if (btcProd)
{
btcProd.ProductionTime = this.hideoutHelper.getAdjustedCraftTimeWithSkills(
pmcProfile,
HideoutHelper.bitcoinProductionId,
);
}
}
/** /**
* At some point the property name was changed,migrate data across to new name * At some point the property name was changed,migrate data across to new name
* @param pmcProfile Profile to migrate improvements in * @param pmcProfile Profile to migrate improvements in
@ -1337,7 +1421,7 @@ export class ProfileFixerService
repeatableQuests.push(...repeatableQuestType.activeQuests); repeatableQuests.push(...repeatableQuestType.activeQuests);
} }
for (let i = 0; i < profileQuests.length; i++) for (let i = profileQuests.length - 1; i >= 0; i--)
{ {
if (!(quests[profileQuests[i].qid] || repeatableQuests.find((x) => x._id === profileQuests[i].qid))) if (!(quests[profileQuests[i].qid] || repeatableQuests.find((x) => x._id === profileQuests[i].qid)))
{ {

View File

@ -223,109 +223,129 @@ export class RagfairPriceService implements OnLoad
*/ */
public getDynamicOfferPriceForOffer(offerItems: Item[], desiredCurrency: string, isPackOffer: boolean): number public getDynamicOfferPriceForOffer(offerItems: Item[], desiredCurrency: string, isPackOffer: boolean): number
{ {
const rootItem = offerItems[0]; // Price to return.
// Price to return
let price = 0; let price = 0;
let endLoop = false; // Iterate over each item in the offer.
let isPreset = false;
let manuallyAdjusted = false;
for (const item of offerItems) for (const item of offerItems)
{ {
// Armor insert, skip - we dont factor these into an items price // Skip over armour inserts as those are not factored into item prices.
if (this.itemHelper.isOfBaseclass(item._tpl, BaseClasses.BUILT_IN_INSERTS)) if (this.itemHelper.isOfBaseclass(item._tpl, BaseClasses.BUILT_IN_INSERTS))
{ {
continue; continue;
} }
// Get dynamic price, fallback to handbook price if value of 1 found price += this.getDynamicItemPrice(item._tpl, desiredCurrency, item, offerItems, isPackOffer);
let itemPrice = this.getFleaPriceForItem(item._tpl);
// Check if the item is a weapon preset.
if (item?.upd?.sptPresetId && this.presetHelper.isPresetBaseClass(item.upd.sptPresetId, BaseClasses.WEAPON))
{
// This is a weapon preset, which has it's own price calculation that takes into account the mods in the
// preset. Since we've already calculated the price for the preset entire preset in
// `getDynamicItemPrice`, we can skip the rest of the items in the offer.
break;
}
}
return Math.round(price);
}
/**
* @param itemTemplateId
* @param desiredCurrency
* @param item
* @param offerItems
* @param isPackOffer
* @returns
*/
public getDynamicItemPrice(
itemTemplateId: string,
desiredCurrency: string,
item?: Item,
offerItems?: Item[],
isPackOffer?: boolean,
): number
{
let isPreset = false;
let price = this.getFleaPriceForItem(itemTemplateId);
// Adjust price if below handbook price, based on config.
if (this.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice) if (this.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice)
{ {
itemPrice = this.adjustPriceIfBelowHandbook(itemPrice, item._tpl); price = this.adjustPriceIfBelowHandbook(price, itemTemplateId);
} }
// Use trader price if higher, based on config.
if (this.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher) if (this.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher)
{ {
// Get highest trader price for item, if greater than value found so far, use it const traderPrice = this.traderHelper.getHighestSellToTraderPrice(itemTemplateId);
const traderPrice = this.traderHelper.getHighestSellToTraderPrice(item._tpl); if (traderPrice > price)
if (traderPrice > itemPrice)
{ {
itemPrice = traderPrice; price = traderPrice;
} }
} }
// Check if item type is weapon preset, handle differently // Prices for weapon presets are handled differently.
const itemDetails = this.itemHelper.getItem(item._tpl); if (
if (this.presetHelper.isPreset(item.upd?.sptPresetId) && itemDetails[1]._props.weapFireType) item?.upd?.sptPresetId
&& offerItems
&& this.presetHelper.isPresetBaseClass(item.upd.sptPresetId, BaseClasses.WEAPON)
)
{ {
itemPrice = this.getWeaponPresetPrice(item, offerItems, itemPrice); price = this.getWeaponPresetPrice(item, offerItems, price);
endLoop = true;
isPreset = true; isPreset = true;
} }
// Check for existance of manual price adjustment multiplier // Check for existence of manual price adjustment multiplier
const manualPriceMultipler = this.ragfairConfig.dynamic.itemPriceMultiplier[item._tpl]; const multiplier = this.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId];
if (manualPriceMultipler) if (multiplier)
{ {
manuallyAdjusted = true; price *= multiplier;
itemPrice *= manualPriceMultipler;
} }
// Multiply dynamic price by quality modifier // The quality of the item affects the price.
const itemQualityModifier = this.itemHelper.getItemQualityModifier(item); if (item)
price += itemPrice * itemQualityModifier;
// Stop loop if weapon preset price function has been run
if (endLoop)
{ {
break; const qualityModifier = this.itemHelper.getItemQualityModifier(item);
} price *= qualityModifier;
} }
// Check for unreasonable price on singular items // Make adjustments for unreasonably priced items.
if (offerItems.length === 1 && !manuallyAdjusted) for (const baseClassTemplateId of Object.keys(this.ragfairConfig.dynamic.unreasonableModPrices))
{ {
const rootItemDb = this.itemHelper.getItem(rootItem._tpl)[1]; if (this.itemHelper.isOfBaseclass(itemTemplateId, baseClassTemplateId))
let unreasonableItemPriceChange: IUnreasonableModPrices;
for (const key of Object.keys(this.ragfairConfig.dynamic.unreasonableModPrices))
{ {
if (this.itemHelper.isOfBaseclass(rootItemDb._id, key)) // Found an unreasonable price type.
{ const unreasonableModifier: IUnreasonableModPrices =
unreasonableItemPriceChange = this.ragfairConfig.dynamic.unreasonableModPrices[key]; this.ragfairConfig.dynamic.unreasonableModPrices[baseClassTemplateId];
break; if (unreasonableModifier.enabled)
}
}
if (unreasonableItemPriceChange?.enabled)
{ {
price = this.adjustUnreasonablePrice( price = this.adjustUnreasonablePrice(
this.databaseServer.getTables().templates.handbook.Items, this.databaseServer.getTables().templates.handbook.Items,
unreasonableItemPriceChange, unreasonableModifier,
rootItem._tpl, itemTemplateId,
price, price,
); );
} }
} }
}
// Get price multiplier min/max to vary price // Vary the price based on the type of offer.
const rangeValues = this.getOfferTypeRangeValues(isPreset, isPackOffer); const range = this.getOfferTypeRangeValues(isPreset, isPackOffer);
price = this.randomiseOfferPrice(price, rangeValues); price = this.randomiseOfferPrice(price, range);
// Convert to different currency if desiredCurrency param is not roubles // Convert to different currency if required.
if (desiredCurrency !== Money.ROUBLES) const roublesId = Money.ROUBLES;
if (desiredCurrency !== roublesId)
{ {
price = this.handbookHelper.fromRUB(price, desiredCurrency); price = this.handbookHelper.fromRUB(price, desiredCurrency);
} }
// Guard against weird prices
if (price < 1) if (price < 1)
{ {
price = 1; return 1;
} }
return price; return price;
} }
@ -400,7 +420,7 @@ export class RagfairPriceService implements OnLoad
const itemHandbookPrice = this.getStaticPriceForItem(itemTpl); const itemHandbookPrice = this.getStaticPriceForItem(itemTpl);
const priceDifferencePercent = this.getPriceDifference(itemHandbookPrice, itemPrice); const priceDifferencePercent = this.getPriceDifference(itemHandbookPrice, itemPrice);
// Only adjust price if difference is > a percent AND item price passes threshhold set in config // Only adjust price if difference is > a percent AND item price passes threshold set in config
if ( if (
priceDifferencePercent > this.ragfairConfig.dynamic.offerAdjustment.maxPriceDifferenceBelowHandbookPercent priceDifferencePercent > this.ragfairConfig.dynamic.offerAdjustment.maxPriceDifferenceBelowHandbookPercent
&& itemPrice >= this.ragfairConfig.dynamic.offerAdjustment.priceThreshholdRub && itemPrice >= this.ragfairConfig.dynamic.offerAdjustment.priceThreshholdRub

View File

@ -70,6 +70,7 @@ export abstract class AbstractWinstonLogger implements ILogger
filename: this.filePath, filename: this.filePath,
datePattern: "YYYY-MM-DD", datePattern: "YYYY-MM-DD",
zippedArchive: true, zippedArchive: true,
frequency: this.getLogFrequency(),
maxSize: this.getLogMaxSize(), maxSize: this.getLogMaxSize(),
maxFiles: this.getLogMaxFiles(), maxFiles: this.getLogMaxFiles(),
format: format.combine( format: format.combine(
@ -108,6 +109,11 @@ export abstract class AbstractWinstonLogger implements ILogger
protected abstract getFileName(): string; protected abstract getFileName(): string;
protected getLogFrequency(): string
{
return "3h";
}
protected getLogMaxSize(): string protected getLogMaxSize(): string
{ {
return "5m"; return "5m";

View File

@ -24,7 +24,7 @@ export class ProfileInsuranceFactory
} }
/** /**
* Adjusts the scheduledTime and messageContent.systemData.date and messageContent.systemData.time, otherwise the * Adjusts the scheduledTime, messageContent.systemData.date, and messageContent.systemData.time, otherwise the
* dates in the original fixture will likely be expired. * dates in the original fixture will likely be expired.
*/ */
public adjustPackageDates(dateInput?: DateInput): this public adjustPackageDates(dateInput?: DateInput): this
@ -45,8 +45,8 @@ export class ProfileInsuranceFactory
} }
insurance.scheduledTime = date; insurance.scheduledTime = date;
insurance.messageContent.systemData.date = format(date, "MM.dd.yyyy"); insurance.systemData.date = format(date, "MM.dd.yyyy");
insurance.messageContent.systemData.time = format(date, "HH:mm"); insurance.systemData.time = format(date, "HH:mm");
return insurance; return insurance;
}); });

View File

@ -1,747 +1,1388 @@
import { Insurance } from "@spt-aki/models/eft/profile/IAkiProfile"; import { Insurance } from "@spt-aki/models/eft/profile/IAkiProfile";
export const profileInsuranceFixture: Insurance[] = [{ export const profileInsuranceFixture: Insurance[] = [{
scheduledTime: 1698945140, scheduledTime: 1712950044.4,
traderId: "54cb50c76803fa8b248b4571", // Prapor traderId: "54cb50c76803fa8b248b4571",
messageContent: {
templateId: "58fe0e4586f774728248ca13 4",
type: 8,
maxStorageTime: 345600, maxStorageTime: 345600,
text: "", systemData: { date: "11.04.2024", time: "18:59", location: "factory4_day" },
profileChangeEvents: [], messageType: 8,
systemData: { date: "01.11.2023", time: "10:51", location: "factory4_day" }, messageTemplateId: "58fe0e4586f774728248ca13 0",
},
items: [{ items: [{
_id: "3679078e05f5b14466d6a730", _id: "35111c9b72a87b6b7d95ad35",
_tpl: "5d6d3716a4b9361bc8618872", _tpl: "58948c8e86f77409493f7266",
parentId: "5fe49444ae6628187a2e77b8", parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout", slotId: "hideout",
upd: { StackObjectsCount: 1, Repairable: { Durability: 55, MaxDurability: 55 } }, upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, { }, {
_id: "911a0f04d5d9c7e239807ae0", _id: "d45436a159654f43ca3aa52f",
_tpl: "5644bd2b4bdc2d3b4c8b4572", _tpl: "5580223e4bdc2d1c128b457f",
parentId: "5fe49444ae6628187a2e77b8", parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout", slotId: "hideout",
upd: { StackObjectsCount: 1, Repairable: { Durability: 97.7862549, MaxDurability: 100 } }, upd: {
FireMode: { FireMode: "single" },
StackObjectsCount: 1,
Repairable: { Durability: 100, MaxDurability: 100 },
},
}, { }, {
_id: "695b13896108f765e8985698", _id: "2c60ad9b6051f059ab796aa6",
_tpl: "5648a69d4bdc2ded0b8b457b", _tpl: "5a7ae0c351dfba0017554310",
parentId: "5fe49444ae6628187a2e77b8", parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, {
_id: "a5c86cef7d25f57bf0fb593c",
_tpl: "5b432f3d5acfc4704b4a1dfb",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout", slotId: "hideout",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "bb49d6ceb3e87d8563a06455", _id: "8ac63abcbaf95d09a4d50c02",
_tpl: "5df8a4d786f77412672a1e3b", _tpl: "5ea17ca01412a1425304d1c0",
parentId: "5fe49444ae6628187a2e77b8", parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout", slotId: "hideout",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "631f8492de748dec852f7ddf", _id: "33c99e86f72af509da01dc9a",
_tpl: "64abd93857958b4249003418", _tpl: "657f9a55c6679fefb3051e19",
parentId: "5fe49444ae6628187a2e77b8", parentId: "8ac63abcbaf95d09a4d50c02",
slotId: "Helmet_top",
upd: { Repairable: { Durability: 24, MaxDurability: 24 } },
}, {
_id: "426902ae3d7efa5f8c78acf7",
_tpl: "657f9a94ada5fadd1f07a589",
parentId: "8ac63abcbaf95d09a4d50c02",
slotId: "Helmet_back",
upd: { Repairable: { Durability: 24, MaxDurability: 24 } },
}, {
_id: "5d2be23efb34d0d1da9d3701",
_tpl: "603648ff5a45383c122086ac",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout", slotId: "hideout",
upd: { StackObjectsCount: 1, Repairable: { Durability: 49.2865, MaxDurability: 60 } },
}, {
_id: "a2b0c716162c5e31ec28c55a",
_tpl: "5a16b8a9fcdbcb00165aa6ca",
parentId: "3679078e05f5b14466d6a730",
slotId: "mod_nvg",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "dc565f750342cb2d19eeda06", _id: "9f601faab37dcc58190898ac",
_tpl: "5d6d3be5a4b9361bc73bc763", _tpl: "618bb76513f5097c8d5aa2d5",
parentId: "3679078e05f5b14466d6a730", parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "mod_equipment_001", slotId: "hideout",
upd: { StackObjectsCount: 1, Repairable: { Durability: 29.33, MaxDurability: 29.33 } },
}, {
_id: "e9ff62601669d9e2ea9c2fbb",
_tpl: "5d6d3943a4b9360dbc46d0cc",
parentId: "3679078e05f5b14466d6a730",
slotId: "mod_equipment_002",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "ac134d7cf6c9d8e25edd0015", _id: "f74d377063e65d350e0099be",
_tpl: "5c11046cd174af02a012e42b", _tpl: "5c0e5bab86f77461f55ed1f3",
parentId: "a2b0c716162c5e31ec28c55a", parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "mod_nvg", slotId: "hideout",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "22274b895ecc80d51c3cba1c", _id: "0ab2a81fc507ac846f43b15f",
_tpl: "5c110624d174af029e69734c", _tpl: "6571b27a6d84a2b8b6007f92",
parentId: "ac134d7cf6c9d8e25edd0015", parentId: "f74d377063e65d350e0099be",
slotId: "mod_nvg", slotId: "Soft_armor_front",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 }, Togglable: { On: true } }, upd: { Repairable: { Durability: 50.88512, MaxDurability: 52 } },
}, { }, {
_id: "c9278dd8251e99578bf7a274", _id: "2ca1b6606d918483ed6b70a5",
_tpl: "59c6633186f7740cf0493bb9", _tpl: "6571baa74cb80d995d0a1490",
parentId: "911a0f04d5d9c7e239807ae0", parentId: "f74d377063e65d350e0099be",
slotId: "mod_gas_block", slotId: "Soft_armor_back",
upd: { Repairable: { Durability: 49, MaxDurability: 52 } },
}, {
_id: "5658a9d10f9d44112a991561",
_tpl: "6571baac6d84a2b8b6007fa3",
parentId: "f74d377063e65d350e0099be",
slotId: "Soft_armor_left",
upd: { Repairable: { Durability: 8, MaxDurability: 8 } },
}, {
_id: "8d0ba4d12fa601312b71d3d7",
_tpl: "6571bab0f41985531a038091",
parentId: "f74d377063e65d350e0099be",
slotId: "soft_armor_right",
upd: { Repairable: { Durability: 8, MaxDurability: 8 } },
}, {
_id: "45d19bbff6d42c8f781abb38",
_tpl: "6571babb4076795e5e07383f",
parentId: "f74d377063e65d350e0099be",
slotId: "Collar",
upd: { Repairable: { Durability: 14, MaxDurability: 14 } },
}, {
_id: "cf2ba30bab4d8e80393a8ffe",
_tpl: "6571bac34076795e5e073843",
parentId: "f74d377063e65d350e0099be",
slotId: "Groin",
upd: { Repairable: { Durability: 10, MaxDurability: 10 } },
}, {
_id: "a3f866e60ccd9c29e77eb5ef",
_tpl: "6571babf4cb80d995d0a1494",
parentId: "f74d377063e65d350e0099be",
slotId: "Groin_back",
upd: { Repairable: { Durability: 12, MaxDurability: 12 } },
}, {
_id: "a3287a706e1b77b44db82fa1",
_tpl: "5aa2ba71e5b5b000137b758f",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "677c209ebb45445ebb42c405", _id: "c6ad1be7e8401755de69d6a0",
_tpl: "5649ab884bdc2ded0b8b457f", _tpl: "5d6d2ef3a4b93618084f58bd",
parentId: "911a0f04d5d9c7e239807ae0", parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "mod_muzzle", slotId: "hideout",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "8ada5c9cc26585281577c6eb", _id: "7c42d3dce0ddbc4806bce48b",
_tpl: "5649ae4a4bdc2d1b2b8b4588", _tpl: "5894a51286f77426d13baf02",
parentId: "911a0f04d5d9c7e239807ae0", parentId: "35111c9b72a87b6b7d95ad35",
slotId: "mod_pistol_grip", slotId: "mod_pistol_grip",
upd: { StackObjectsCount: 1 }, upd: {},
}, { }, {
_id: "4bd10f89836fd9f86aedcac1", _id: "10b97872c5f4e0e1949a0369",
_tpl: "5649af094bdc2df8348b4586", _tpl: "5c5db6742e2216000f1b2852",
parentId: "911a0f04d5d9c7e239807ae0", upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "35111c9b72a87b6b7d95ad35",
slotId: "mod_magazine",
}, {
_id: "a6cd9986dde4cabddcd2dce2",
_tpl: "5894a5b586f77426d2590767",
parentId: "35111c9b72a87b6b7d95ad35",
slotId: "mod_reciever", slotId: "mod_reciever",
upd: { StackObjectsCount: 1 }, upd: {},
}, { }, {
_id: "8b1327270791b142ac341b03", _id: "b65635b515712f990fdcc201",
_tpl: "5649d9a14bdc2d79388b4580", _tpl: "58ac1bf086f77420ed183f9f",
parentId: "911a0f04d5d9c7e239807ae0", upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "35111c9b72a87b6b7d95ad35",
slotId: "mod_stock",
}, {
_id: "0e11045873efe3625695c1ae",
_tpl: "5c5db6b32e221600102611a0",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "35111c9b72a87b6b7d95ad35",
slotId: "mod_charge",
}, {
_id: "94c4161abe8bf654fb986063",
_tpl: "57adff4f24597737f373b6e6",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "a6cd9986dde4cabddcd2dce2",
slotId: "mod_scope",
}, {
_id: "9b284ccfd0d535acec1ff58b",
_tpl: "5c5db5c62e22160012542255",
parentId: "a6cd9986dde4cabddcd2dce2",
slotId: "mod_barrel",
upd: {},
}, {
_id: "d730caa83a11fd01250a7261",
_tpl: "5c5db63a2e2216000f1b284a",
parentId: "a6cd9986dde4cabddcd2dce2",
slotId: "mod_handguard",
upd: {},
}, {
_id: "24291c7bcf91e362adb6d68b",
_tpl: "5fb6564947ce63734e3fa1da",
parentId: "a6cd9986dde4cabddcd2dce2",
slotId: "mod_sight_rear", slotId: "mod_sight_rear",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "566335b3df586f34b47f5e35", _id: "0d98fd0769cce8e473bbe540",
_tpl: "5649b2314bdc2d79388b4576", _tpl: "58d2664f86f7747fec5834f6",
parentId: "911a0f04d5d9c7e239807ae0",
slotId: "mod_stock",
upd: { StackObjectsCount: 1 },
}, {
_id: "da8cde1b3024c336f6e06152",
_tpl: "55d482194bdc2d1d4e8b456b",
parentId: "911a0f04d5d9c7e239807ae0",
slotId: "mod_magazine",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } }, upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "94c4161abe8bf654fb986063",
slotId: "mod_scope",
}, { }, {
_id: "1e0b177df108c0c117028812", _id: "11b174510f039e8217fbd202",
_tpl: "57cffddc24597763133760c6", _tpl: "58d268fc86f774111273f8c2",
parentId: "c9278dd8251e99578bf7a274", upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
slotId: "mod_handguard", parentId: "0d98fd0769cce8e473bbe540",
slotId: "mod_scope",
}, {
_id: "c435230e530574b1d7c32300",
_tpl: "5c7e8fab2e22165df16b889b",
parentId: "9b284ccfd0d535acec1ff58b",
slotId: "mod_muzzle",
upd: {},
}, {
_id: "15666fe6fd2d95206612e418",
_tpl: "6269220d70b6c02e665f2635",
parentId: "d730caa83a11fd01250a7261",
slotId: "mod_mount_000",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "bc041c0011d76f714b898400", _id: "a54de8b9014eee71fdf1d01d",
_tpl: "57cffcd624597763133760c5", _tpl: "6269220d70b6c02e665f2635",
parentId: "1e0b177df108c0c117028812", parentId: "d730caa83a11fd01250a7261",
slotId: "mod_mount_001",
upd: { StackObjectsCount: 1 },
}, {
_id: "c34555bc95a9a7a23150a36f",
_tpl: "6269220d70b6c02e665f2635",
parentId: "d730caa83a11fd01250a7261",
slotId: "mod_mount_002",
upd: { StackObjectsCount: 1 },
}, {
_id: "91cae4ae30d1366b87158238",
_tpl: "6269220d70b6c02e665f2635",
parentId: "d730caa83a11fd01250a7261",
slotId: "mod_mount_003", slotId: "mod_mount_003",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "9f8d7880a6e0a47a211ec5d3", _id: "48f23df4509164cf397b9ab5",
_tpl: "58491f3324597764bc48fa02", _tpl: "6269220d70b6c02e665f2635",
parentId: "8b1327270791b142ac341b03", parentId: "d730caa83a11fd01250a7261",
slotId: "mod_scope", slotId: "mod_mount_004",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "402b4086535a50ef7d9cef88", _id: "a55f05f689978ac65c7da654",
_tpl: "5649be884bdc2d79388b4577", _tpl: "5b7be4895acfc400170e2dd5",
parentId: "566335b3df586f34b47f5e35", parentId: "d730caa83a11fd01250a7261",
slotId: "mod_stock", slotId: "mod_foregrip",
upd: {},
}, {
_id: "8ae4ea81a2d6074162d87a9c",
_tpl: "5b7be47f5acfc400170e2dd2",
parentId: "d730caa83a11fd01250a7261",
slotId: "mod_mount_005",
upd: {},
}, {
_id: "312cc0f6687963305457235e",
_tpl: "5b7be47f5acfc400170e2dd2",
parentId: "d730caa83a11fd01250a7261",
slotId: "mod_mount_006",
upd: {},
}, {
_id: "e1e5aaf474b7282a52ac9a14",
_tpl: "5fb6567747ce63734e3fa1dc",
parentId: "d730caa83a11fd01250a7261",
slotId: "mod_sight_front",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "db2ef9442178910eba985b51", _id: "bb9a34648e08f005db5d7484",
_tpl: "58d2946386f774496974c37e", _tpl: "5cc9c20cd7f00c001336c65d",
parentId: "402b4086535a50ef7d9cef88",
slotId: "mod_stock_000",
upd: { StackObjectsCount: 1 },
}, {
_id: "3c32b7d47ad80e83749fa906",
_tpl: "58d2912286f7744e27117493",
parentId: "db2ef9442178910eba985b51",
slotId: "mod_stock",
upd: { StackObjectsCount: 1 },
}, {
_id: "574a9b5535585255cde19570",
_tpl: "55d482194bdc2d1d4e8b456b",
parentId: "695b13896108f765e8985698",
slotId: "1",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } }, upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "15666fe6fd2d95206612e418",
slotId: "mod_tactical",
}, { }, {
_id: "696835b2badfb96623ea887c", _id: "dd9ac99d3ea4c9656221bcc9",
_tpl: "55d482194bdc2d1d4e8b456b", _tpl: "5cc9c20cd7f00c001336c65d",
parentId: "695b13896108f765e8985698", upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "a54de8b9014eee71fdf1d01d",
slotId: "mod_tactical",
}, {
_id: "b22748de8da5f3c1362dd8e0",
_tpl: "5cc9c20cd7f00c001336c65d",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "c34555bc95a9a7a23150a36f",
slotId: "mod_tactical",
}, {
_id: "e3cc1be8954c4889f94b435a",
_tpl: "5cc9c20cd7f00c001336c65d",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "91cae4ae30d1366b87158238",
slotId: "mod_tactical",
}, {
_id: "e73f05be5a306168e847da82",
_tpl: "5cc9c20cd7f00c001336c65d",
parentId: "48f23df4509164cf397b9ab5",
slotId: "mod_tactical",
upd: { StackObjectsCount: 1 },
}, {
_id: "847cf35ec92d8af8e4814ea8",
_tpl: "5c1cd46f2e22164bef5cfedb",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "a55f05f689978ac65c7da654",
slotId: "mod_foregrip",
}, {
_id: "bb4b7a4475fea0f0135305f6",
_tpl: "5cc9c20cd7f00c001336c65d",
parentId: "8ae4ea81a2d6074162d87a9c",
slotId: "mod_tactical",
upd: { StackObjectsCount: 1 },
}, {
_id: "d0ac8e688a0bb17668589909",
_tpl: "5cc9c20cd7f00c001336c65d",
parentId: "312cc0f6687963305457235e",
slotId: "mod_tactical",
upd: { StackObjectsCount: 1 },
}, {
_id: "5dbcf8cbbb3f8ef669836320",
_tpl: "5c793fc42e221600114ca25d",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "b65635b515712f990fdcc201",
slotId: "mod_stock",
}, {
_id: "f996645c809968f8033593a6",
_tpl: "5fc2369685fd526b824a5713",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5dbcf8cbbb3f8ef669836320",
slotId: "mod_stock_000",
}, {
_id: "7d959c20811fdc440387f0a4",
_tpl: "55d447bb4bdc2d892f8b456f",
parentId: "d45436a159654f43ca3aa52f",
slotId: "mod_barrel",
upd: {},
}, {
_id: "16969c588bd20e223d93e65a",
_tpl: "611a31ce5b7ffe001b4649d1",
parentId: "d45436a159654f43ca3aa52f",
slotId: "mod_stock",
upd: {},
}, {
_id: "d3a31aa632d852bfe57d7aca",
_tpl: "5a6b5f868dc32e000a311389",
parentId: "2c60ad9b6051f059ab796aa6",
slotId: "mod_barrel",
upd: {},
}, {
_id: "fdba343644672594e7c73f47",
_tpl: "5a7b4960e899ef197b331a2d",
parentId: "2c60ad9b6051f059ab796aa6",
slotId: "mod_pistol_grip",
upd: {},
}, {
_id: "5f47943e00d184b3c8f9c2b5",
_tpl: "5a6f5e048dc32e00094b97da",
parentId: "2c60ad9b6051f059ab796aa6",
slotId: "mod_reciever",
upd: {},
}, {
_id: "f616853cb3b860d670252e66",
_tpl: "5a718b548dc32e000d46d262",
parentId: "2c60ad9b6051f059ab796aa6",
slotId: "mod_magazine",
upd: {},
}, {
_id: "5153ee12f6d4abc4856dd4ae",
_tpl: "5a7ad74e51dfba0015068f45",
parentId: "2c60ad9b6051f059ab796aa6",
slotId: "mod_tactical",
upd: {},
}, {
_id: "aa5dc438d849a311e335667b",
_tpl: "5a7d9122159bd4001438dbf4",
parentId: "5f47943e00d184b3c8f9c2b5",
slotId: "mod_sight_rear",
upd: {},
}, {
_id: "79a1dfa8bff1b7ca118d6b0f",
_tpl: "5a7d90eb159bd400165484f1",
parentId: "5f47943e00d184b3c8f9c2b5",
slotId: "mod_sight_front",
upd: {},
}, {
_id: "e784a6d774f9a885bf5ed847",
_tpl: "5a7b483fe899ef0016170d15",
parentId: "5153ee12f6d4abc4856dd4ae",
slotId: "mod_tactical",
upd: {},
}, {
_id: "fd79789b0e394e2cc1299ab1",
_tpl: "5c5db6742e2216000f1b2852",
parentId: "5d2be23efb34d0d1da9d3701",
slotId: "1",
upd: { StackObjectsCount: 1 },
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "a19f5e338bfd32f1c1f3fb73",
_tpl: "5c5db6742e2216000f1b2852",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5d2be23efb34d0d1da9d3701",
slotId: "2", slotId: "2",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true }, location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, { }, {
_id: "c2d5e23c7886e8ff02010731", _id: "9e709808929c226f7bdbf57a",
_tpl: "55d482194bdc2d1d4e8b456b", _tpl: "5c5db6742e2216000f1b2852",
parentId: "695b13896108f765e8985698", upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5d2be23efb34d0d1da9d3701",
slotId: "3", slotId: "3",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true }, location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, { }, {
_id: "306de2f475a559610a4f6f1d", _id: "9fe70bf25a2db7f8c1b23502",
_tpl: "55d482194bdc2d1d4e8b456b", _tpl: "5c5db6742e2216000f1b2852",
parentId: "695b13896108f765e8985698", upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5d2be23efb34d0d1da9d3701",
slotId: "4", slotId: "4",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true }, location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, { }, {
_id: "eb0445b49a97e84e27d47f3c", _id: "4519dc962deebb2dbfc9e70c",
_tpl: "5aa2ba71e5b5b000137b758f", _tpl: "5c5db6742e2216000f1b2852",
parentId: "695b13896108f765e8985698", upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5d2be23efb34d0d1da9d3701",
slotId: "5",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "a94275912f1cbcd483563916",
_tpl: "5c5db6742e2216000f1b2852",
parentId: "5d2be23efb34d0d1da9d3701",
slotId: "6",
upd: { StackObjectsCount: 1 },
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "7f2ae8c0685bf3a2195185dd",
_tpl: "5c5db6742e2216000f1b2852",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5d2be23efb34d0d1da9d3701",
slotId: "7",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "53a9f3dc5c08cbd02ff31b12",
_tpl: "5c5db6742e2216000f1b2852",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5d2be23efb34d0d1da9d3701",
slotId: "8",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "e0ab45585b1a874dbaa68fb3",
_tpl: "5c5db6742e2216000f1b2852",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5d2be23efb34d0d1da9d3701",
slotId: "9",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "3f66f7abde039a848f8b4cf0",
_tpl: "5c5db6742e2216000f1b2852",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5d2be23efb34d0d1da9d3701",
slotId: "10",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "04a202a45f8a39b61a58a05a",
_tpl: "544a5caa4bdc2d1a388b4568",
parentId: "9f601faab37dcc58190898ac",
slotId: "main",
upd: { StackObjectsCount: 1 },
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "d5f6e03c07ede944e89fb407",
_tpl: "6570e83223c1f638ef0b0ede",
parentId: "04a202a45f8a39b61a58a05a",
slotId: "Soft_armor_front",
upd: { Repairable: { Durability: 42, MaxDurability: 42 } },
}, {
_id: "5433cbf0f07a68651e888c74",
_tpl: "6570e87c23c1f638ef0b0ee2",
parentId: "04a202a45f8a39b61a58a05a",
slotId: "Soft_armor_back",
upd: { Repairable: { Durability: 42, MaxDurability: 42 } },
}, {
_id: "da91bed43f688a80b627ad4d",
_tpl: "6570e90b3a5689d85f08db97",
parentId: "04a202a45f8a39b61a58a05a",
slotId: "Groin",
upd: { Repairable: { Durability: 28, MaxDurability: 28 } },
}, {
_id: "ad7f524f3de9ad544df8c0b8",
_tpl: "5c5db6742e2216000f1b2852",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "9f601faab37dcc58190898ac",
slotId: "main",
location: { x: 3, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "0844cf6b7a89c13454b6e3db",
_tpl: "5c5db6742e2216000f1b2852",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "9f601faab37dcc58190898ac",
slotId: "main",
location: { x: 4, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "c3f58f44366d0a06d29d66ba",
_tpl: "5a38e6bac4a2826c6e06d79b",
parentId: "9f601faab37dcc58190898ac",
slotId: "main",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
location: { x: 0, y: 4, r: "Horizontal", isSearched: true },
}, {
_id: "d46e16faba780c68e0600532",
_tpl: "656fa0fb498d1b7e3e071d9c",
parentId: "9f601faab37dcc58190898ac",
slotId: "main",
upd: { StackObjectsCount: 1, Repairable: { Durability: 45, MaxDurability: 45 } },
location: { x: 3, y: 2, r: "Horizontal", isSearched: true },
}, {
_id: "26598f88d49198c4a0a9391c",
_tpl: "571a12c42459771f627b58a0",
parentId: "9f601faab37dcc58190898ac",
slotId: "main",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
location: { x: 3, y: 4, r: "Horizontal", isSearched: true },
}, {
_id: "5ee8e16837809adc34caae00",
_tpl: "656f9fa0498d1b7e3e071d98",
parentId: "04a202a45f8a39b61a58a05a",
slotId: "Front_plate",
upd: { Repairable: { Durability: 50, MaxDurability: 50 } },
}, {
_id: "de042f9ebf0fd9ad451033d4",
_tpl: "656f9fa0498d1b7e3e071d98",
parentId: "04a202a45f8a39b61a58a05a",
slotId: "Back_plate",
upd: { Repairable: { Durability: 50, MaxDurability: 50 } },
}, {
_id: "03de471c2a3faa359aca7486",
_tpl: "5c5db6742e2216000f1b2852",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "04a202a45f8a39b61a58a05a",
slotId: "1",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "091c85804613176da9478edd",
_tpl: "5c5db6742e2216000f1b2852",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "04a202a45f8a39b61a58a05a",
slotId: "2",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "5482888e242a98ff154c0ee8",
_tpl: "5c5db6742e2216000f1b2852",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "04a202a45f8a39b61a58a05a",
slotId: "3",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "3c8e206a2c2e9b0fee45b56b",
_tpl: "5c5db6742e2216000f1b2852",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "04a202a45f8a39b61a58a05a",
slotId: "4",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "b2405216e5730f3511884a10",
_tpl: "5ea17ca01412a1425304d1c0",
parentId: "04a202a45f8a39b61a58a05a",
slotId: "5", slotId: "5",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
location: { x: 0, y: 0, r: "Horizontal", isSearched: true }, location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, { }, {
_id: "fad89a5bdfd23e3248123346", _id: "7a0675280dbbad69ce592d74",
_tpl: "5fc5396e900b1d5091531e72", _tpl: "657f9a55c6679fefb3051e19",
parentId: "695b13896108f765e8985698", parentId: "b2405216e5730f3511884a10",
slotId: "Helmet_top",
upd: { Repairable: { Durability: 24, MaxDurability: 24 } },
}, {
_id: "c0c182942f54d3c183f0e179",
_tpl: "657f9a94ada5fadd1f07a589",
parentId: "b2405216e5730f3511884a10",
slotId: "Helmet_back",
upd: { Repairable: { Durability: 24, MaxDurability: 24 } },
}, {
_id: "8ec4534a4fe96f89ea88c107",
_tpl: "5c165d832e2216398b5a7e36",
parentId: "04a202a45f8a39b61a58a05a",
slotId: "6", slotId: "6",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, {
_id: "b16c2a938954cd69c687c51a",
_tpl: "5b4736b986f77405cb415c10",
parentId: "695b13896108f765e8985698",
slotId: "7",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true }, location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, { }, {
_id: "a2b3019ac8d340eeb068d429", _id: "0d91ed3d44881d33b1fd94ec",
_tpl: "5c5db6742e2216000f1b2852",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "04a202a45f8a39b61a58a05a",
slotId: "11",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "f7066fdfeefb29eca1d2dbeb",
_tpl: "5ea18c84ecf1982c7712d9a2", _tpl: "5ea18c84ecf1982c7712d9a2",
parentId: "695b13896108f765e8985698", upd: { StackObjectsCount: 1, Repairable: { Durability: 22, MaxDurability: 25 } },
slotId: "10", parentId: "b2405216e5730f3511884a10",
location: { x: 0, y: 0, r: "Vertical", isSearched: true }, slotId: "mod_nvg",
upd: { StackObjectsCount: 1, Repairable: { Durability: 29, MaxDurability: 33 } },
}, { }, {
_id: "0b3c5d183e8b506d655f85c4", _id: "ee0ec86e9608abe773175e3a",
_tpl: "644a3df63b0b6f03e101e065", _tpl: "5c0558060db834001b735271",
parentId: "fad89a5bdfd23e3248123346", parentId: "f7066fdfeefb29eca1d2dbeb",
slotId: "mod_tactical", slotId: "mod_nvg",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, {
_id: "757211a0b648fe27b0475ded",
_tpl: "59f8a37386f7747af3328f06",
parentId: "b16c2a938954cd69c687c51a",
slotId: "mod_foregrip",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "870a887c63ca30fb15736b3d", _id: "0515d1e589fd626b504e59cd",
_tpl: "62a1b7fbc30cfa1d366af586", _tpl: "5a38ee51c4a282000c5a955c",
parentId: "bb49d6ceb3e87d8563a06455", parentId: "c3f58f44366d0a06d29d66ba",
slotId: "main",
upd: { StackObjectsCount: 1 },
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "f3de631a1bb2b74bd0160d9a",
_tpl: "5d6d3be5a4b9361bc73bc763",
parentId: "bb49d6ceb3e87d8563a06455",
slotId: "main",
location: { x: 5, y: 0, r: "Vertical", isSearched: true },
upd: { StackObjectsCount: 1, Repairable: { Durability: 22.41, MaxDurability: 22.41 } },
}, {
_id: "351180f3248d45c71cb2ebdc",
_tpl: "57c44b372459772d2b39b8ce",
parentId: "870a887c63ca30fb15736b3d",
slotId: "main",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, {
_id: "7237f722106866f2df8dc8d1",
_tpl: "56e33680d2720be2748b4576",
parentId: "870a887c63ca30fb15736b3d",
slotId: "main",
location: { x: 0, y: 3, r: "Horizontal", isSearched: true },
upd: { StackObjectsCount: 1 },
}, {
_id: "d0cf00aff56ea520cdd94330",
_tpl: "57c44dd02459772d2e0ae249",
parentId: "351180f3248d45c71cb2ebdc",
slotId: "mod_muzzle",
upd: { StackObjectsCount: 1 },
}, {
_id: "5119653b2c66d57ee219e26f",
_tpl: "57c44f4f2459772d2c627113",
parentId: "351180f3248d45c71cb2ebdc",
slotId: "mod_reciever",
upd: { StackObjectsCount: 1 },
}, {
_id: "ed1ac0183a8af587110aa74e",
_tpl: "5a9e81fba2750c00164f6b11",
parentId: "351180f3248d45c71cb2ebdc",
slotId: "mod_magazine", slotId: "mod_magazine",
upd: { StackObjectsCount: 1 }, upd: {},
}, { }, {
_id: "310a7d1bb07ae0e522f3f8e3", _id: "cb30ae6f997a2e6d119f2186",
_tpl: "5a69a2ed8dc32e000d46d1f1", _tpl: "5a38ef1fc4a282000b1521f6",
parentId: "351180f3248d45c71cb2ebdc", parentId: "c3f58f44366d0a06d29d66ba",
slotId: "mod_pistol_grip",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, {
_id: "8a7e3489197b3b98126447fd",
_tpl: "6130ca3fd92c473c77020dbd",
parentId: "351180f3248d45c71cb2ebdc",
slotId: "mod_charge",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, {
_id: "e818616e11ae07aa05388759",
_tpl: "5dff8db859400025ea5150d4",
parentId: "351180f3248d45c71cb2ebdc",
slotId: "mod_mount_000",
upd: { StackObjectsCount: 1 },
}, {
_id: "768812984debf2756bece089",
_tpl: "57c44e7b2459772d28133248",
parentId: "d0cf00aff56ea520cdd94330",
slotId: "mod_sight_rear",
upd: { StackObjectsCount: 1 },
}, {
_id: "67c610585ed668baf4604931",
_tpl: "59eb7ebe86f7740b373438ce",
parentId: "d0cf00aff56ea520cdd94330",
slotId: "mod_mount_000",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, {
_id: "80e9dffa49bfe263ab0128c7",
_tpl: "6267c6396b642f77f56f5c1c",
parentId: "67c610585ed668baf4604931",
slotId: "mod_tactical_000",
upd: { StackObjectsCount: 1 },
}, {
_id: "dee323443ce23ba8c54b9f1c",
_tpl: "5cc9c20cd7f00c001336c65d",
parentId: "67c610585ed668baf4604931",
slotId: "mod_tactical_001",
upd: { StackObjectsCount: 1 },
}, {
_id: "3008088022dd55f1c99e5a32",
_tpl: "5c1cd46f2e22164bef5cfedb",
parentId: "67c610585ed668baf4604931",
slotId: "mod_foregrip",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, {
_id: "71e9f8d005c72940d857fe64",
_tpl: "59d790f486f77403cb06aec6",
parentId: "80e9dffa49bfe263ab0128c7",
slotId: "mod_flashlight",
upd: { StackObjectsCount: 1 },
}, {
_id: "8c610c6cc67115a5fc1662ff",
_tpl: "56eabf3bd2720b75698b4569",
parentId: "310a7d1bb07ae0e522f3f8e3",
slotId: "mod_stock_000",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, {
_id: "9bf01177f0c1e346b2d65373",
_tpl: "58d2912286f7744e27117493",
parentId: "8c610c6cc67115a5fc1662ff",
slotId: "mod_stock", slotId: "mod_stock",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } }, upd: { Foldable: { Folded: true } },
}, { }, {
_id: "7dd43ffa6e03c2da6cddc56e", _id: "be57a04835a8c1ae85811949",
_tpl: "6171407e50224f204c1da3c5", _tpl: "5a38eecdc4a282329a73b512",
parentId: "e818616e11ae07aa05388759", parentId: "cb30ae6f997a2e6d119f2186",
slotId: "mod_scope", slotId: "mod_pistol_grip",
upd: { StackObjectsCount: 1 }, upd: {},
}, { }, {
_id: "fa9da4ccf3630cb173c293f9", _id: "fd6ef6e377e6280ca9386dbc",
_tpl: "5b3b99475acfc432ff4dcbee", _tpl: "571a26d524597720680fbe8a",
parentId: "7dd43ffa6e03c2da6cddc56e", parentId: "26598f88d49198c4a0a9391c",
slotId: "mod_scope_000", slotId: "mod_barrel",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } }, upd: {},
}, { }, {
_id: "6e2727806fb12e12123e9a57", _id: "5c40aff0d1c5d4f206123b83",
_tpl: "616554fe50224f204c1da2aa", _tpl: "571a282c2459771fb2755a69",
parentId: "7dd43ffa6e03c2da6cddc56e", parentId: "26598f88d49198c4a0a9391c",
slotId: "mod_scope_001", slotId: "mod_pistol_grip",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } }, upd: {},
}, { }, {
_id: "2c868d4676adc934f897e9a7", _id: "34b2c7cf0f6b8f484411cebf",
_tpl: "61605d88ffa6e502ac5e7eeb", _tpl: "571a29dc2459771fb2755a6a",
parentId: "7dd43ffa6e03c2da6cddc56e", parentId: "26598f88d49198c4a0a9391c",
slotId: "mod_scope_002", slotId: "mod_magazine",
upd: { StackObjectsCount: 1 }, upd: {},
}, { }, {
_id: "1b159fdc14c350f8a4a7e19e", _id: "9932dd0e1339053e27d54a41",
_tpl: "58d39b0386f77443380bf13c", _tpl: "654a4dea7c17dec2f50cc86a",
parentId: "6e2727806fb12e12123e9a57", parentId: "f74d377063e65d350e0099be",
slotId: "mod_scope", slotId: "Front_plate",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } }, upd: { Repairable: { Durability: 50, MaxDurability: 50 } },
}, { }, {
_id: "7691790ffc5290da292cab99", _id: "0f69c261881206320d8f583d",
_tpl: "61657230d92c473c770213d7", _tpl: "657b22485f444d6dff0c6c2f",
parentId: "1b159fdc14c350f8a4a7e19e", parentId: "f74d377063e65d350e0099be",
slotId: "mod_scope", slotId: "Back_plate",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } }, upd: { StackObjectsCount: 1, Repairable: { Durability: 4.681114, MaxDurability: 40 } },
}, {
_id: "012a11e7dcb1280a1ab9d2f6",
_tpl: "618168b350224f204c1da4d8",
parentId: "7237f722106866f2df8dc8d1",
slotId: "main",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
upd: { StackObjectsCount: 1 },
}, {
_id: "38ca7415a458c4d22ba2f3c3",
_tpl: "6130c43c67085e45ef1405a1",
parentId: "012a11e7dcb1280a1ab9d2f6",
slotId: "mod_muzzle",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, {
_id: "c5a0621ebf856ce1b0945efc",
_tpl: "61816fcad92c473c770215cc",
parentId: "012a11e7dcb1280a1ab9d2f6",
slotId: "mod_sight_front",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, {
_id: "a74677b17c1c49edc002df9b",
_tpl: "5dfa3d2b0dee1b22f862eade",
parentId: "38ca7415a458c4d22ba2f3c3",
slotId: "mod_muzzle",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}], }],
}, { }, {
scheduledTime: 1698945140, scheduledTime: 1712896726,
traderId: "54cb57776803fa99248b456e", // Therapist traderId: "54cb57776803fa99248b456e",
messageContent: {
templateId: "58fe0e3486f77471f772c3f2 2",
type: 8,
maxStorageTime: 518400, maxStorageTime: 518400,
text: "", systemData: { date: "11.04.2024", time: "19:19", location: "factory4_day" },
profileChangeEvents: [], messageType: 8,
systemData: { date: "01.11.2023", time: "11:18", location: "factory4_day" }, messageTemplateId: "58fe0e3486f77471f772c3f2 3",
},
items: [{ items: [{
_id: "5ae1c2b99a0a339adc620148", _id: "5cfe91bfe022641c19bc8c60",
_tpl: "5cebec38d7f00c00110a652a", _tpl: "5aafa857e5b5b00018480968",
parentId: "ad018df9da0cbf2726394ef1", upd: {
slotId: "mod_mount_000", StackObjectsCount: 1,
upd: { StackObjectsCount: 1 }, sptPresetId: "5ac4ad3686f774181345c3da",
}, { Repairable: { Durability: 98.33, MaxDurability: 98.33 },
_id: "30f4bcb87bcc4604e27c02c1", },
_tpl: "5cc70146e4a949000d73bf6b", parentId: "d2b3b859f667d4fd8b35bc96",
parentId: "ad018df9da0cbf2726394ef1",
slotId: "mod_mount_001",
upd: { StackObjectsCount: 1 },
}, {
_id: "ad018df9da0cbf2726394ef1",
_tpl: "5cc70102e4a949035e43ba74",
parentId: "3bc4ff5bd99f165dc75cbd25",
slotId: "main",
upd: { StackObjectsCount: 1 },
location: { x: 3, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "12c243bd6b3e486e61325f81",
_tpl: "5cc82d76e24e8d00134b4b83",
parentId: "5fe49444ae6628187a2e77b8",
slotId: "hideout", slotId: "hideout",
upd: { StackObjectsCount: 1, Repairable: { Durability: 99.93771, MaxDurability: 100 } },
}, { }, {
_id: "760652d86ee78eed513e0ad7", _id: "a5063619e7f4db123ca07fcc",
_tpl: "5ab8f39486f7745cd93a1cca", _tpl: "60db29ce99594040e04c4a27",
parentId: "5fe49444ae6628187a2e77b8", parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: {
FireMode: { FireMode: "single" },
StackObjectsCount: 1,
Repairable: { Durability: 100, MaxDurability: 100 },
},
}, {
_id: "3702c30b6333e28d6a15d62c",
_tpl: "56e0598dd2720bb5668b45a6",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, {
_id: "387c9f2b44d2da266f856b31",
_tpl: "6571bde39837cc51b800c212",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout", slotId: "hideout",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "61ab4afefac354dfc64c7874", _id: "275f046ea1a7b40046cd54fa",
_tpl: "5b432d215acfc4771e1c6624", _tpl: "5b40e4035acfc47a87740943",
parentId: "5fe49444ae6628187a2e77b8", parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: { StackObjectsCount: 1, Repairable: { Durability: 30, MaxDurability: 30 } },
}, {
_id: "285e9d9ae196ae4e336cd04f",
_tpl: "5d5d87f786f77427997cfaef",
parentId: "5fe49444ae6628187a2e77b8",
slotId: "hideout",
upd: { StackObjectsCount: 1, Repairable: { Durability: 75, MaxDurability: 80 } },
}, {
_id: "3bc4ff5bd99f165dc75cbd25",
_tpl: "5f5e467b0bc58666c37e7821",
parentId: "5fe49444ae6628187a2e77b8",
slotId: "hideout", slotId: "hideout",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "6bf5d8ee81a3c9aec21bbbad", _id: "f30858ff9924b1fe211dd1f7",
_tpl: "5d5fca1ea4b93635fd598c07", _tpl: "657f95bff92cd718b701550c",
parentId: "5fe49444ae6628187a2e77b8", parentId: "275f046ea1a7b40046cd54fa",
slotId: "Helmet_top",
upd: { Repairable: { Durability: 10.3212032, MaxDurability: 18 } },
}, {
_id: "eec1072ac0cc44984e1ed43b",
_tpl: "657f9605f4c82973640b2358",
parentId: "275f046ea1a7b40046cd54fa",
slotId: "Helmet_back",
upd: { Repairable: { Durability: 13.3160009, MaxDurability: 18 } },
}, {
_id: "b82495b01ad0bfe5dd7e864d",
_tpl: "5c0e746986f7741453628fe5",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout", slotId: "hideout",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "2371438cf809b5e483bf5d85", _id: "31531773990cd1aefa751db7",
_tpl: "5cc70093e4a949033c734312", _tpl: "6570df294cc0d2ab1e05ed74",
parentId: "12c243bd6b3e486e61325f81", parentId: "b82495b01ad0bfe5dd7e864d",
slotId: "Soft_armor_front",
upd: { Repairable: { Durability: 31.0571022, MaxDurability: 35 } },
}, {
_id: "eb8c6c7c671d2a2490454e7c",
_tpl: "6570df9c615f54368b04fca9",
parentId: "b82495b01ad0bfe5dd7e864d",
slotId: "Soft_armor_back",
upd: { Repairable: { Durability: 30.8, MaxDurability: 35 } },
}, {
_id: "f9cc99048aa37c5a4a837ef9",
_tpl: "5ca20d5986f774331e7c9602",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: { StackObjectsCount: 1 },
}, {
_id: "bd8a4a3783d80b81cc8655ee",
_tpl: "5aa2ba71e5b5b000137b758f",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: { StackObjectsCount: 1 },
}, {
_id: "025748ec34dcd1bfb2529537",
_tpl: "5c0d32fcd174af02a1659c75",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: { StackObjectsCount: 1 },
}, {
_id: "9c552e79f1ae38350afb3723",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5cfe91bfe022641c19bc8c60",
slotId: "mod_magazine", slotId: "mod_magazine",
upd: { StackObjectsCount: 1 },
}, { }, {
_id: "7f890346ea5b2cbc68c3170f", _id: "14cb5b7c9789876325670163",
_tpl: "5cc700b9e4a949000f0f0f25", _tpl: "5aaf8e43e5b5b00015693246",
parentId: "12c243bd6b3e486e61325f81", parentId: "5cfe91bfe022641c19bc8c60",
slotId: "mod_stock", slotId: "mod_stock",
upd: { StackObjectsCount: 1 }, upd: {},
}, { }, {
_id: "12fb79a9c4929009ff8d89e1", _id: "0e4c9e11000589751523a62c",
_tpl: "5cc700ede4a949033c734315", _tpl: "5addbac75acfc400194dbc56",
parentId: "12c243bd6b3e486e61325f81", parentId: "5cfe91bfe022641c19bc8c60",
slotId: "mod_reciever",
upd: { StackObjectsCount: 1 },
}, {
_id: "d4c5274082ed716e19447f46",
_tpl: "5cc701d7e4a94900100ac4e7",
parentId: "12c243bd6b3e486e61325f81",
slotId: "mod_barrel", slotId: "mod_barrel",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "d819dd4d2b13de10e9d6d805", _id: "a63cf65e9646a04944d18106",
_tpl: "5cc6ea85e4a949000e1ea3c3", _tpl: "5abcbb20d8ce87001773e258",
parentId: "12c243bd6b3e486e61325f81",
slotId: "mod_charge",
upd: { StackObjectsCount: 1 },
}, {
_id: "fc9a664cacc477c4e725a81a",
_tpl: "5cc700d4e4a949000f0f0f28",
parentId: "7f890346ea5b2cbc68c3170f",
slotId: "mod_stock_000",
upd: { StackObjectsCount: 1 },
}, {
_id: "372891c593cf14e176b93ce2",
_tpl: "5cc7012ae4a949001252b43e",
parentId: "12fb79a9c4929009ff8d89e1",
slotId: "mod_mount_000",
upd: { StackObjectsCount: 1 },
}, {
_id: "bd196435a57bdc433df1e49d",
_tpl: "5cc7012ae4a949001252b43e",
parentId: "12fb79a9c4929009ff8d89e1",
slotId: "mod_mount_001",
upd: { StackObjectsCount: 1 },
}, {
_id: "ea3349d29797354d835c2192",
_tpl: "58491f3324597764bc48fa02",
parentId: "12fb79a9c4929009ff8d89e1",
slotId: "mod_scope",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } }, upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5cfe91bfe022641c19bc8c60",
slotId: "mod_sight_rear",
}, { }, {
_id: "4ccf7c74ca7d2167deb0ae5c", _id: "f67388e02546cd97c976d479",
_tpl: "626becf9582c3e319310b837", _tpl: "5addbfe15acfc4001a5fc58b",
parentId: "372891c593cf14e176b93ce2", parentId: "5cfe91bfe022641c19bc8c60",
slotId: "mod_tactical",
upd: { StackObjectsCount: 1 },
}, {
_id: "adfd3640fc93daf21c721ca6",
_tpl: "5cc9c20cd7f00c001336c65d",
parentId: "bd196435a57bdc433df1e49d",
slotId: "mod_tactical",
upd: { StackObjectsCount: 1 },
}, {
_id: "deeb36b1812790b0145d2532",
_tpl: "5a16badafcdbcb001865f72d",
parentId: "61ab4afefac354dfc64c7874",
slotId: "mod_equipment_000",
upd: { StackObjectsCount: 1, Repairable: { Durability: 12, MaxDurability: 25 } },
}, {
_id: "4c0e0548df904c384569190c",
_tpl: "5ea058e01dbce517f324b3e2",
parentId: "61ab4afefac354dfc64c7874",
slotId: "mod_nvg",
upd: { StackObjectsCount: 1, Repairable: { Durability: 3, MaxDurability: 39 } },
}, {
_id: "da82c293cabc705b30fef93a",
_tpl: "5a398ab9c4a282000c5a9842",
parentId: "61ab4afefac354dfc64c7874",
slotId: "mod_mount", slotId: "mod_mount",
upd: { StackObjectsCount: 1 },
}, {
_id: "bed3b1a2f866e18743db2a63",
_tpl: "5addbfbb5acfc400194dbcf7",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } }, upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "14cb5b7c9789876325670163",
slotId: "mod_mount",
}, { }, {
_id: "b8fc94611def6e9ba534a8b3", _id: "821a4953b87f562b3f435fd7",
_tpl: "5a16b8a9fcdbcb00165aa6ca", _tpl: "5649a2464bdc2d91118b45a8",
parentId: "4c0e0548df904c384569190c",
slotId: "mod_nvg",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } }, upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "bed3b1a2f866e18743db2a63",
slotId: "mod_scope",
}, { }, {
_id: "20d6193c1f399e6326ebbc10", _id: "24813deb9b9a6ec3ca8376ef",
_tpl: "5a16b93dfcdbcbcae6687261", _tpl: "5d10b49bd7ad1a1a560708b0",
parentId: "b8fc94611def6e9ba534a8b3",
slotId: "mod_nvg",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } }, upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, { parentId: "bed3b1a2f866e18743db2a63",
_id: "065c4f13b2bd8be266e1e809",
_tpl: "57235b6f24597759bf5a30f1",
parentId: "20d6193c1f399e6326ebbc10",
slotId: "mod_nvg",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 }, Togglable: { On: true } },
}, {
_id: "1883b955ab202fa099809278",
_tpl: "57d17c5e2459775a5c57d17d",
parentId: "da82c293cabc705b30fef93a",
slotId: "mod_flashlight",
upd: { StackObjectsCount: 1 },
}, {
_id: "e3c9e50ce31900c950b4ff6f",
_tpl: "5cc70093e4a949033c734312",
parentId: "285e9d9ae196ae4e336cd04f",
slotId: "1",
location: { x: 0, y: 0, r: "Vertical", isSearched: true },
upd: { StackObjectsCount: 1 },
}, {
_id: "193259b5eb848af4d036bee5",
_tpl: "5cc70093e4a949033c734312",
parentId: "285e9d9ae196ae4e336cd04f",
slotId: "2",
location: { x: 0, y: 0, r: "Vertical", isSearched: true },
upd: { StackObjectsCount: 1 },
}, {
_id: "f97ce69443f63bbe8f8097a7",
_tpl: "5cc70093e4a949033c734312",
parentId: "285e9d9ae196ae4e336cd04f",
slotId: "3",
location: { x: 0, y: 0, r: "Vertical", isSearched: true },
upd: { StackObjectsCount: 1 },
}, {
_id: "5d1c154a8abcfa934e477ac4",
_tpl: "5cc70093e4a949033c734312",
parentId: "285e9d9ae196ae4e336cd04f",
slotId: "4",
location: { x: 0, y: 0, r: "Vertical", isSearched: true },
upd: { StackObjectsCount: 1 },
}, {
_id: "289f7af841690c5388095477",
_tpl: "5cc70093e4a949033c734312",
parentId: "285e9d9ae196ae4e336cd04f",
slotId: "5",
location: { x: 0, y: 0, r: "Vertical", isSearched: true },
upd: { StackObjectsCount: 1 },
}, {
_id: "3e6d578165b61aef9865f677",
_tpl: "5cc70093e4a949033c734312",
parentId: "285e9d9ae196ae4e336cd04f",
slotId: "6",
location: { x: 0, y: 0, r: "Vertical", isSearched: true },
upd: { StackObjectsCount: 1 },
}, {
_id: "338682523f8504f97f84f3ab",
_tpl: "5cc70093e4a949033c734312",
parentId: "285e9d9ae196ae4e336cd04f",
slotId: "7",
location: { x: 0, y: 0, r: "Vertical", isSearched: true },
upd: { StackObjectsCount: 1 },
}, {
_id: "6d18ac01aa04b16e4f0d5d2f",
_tpl: "5cc70093e4a949033c734312",
parentId: "285e9d9ae196ae4e336cd04f",
slotId: "8",
location: { x: 0, y: 0, r: "Vertical", isSearched: true },
upd: { StackObjectsCount: 1 },
}, {
_id: "ac4ed54d61daa0c5219f8522",
_tpl: "5cc70093e4a949033c734312",
parentId: "285e9d9ae196ae4e336cd04f",
slotId: "9",
location: { x: 0, y: 0, r: "Vertical", isSearched: true },
upd: { StackObjectsCount: 1 },
}, {
_id: "2460460ef3d3df5c1ce07edb",
_tpl: "5cc70093e4a949033c734312",
parentId: "285e9d9ae196ae4e336cd04f",
slotId: "10",
location: { x: 0, y: 0, r: "Vertical", isSearched: true },
upd: { StackObjectsCount: 1 },
}, {
_id: "3aeb18aac0b532f34255f162",
_tpl: "5cc70146e4a949000d73bf6b",
parentId: "285e9d9ae196ae4e336cd04f",
slotId: "11",
upd: { StackObjectsCount: 1 },
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "bdb46107abbf1d92edaaf14e",
_tpl: "6272379924e29f06af4d5ecb",
parentId: "3aeb18aac0b532f34255f162",
slotId: "mod_tactical", slotId: "mod_tactical",
upd: { StackObjectsCount: 1 },
}, { }, {
_id: "0caadd8507a36d9ea871e88e", _id: "4194116ceb7e9e623cba4e89",
_tpl: "5ab8f04f86f774585f4237d8", _tpl: "609bab8b455afd752b2e6138",
parentId: "3bc4ff5bd99f165dc75cbd25", upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
slotId: "main", parentId: "821a4953b87f562b3f435fd7",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true }, slotId: "mod_scope",
upd: { StackObjectsCount: 1 },
}, { }, {
_id: "240046eebc9040c1d7e58611", _id: "3bd8b76ba1ff8f1cf954af91",
_tpl: "5ac66d015acfc400180ae6e4", _tpl: "59bffc1f86f77435b128b872",
parentId: "0caadd8507a36d9ea871e88e", parentId: "0e4c9e11000589751523a62c",
slotId: "main",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
upd: {
StackObjectsCount: 1,
sptPresetId: "5acf7dfc86f774401e19c390",
Repairable: { Durability: 32, MaxDurability: 59 },
Foldable: { Folded: true },
},
}, {
_id: "70b23c628fa17699d9a71e94",
_tpl: "59c6633186f7740cf0493bb9",
parentId: "240046eebc9040c1d7e58611",
slotId: "mod_gas_block",
upd: {
StackObjectsCount: 1,
sptPresetId: "5acf7dfc86f774401e19c390",
Repairable: { Durability: 32, MaxDurability: 59 },
},
}, {
_id: "7cc2e24dc6bc0716bdddc472",
_tpl: "5943ee5a86f77413872d25ec",
parentId: "240046eebc9040c1d7e58611",
slotId: "mod_muzzle", slotId: "mod_muzzle",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, {
_id: "7a51ebbad703082660d59d27",
_tpl: "5649ade84bdc2d1b2b8b4587",
parentId: "240046eebc9040c1d7e58611",
slotId: "mod_pistol_grip",
upd: {
StackObjectsCount: 1,
sptPresetId: "5acf7dfc86f774401e19c390",
Repairable: { Durability: 32, MaxDurability: 59 },
},
}, {
_id: "b481bc57436ed9a0c3abe7f3",
_tpl: "5d2c76ed48f03532f2136169",
parentId: "240046eebc9040c1d7e58611",
slotId: "mod_reciever",
upd: { StackObjectsCount: 1 }, upd: { StackObjectsCount: 1 },
}, { }, {
_id: "5774ef80597c7f91bff40dbb", _id: "3d953b4a4283363d0494d614",
_tpl: "5ac50c185acfc400163398d4", _tpl: "59bffbb386f77435b379b9c2",
parentId: "240046eebc9040c1d7e58611", upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
slotId: "mod_stock", parentId: "3bd8b76ba1ff8f1cf954af91",
upd: { slotId: "mod_muzzle",
StackObjectsCount: 1,
sptPresetId: "5acf7dfc86f774401e19c390",
Repairable: { Durability: 32, MaxDurability: 59 },
},
}, { }, {
_id: "8b7c8e6ba172ac390c99a2ae", _id: "4b2c9fb752a7c3458e07a35d",
_tpl: "5ac66c5d5acfc4001718d314", _tpl: "626bb8532c923541184624b4",
parentId: "240046eebc9040c1d7e58611", upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "f67388e02546cd97c976d479",
slotId: "mod_scope",
}, {
_id: "ce39864f056a3ad649d77243",
_tpl: "60dc519adf4c47305f6d410d",
parentId: "a5063619e7f4db123ca07fcc",
slotId: "mod_magazine", slotId: "mod_magazine",
upd: { StackObjectsCount: 1 }, upd: {},
}, { }, {
_id: "1ed3a416b1fc7adbed1160df", _id: "4ffc8f4e80708f6b9336c224",
_tpl: "6130ca3fd92c473c77020dbd", _tpl: "612368f58b401f4f51239b33",
parentId: "240046eebc9040c1d7e58611", parentId: "a5063619e7f4db123ca07fcc",
slotId: "mod_charge", slotId: "mod_barrel",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } }, upd: {},
}, { }, {
_id: "bbe087661947c0d9c1cde146", _id: "b401a4ce551dfcb0602e4073",
_tpl: "5648b1504bdc2d9d488b4584", _tpl: "612781056f3d944a17348d60",
parentId: "70b23c628fa17699d9a71e94", parentId: "a5063619e7f4db123ca07fcc",
slotId: "mod_stock",
upd: {},
}, {
_id: "b206761507a97037d05f0268",
_tpl: "6123649463849f3d843da7c4",
parentId: "a5063619e7f4db123ca07fcc",
slotId: "mod_handguard", slotId: "mod_handguard",
upd: {},
}, {
_id: "25af8615f5c902fd5920965f",
_tpl: "619d36da53b4d42ee724fae4",
parentId: "4ffc8f4e80708f6b9336c224",
slotId: "mod_muzzle",
upd: {},
}, {
_id: "38c5e9751e1d69d4d0804a49",
_tpl: "5448c12b4bdc2d02308b456f",
parentId: "3702c30b6333e28d6a15d62c",
slotId: "mod_magazine",
upd: {},
}, {
_id: "54da5dc9656bb9477eb16c88",
_tpl: "56e05b06d2720bb2668b4586",
parentId: "3702c30b6333e28d6a15d62c",
slotId: "mod_muzzle",
upd: {},
}, {
_id: "ad690f7145984b942288457f",
_tpl: "56e05a6ed2720bd0748b4567",
parentId: "3702c30b6333e28d6a15d62c",
slotId: "mod_pistolgrip",
upd: {},
}, {
_id: "c4ffff33f0a5f48c9500699a",
_tpl: "656fa0fb498d1b7e3e071d9c",
parentId: "b82495b01ad0bfe5dd7e864d",
slotId: "Front_plate",
upd: { Repairable: { Durability: 31.1713047, MaxDurability: 45 } },
}, {
_id: "8453961a28b572039197e140",
_tpl: "656fa0fb498d1b7e3e071d9c",
parentId: "b82495b01ad0bfe5dd7e864d",
slotId: "Back_plate",
upd: { Repairable: { Durability: 32.4, MaxDurability: 45 } },
}, {
_id: "43e7482d78b276a5db4f4fef",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "b82495b01ad0bfe5dd7e864d",
slotId: "1",
location: { x: 0, y: 1, r: "Horizontal", isSearched: true },
}, {
_id: "7372a194a2de632f5941b701",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "b82495b01ad0bfe5dd7e864d",
slotId: "2",
location: { x: 0, y: 1, r: "Horizontal", isSearched: true },
}, {
_id: "624555830937dfa5190a11bf",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "b82495b01ad0bfe5dd7e864d",
slotId: "3",
location: { x: 0, y: 1, r: "Horizontal", isSearched: true },
}, {
_id: "05aedd00b683ce0d00ac5c74",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "b82495b01ad0bfe5dd7e864d",
slotId: "4",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "8c3eccca9f25989d68d90e59",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "b82495b01ad0bfe5dd7e864d",
slotId: "5",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "b8b2e1651d7dbb891053b514",
_tpl: "5aa7e454e5b5b0214e506fa2",
parentId: "b82495b01ad0bfe5dd7e864d",
slotId: "8",
upd: { StackObjectsCount: 1 },
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "f9b891b8a5cf948b858a7b1b",
_tpl: "657f925dada5fadd1f07a57a",
parentId: "b8b2e1651d7dbb891053b514",
slotId: "Helmet_top",
upd: { Repairable: { Durability: 21, MaxDurability: 21 } },
}, {
_id: "8b66bc46287219eb0e7c190d",
_tpl: "657f92acada5fadd1f07a57e",
parentId: "b8b2e1651d7dbb891053b514",
slotId: "Helmet_back",
upd: { Repairable: { Durability: 21, MaxDurability: 21 } },
}, {
_id: "24cd31ecc0fdac526c8bd21d",
_tpl: "657f92e7f4c82973640b2354",
parentId: "b8b2e1651d7dbb891053b514",
slotId: "Helmet_ears",
upd: { Repairable: { Durability: 21, MaxDurability: 21 } },
}, {
_id: "c6125558f051cd10cfaf77e0",
_tpl: "5d6d3716a4b9361bc8618872",
parentId: "b82495b01ad0bfe5dd7e864d",
slotId: "9",
upd: { StackObjectsCount: 1 },
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "ee92a87a762b35b45f9d7fa7",
_tpl: "657fa009d4caf976440afe3a",
parentId: "c6125558f051cd10cfaf77e0",
slotId: "Helmet_top",
upd: { Repairable: { Durability: 33, MaxDurability: 33 } },
}, {
_id: "be9a15c5ab850ae0437315bb",
_tpl: "657fa04ac6679fefb3051e24",
parentId: "c6125558f051cd10cfaf77e0",
slotId: "Helmet_back",
upd: { Repairable: { Durability: 33, MaxDurability: 33 } },
}, {
_id: "65c9ceccbebe5f2813a8c70e",
_tpl: "657fa07387e11c61f70bface",
parentId: "c6125558f051cd10cfaf77e0",
slotId: "Helmet_ears",
upd: { Repairable: { Durability: 33, MaxDurability: 33 } },
}, {
_id: "769eab07773ecfaa6e12e4c4",
_tpl: "5aa7e3abe5b5b000171d064d",
upd: { StackObjectsCount: 1, Repairable: { Durability: 50, MaxDurability: 50 }, Togglable: { On: true } },
parentId: "b8b2e1651d7dbb891053b514",
slotId: "mod_equipment",
}, {
_id: "82aa530739c59be8dd5a0911",
_tpl: "5d6d3829a4b9361bc8618943",
upd: { StackObjectsCount: 1, Repairable: { Durability: 50, MaxDurability: 50 }, Togglable: { On: true } },
parentId: "c6125558f051cd10cfaf77e0",
slotId: "mod_equipment_000",
}, {
_id: "996a6f0f8e90cb31f758c801",
_tpl: "5d6d3be5a4b9361bc73bc763",
upd: { StackObjectsCount: 1, Repairable: { Durability: 10, MaxDurability: 24 } },
parentId: "c6125558f051cd10cfaf77e0",
slotId: "mod_equipment_001",
}, {
_id: "8c43cca672e16a931590945f",
_tpl: "5d6d3943a4b9360dbc46d0cc",
upd: { StackObjectsCount: 1, Repairable: { Durability: 1, MaxDurability: 1 } },
parentId: "c6125558f051cd10cfaf77e0",
slotId: "mod_equipment_002",
}, {
_id: "5858b72da0ca732b2fb5ed95",
_tpl: "544a5caa4bdc2d1a388b4568",
parentId: "f9cc99048aa37c5a4a837ef9",
slotId: "main",
upd: { StackObjectsCount: 1 },
location: { x: 0, y: 0, r: "Vertical", isSearched: true },
}, {
_id: "43ef5a5e1c93ba3ab032811f",
_tpl: "6570e83223c1f638ef0b0ede",
parentId: "5858b72da0ca732b2fb5ed95",
slotId: "Soft_armor_front",
upd: { Repairable: { Durability: 42, MaxDurability: 42 } },
}, {
_id: "e1f16c6d9e853f2735948665",
_tpl: "6570e87c23c1f638ef0b0ee2",
parentId: "5858b72da0ca732b2fb5ed95",
slotId: "Soft_armor_back",
upd: { Repairable: { Durability: 42, MaxDurability: 42 } },
}, {
_id: "d9bdf684a8264ac1fb3208bf",
_tpl: "6570e90b3a5689d85f08db97",
parentId: "5858b72da0ca732b2fb5ed95",
slotId: "Groin",
upd: { Repairable: { Durability: 28, MaxDurability: 28 } },
}, {
_id: "e4b484ba7209d770482732c8",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "f9cc99048aa37c5a4a837ef9",
slotId: "main",
location: { x: 0, y: 3, r: "Horizontal", isSearched: true },
}, {
_id: "ddc3945694d52dcdae9cba4d",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "f9cc99048aa37c5a4a837ef9",
slotId: "main",
location: { x: 1, y: 3, r: "Horizontal", isSearched: true },
}, {
_id: "59923cfba8be35031e5d95e6",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "f9cc99048aa37c5a4a837ef9",
slotId: "main",
location: { x: 2, y: 3, r: "Horizontal", isSearched: true },
}, {
_id: "95cf47373df5c4f07f458a93",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "f9cc99048aa37c5a4a837ef9",
slotId: "main",
location: { x: 3, y: 3, r: "Horizontal", isSearched: true },
}, {
_id: "98661f27ea826095d0cdd609",
_tpl: "656f9fa0498d1b7e3e071d98",
parentId: "5858b72da0ca732b2fb5ed95",
slotId: "Front_plate",
upd: { Repairable: { Durability: 50, MaxDurability: 50 } },
}, {
_id: "525179b06d14baaddb2b04fb",
_tpl: "656f9fa0498d1b7e3e071d98",
parentId: "5858b72da0ca732b2fb5ed95",
slotId: "Back_plate",
upd: { Repairable: { Durability: 50, MaxDurability: 50 } },
}, {
_id: "353638d16450339e40f5b5eb",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5858b72da0ca732b2fb5ed95",
slotId: "1",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "fa4a5c3e4e3c2f017e35eb1a",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5858b72da0ca732b2fb5ed95",
slotId: "2",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "7554a8ddb30e0306de7b7d80",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5858b72da0ca732b2fb5ed95",
slotId: "3",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "4c73b93f4d9f17a05a5782fa",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5858b72da0ca732b2fb5ed95",
slotId: "4",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "8ce190fcd2ffe5f1f9ad345a",
_tpl: "5aa7e4a4e5b5b000137b76f2",
parentId: "5858b72da0ca732b2fb5ed95",
slotId: "5",
upd: { StackObjectsCount: 1 },
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "6607b1b9d73f83e559304208",
_tpl: "657f925dada5fadd1f07a57a",
parentId: "8ce190fcd2ffe5f1f9ad345a",
slotId: "Helmet_top",
upd: { Repairable: { Durability: 21, MaxDurability: 21 } },
}, {
_id: "41912d06dac91585499c05a2",
_tpl: "657f92acada5fadd1f07a57e",
parentId: "8ce190fcd2ffe5f1f9ad345a",
slotId: "Helmet_back",
upd: { Repairable: { Durability: 21, MaxDurability: 21 } },
}, {
_id: "db24f69de28bddd7f09b9c3e",
_tpl: "657f92e7f4c82973640b2354",
parentId: "8ce190fcd2ffe5f1f9ad345a",
slotId: "Helmet_ears",
upd: { Repairable: { Durability: 21, MaxDurability: 21 } },
}, {
_id: "6c578731bc0c3f91c8089116",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5858b72da0ca732b2fb5ed95",
slotId: "6",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "aa7b1ce4897aa7e64309ce86",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5858b72da0ca732b2fb5ed95",
slotId: "6",
location: { x: 1, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "7e5e5dbe18de6aea779d904d",
_tpl: "5addccf45acfc400185c2989",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
parentId: "5858b72da0ca732b2fb5ed95",
slotId: "11",
location: { x: 0, y: 0, r: "Horizontal", isSearched: true },
}, {
_id: "b50b5052892729841e26934f",
_tpl: "5aa7e3abe5b5b000171d064d",
upd: { StackObjectsCount: 1, Repairable: { Durability: 47, MaxDurability: 47 }, Togglable: { On: true } },
parentId: "8ce190fcd2ffe5f1f9ad345a",
slotId: "mod_equipment",
}],
}, {
scheduledTime: 1712960777.6,
traderId: "54cb50c76803fa8b248b4571",
maxStorageTime: 345600,
systemData: { date: "11.04.2024", time: "19:30", location: "factory4_day" },
messageType: 8,
messageTemplateId: "58fe0e4586f774728248ca13 4",
items: [{
_id: "b29c463afe52421ba72b1816",
_tpl: "5aa7e3abe5b5b000171d064d",
upd: { StackObjectsCount: 1, Repairable: { Durability: 50, MaxDurability: 50 } },
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
}, {
_id: "1813e676a1bceefd5424b4bb",
_tpl: "5ac7655e5acfc40016339a19",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: {},
}, {
_id: "a58746a7e98ac16ba9105fc9",
_tpl: "5cf50850d7f00c056e24104c",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: {},
}, {
_id: "70acf65d9944b19d17d92b19",
_tpl: "55d480c04bdc2d1d4e8b456a",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: {},
}, {
_id: "e3a54ae66a2970372eee1888",
_tpl: "602e63fb6335467b0c5ac94d",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: {},
}, {
_id: "a4b1cb12dc5ece274d348e2b",
_tpl: "6033749e88382f4fab3fd2c5",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: {},
}, {
_id: "e3c2162f4001a0d6ed2a199d",
_tpl: "602f85fd9b513876d4338d9c",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: {},
}, {
_id: "548a111c4e48fb058e7a5c09",
_tpl: "603372b4da11d6478d5a07ff",
parentId: "e3a54ae66a2970372eee1888",
slotId: "mod_barrel",
upd: {},
}, {
_id: "d58db125fdd3f3b15a9798ca",
_tpl: "602e620f9b513876d4338d9a",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: {},
}, {
_id: "f85ba56791757174e3447c55",
_tpl: "630764fea987397c0816d219",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: {},
}, {
_id: "e3c409642a067f980ca168b7",
_tpl: "63075cc5962d0247b029dc2a",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: {},
}, {
_id: "5ff60d8b2b61c9f20ee8e91b",
_tpl: "63076701a987397c0816d21b",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: {},
}, {
_id: "b212acf3c8f09f48b4beaa0d",
_tpl: "5648a69d4bdc2ded0b8b457b",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: { StackObjectsCount: 1 },
}],
}, {
scheduledTime: 1712920104.8,
traderId: "54cb57776803fa99248b456e",
maxStorageTime: 518400,
systemData: { date: "11.04.2024", time: "19:30", location: "factory4_day" },
messageType: 8,
messageTemplateId: "58fe0e3486f77471f772c3f2 0",
items: [{
_id: "203161dde59c5a2fdd362da9",
_tpl: "5aa7e4a4e5b5b000137b76f2",
upd: { StackObjectsCount: 1, sptPresetId: "657fa87fc6679fefb3051e32" },
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
}, {
_id: "37c9968cfc4372c968f57c42",
_tpl: "657f925dada5fadd1f07a57a",
parentId: "203161dde59c5a2fdd362da9",
slotId: "Helmet_top",
upd: { Repairable: { Durability: 21, MaxDurability: 21 } },
}, {
_id: "5517446f3cd41cf75d3a9cd2",
_tpl: "657f92acada5fadd1f07a57e",
parentId: "203161dde59c5a2fdd362da9",
slotId: "Helmet_back",
upd: { Repairable: { Durability: 21, MaxDurability: 21 } },
}, {
_id: "0fdc8f25f7dfc61678cdef01",
_tpl: "657f92e7f4c82973640b2354",
parentId: "203161dde59c5a2fdd362da9",
slotId: "Helmet_ears",
upd: { Repairable: { Durability: 21, MaxDurability: 21 } },
}, {
_id: "e2cba41b10edfc595e2bb574",
_tpl: "628b916469015a4e1711ed8d",
parentId: "6c5ef8a5fb3b88641420e9a0",
slotId: "mod_handguard",
upd: {},
}, {
_id: "dd3aa187f853187198860933",
_tpl: "628b9be6cff66b70c002b14c",
parentId: "e2cba41b10edfc595e2bb574",
slotId: "mod_reciever",
upd: {},
}, {
_id: "13d29ea647b01f0ecb774a54",
_tpl: "628b9471078f94059a4b9bfb",
parentId: "dd3aa187f853187198860933",
slotId: "mod_sight_rear",
upd: {},
}, {
_id: "6c5ef8a5fb3b88641420e9a0",
_tpl: "628b8d83717774443b15e248",
parentId: "afcef56bf4fa36d0ec1f4166",
slotId: "mod_gas_block",
upd: {},
}, {
_id: "7980e04a92db0858cb7f4bfa",
_tpl: "55d4ae6c4bdc2d8b2f8b456e",
parentId: "929c1577ba7390558c59d8a5",
slotId: "mod_stock",
upd: {},
}, {
_id: "929c1577ba7390558c59d8a5",
_tpl: "628b9a40717774443b15e9f2",
parentId: "afcef56bf4fa36d0ec1f4166",
slotId: "mod_stock_000",
upd: {},
}, {
_id: "afcef56bf4fa36d0ec1f4166",
_tpl: "628b5638ad252a16da6dd245",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: { upd: {
FireMode: { FireMode: "single" },
StackObjectsCount: 1, StackObjectsCount: 1,
sptPresetId: "5acf7dfc86f774401e19c390", Repairable: { Durability: 100, MaxDurability: 100 },
Repairable: { Durability: 32, MaxDurability: 59 },
}, },
}, { }, {
_id: "724388f8110434efccd79b3a", _id: "a94905f708670fca5de11e7e",
_tpl: "544a3a774bdc2d3a388b4567", _tpl: "60339954d62c9b14ed777c06",
parentId: "b481bc57436ed9a0c3abe7f3", parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "mod_scope", slotId: "hideout",
upd: {
FireMode: { FireMode: "single" },
StackObjectsCount: 1,
Repairable: { Durability: 100, MaxDurability: 100 },
},
}, {
_id: "8f5ce6d3c7730240c3ae78ad",
_tpl: "602e71bd53a60014f9705bfa",
parentId: "a94905f708670fca5de11e7e",
slotId: "mod_pistol_grip",
upd: {},
}, {
_id: "c320122de049da2880d0a235",
_tpl: "5a7ad2e851dfba0016153692",
parentId: "a94905f708670fca5de11e7e",
slotId: "mod_magazine",
upd: {},
}, {
_id: "d2923c8984f26f68f01d20d7",
_tpl: "602e3f1254072b51b239f713",
parentId: "a94905f708670fca5de11e7e",
slotId: "mod_stock_001",
upd: {},
}, {
_id: "ecd363cddbb5361670d531b0",
_tpl: "60337f5dce399e10262255d1",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: {},
}, {
_id: "704c139d092f1bd3e3d18df2",
_tpl: "6034e3cb0ddce744014cb870",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: {},
}, {
_id: "72bd97cb21996a2282ff7bcd",
_tpl: "630765cb962d0247b029dc45",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: {},
}, {
_id: "44e3df51e01d5b23445fb95f",
_tpl: "630765777d50ff5e8a1ea718",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: {},
}, {
_id: "1cb0900a4bd068b04ca05db1",
_tpl: "63088377b5cd696784087147",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } }, upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } },
}, { }, {
_id: "8581038b0f795618a3d26c94", _id: "379ca7cd56ebb1434bd89d62",
_tpl: "58d268fc86f774111273f8c2", _tpl: "656f9d5900d62bcd2e02407c",
parentId: "724388f8110434efccd79b3a", parentId: "eb3bbc6cb084a53ff48b672d",
slotId: "mod_scope", slotId: "Front_plate",
upd: { StackObjectsCount: 1, Repairable: { Durability: 100, MaxDurability: 100 } }, upd: { Repairable: { Durability: 32.4841042, MaxDurability: 45 } },
}, {
_id: "d700aaa1d7a15644f6b91dc7",
_tpl: "656f9d5900d62bcd2e02407c",
parentId: "eb3bbc6cb084a53ff48b672d",
slotId: "Back_plate",
upd: { Repairable: { Durability: 45, MaxDurability: 45 } },
}, {
_id: "eb3bbc6cb084a53ff48b672d",
_tpl: "5b44d22286f774172b0c9de8",
parentId: "d2b3b859f667d4fd8b35bc96",
slotId: "hideout",
upd: { StackObjectsCount: 1 },
}, {
_id: "6a3d83c5230d1779060e17a9",
_tpl: "65704de13e7bba58ea0285c8",
parentId: "eb3bbc6cb084a53ff48b672d",
slotId: "Soft_armor_front",
upd: { Repairable: { Durability: 37.4214172, MaxDurability: 56 } },
}, {
_id: "9fba54de64b37902dd14b6e5",
_tpl: "65705c3c14f2ed6d7d0b7738",
parentId: "eb3bbc6cb084a53ff48b672d",
slotId: "Soft_armor_back",
upd: { Repairable: { Durability: 49.93156, MaxDurability: 56 } },
}, {
_id: "2b534b3af5f240c625a77424",
_tpl: "65705c777260e1139e091408",
parentId: "eb3bbc6cb084a53ff48b672d",
slotId: "Soft_armor_left",
upd: { Repairable: { Durability: 12, MaxDurability: 12 } },
}, {
_id: "e046688c9167ceaced5af3a7",
_tpl: "65705cb314f2ed6d7d0b773c",
parentId: "eb3bbc6cb084a53ff48b672d",
slotId: "soft_armor_right",
upd: { Repairable: { Durability: 12, MaxDurability: 12 } },
}, {
_id: "6c612d370959e61b6c10b7bf",
_tpl: "65705cea4916448ae1050897",
parentId: "eb3bbc6cb084a53ff48b672d",
slotId: "Collar",
upd: { Repairable: { Durability: 14, MaxDurability: 14 } },
}], }],
}]; }];

View File

@ -122,9 +122,7 @@ describe("InsuranceController", () =>
// Verify that the correct methods were called. // Verify that the correct methods were called.
expect(mockGetProfile).toBeCalledTimes(1); expect(mockGetProfile).toBeCalledTimes(1);
expect(mockLoggerDebug).toBeCalledWith( expect(mockLoggerDebug).toBeCalledTimes(1);
`Found ${insuranceFixture.length} insurance packages in profile ${sessionID}`,
);
expect(insuredFiltered.length).toBe(insuranceFixture.length); expect(insuredFiltered.length).toBe(insuranceFixture.length);
}); });
@ -147,9 +145,7 @@ describe("InsuranceController", () =>
// Verify that the correct methods were called. // Verify that the correct methods were called.
expect(mockGetProfile).toBeCalledTimes(1); expect(mockGetProfile).toBeCalledTimes(1);
expect(mockLoggerDebug).toBeCalledWith( expect(mockLoggerDebug).toBeCalledTimes(1);
`Found ${insuranceFixture.length} insurance packages in profile ${sessionID}`,
);
expect(insuredFiltered.length).toBe(insuranceFixture.length - 1); // Should be 1 less than the original fixture. expect(insuredFiltered.length).toBe(insuranceFixture.length - 1); // Should be 1 less than the original fixture.
}); });
@ -173,9 +169,7 @@ describe("InsuranceController", () =>
// Verify that the correct methods were called. // Verify that the correct methods were called.
expect(mockGetProfile).toBeCalledTimes(1); expect(mockGetProfile).toBeCalledTimes(1);
expect(mockLoggerDebug).toBeCalledWith( expect(mockLoggerDebug).toBeCalledTimes(1);
`Found ${insuranceFixture.length} insurance packages in profile ${sessionID}`,
);
// Verify that the returned array is empty. // Verify that the returned array is empty.
expect(insuredFiltered.length).toBe(0); expect(insuredFiltered.length).toBe(0);
@ -187,12 +181,14 @@ describe("InsuranceController", () =>
it("should log information about the insurance package", () => it("should log information about the insurance package", () =>
{ {
const sessionId = "session-id"; const sessionId = "session-id";
const numberOfItems = 666;
// Spy on the logger.debug method. // Spy on the logger.debug method.
const mockLoggerDebug = vi.spyOn(insuranceController.logger, "debug"); const mockLoggerDebug = vi.spyOn(insuranceController.logger, "debug");
vi.spyOn(insuranceController, "countAllInsuranceItems").mockReturnValue(numberOfItems);
vi.spyOn(insuranceController, "findItemsToDelete").mockImplementation(vi.fn()); vi.spyOn(insuranceController, "findItemsToDelete").mockImplementation(vi.fn());
vi.spyOn(insuranceController, "removeItemsFromInsurance").mockImplementation(vi.fn()); vi.spyOn(insuranceController, "removeItemsFromInsurance").mockImplementation(vi.fn());
vi.spyOn(insuranceController, "adoptOrphanedItems").mockImplementation(vi.fn()); vi.spyOn(insuranceController.itemHelper, "adoptOrphanedItems").mockImplementation(vi.fn());
vi.spyOn(insuranceController, "sendMail").mockImplementation(vi.fn()); vi.spyOn(insuranceController, "sendMail").mockImplementation(vi.fn());
vi.spyOn(insuranceController, "removeInsurancePackageFromProfile").mockImplementation(vi.fn()); vi.spyOn(insuranceController, "removeInsurancePackageFromProfile").mockImplementation(vi.fn());
@ -201,9 +197,7 @@ describe("InsuranceController", () =>
// Verify that the log was written. // Verify that the log was written.
expect(mockLoggerDebug).toBeCalledWith( expect(mockLoggerDebug).toBeCalledWith(
`Processing ${insuranceFixture.length} insurance packages, which includes a total of ${ `Processing ${insuranceFixture.length} insurance packages, which includes a total of ${numberOfItems} items, in profile ${sessionId}`,
insuranceController.countAllInsuranceItems(insuranceFixture)
} items, in profile ${sessionId}`,
); );
}); });
@ -218,9 +212,8 @@ describe("InsuranceController", () =>
); );
const mockRemoveItemsFromInsurance = vi.spyOn(insuranceController, "removeItemsFromInsurance") const mockRemoveItemsFromInsurance = vi.spyOn(insuranceController, "removeItemsFromInsurance")
.mockImplementation(vi.fn()); .mockImplementation(vi.fn());
const mockAdoptOrphanedItems = vi.spyOn(insuranceController, "adoptOrphanedItems").mockImplementation( const mockAdoptOrphanedItems = vi.spyOn(insuranceController.itemHelper, "adoptOrphanedItems")
vi.fn(), .mockImplementation(vi.fn());
);
const mockSendMail = vi.spyOn(insuranceController, "sendMail").mockImplementation(vi.fn()); const mockSendMail = vi.spyOn(insuranceController, "sendMail").mockImplementation(vi.fn());
const mockRemoveInsurancePackageFromProfile = vi.spyOn( const mockRemoveInsurancePackageFromProfile = vi.spyOn(
insuranceController, insuranceController,
@ -295,12 +288,17 @@ describe("InsuranceController", () =>
it("should remove the specified insurance package from the profile", () => it("should remove the specified insurance package from the profile", () =>
{ {
const sessionID = "session-id"; const sessionID = "session-id";
const packageToRemove = { date: "01.11.2023", time: "10:51", location: "factory4_day" }; const packageToRemove = {
traderId: "54cb50c76803fa8b248b4571",
systemData: { date: "01.11.2023", time: "11:18", location: "factory4_day" },
};
const profile = { const profile = {
insurance: [{ insurance: [{
messageContent: { systemData: { date: "01.11.2023", time: "11:18", location: "factory4_day" } }, traderId: "54cb50c76803fa8b248b4571",
}, { // This one should be removed systemData: { date: "01.11.2023", time: "11:18", location: "factory4_day" },
messageContent: { systemData: { date: "01.11.2023", time: "10:51", location: "factory4_day" } }, }, {
traderId: "54cb57776803fa99248b456e",
systemData: { date: "01.11.2023", time: "10:51", location: "factory4_day" },
}], }],
}; };
@ -312,20 +310,23 @@ describe("InsuranceController", () =>
// Verify that the specified insurance package was removed. // Verify that the specified insurance package was removed.
expect(profile.insurance.length).toBe(1); expect(profile.insurance.length).toBe(1);
expect(profile.insurance[0].messageContent.systemData).toStrictEqual({ expect(profile.insurance).toStrictEqual([{
date: "01.11.2023", traderId: "54cb57776803fa99248b456e",
time: "11:18", systemData: { date: "01.11.2023", time: "10:51", location: "factory4_day" },
location: "factory4_day", }]);
});
}); });
it("should log a message indicating that the package was removed", () => it("should log a message indicating that the package was removed", () =>
{ {
const sessionID = "session-id"; const sessionID = "session-id";
const packageToRemove = { date: "01.11.2023", time: "10:51", location: "factory4_day" }; const packageToRemove = {
traderId: "54cb50c76803fa8b248b4571",
systemData: { date: "01.11.2023", time: "11:18", location: "factory4_day" },
};
const profile = { const profile = {
insurance: [{ insurance: [{
messageContent: { systemData: { date: "01.11.2023", time: "10:51", location: "factory4_day" } }, traderId: "54cb50c76803fa8b248b4571",
systemData: { date: "01.11.2023", time: "11:18", location: "factory4_day" },
}], }],
}; };
@ -340,17 +341,24 @@ describe("InsuranceController", () =>
// Verify that the log was written. // Verify that the log was written.
expect(mockLoggerDebug).toBeCalledWith( expect(mockLoggerDebug).toBeCalledWith(
`Removed insurance package with date: ${packageToRemove.date}, time: ${packageToRemove.time}, and location: ${packageToRemove.location} from profile ${sessionID}. Remaining packages: ${profile.insurance.length}`, `Removed processed insurance package. Remaining packages: ${profile.insurance.length}`,
); );
}); });
it("should not remove any packages if the specified package is not found", () => it("should not remove any packages if the specified package is not found", () =>
{ {
const sessionID = "session-id"; const sessionID = "session-id";
const packageToRemove = { date: "01.11.2023", time: "10:51", location: "factory4_day" }; const packageToRemove = {
traderId: "54cb50c76803fa8b248b4571",
systemData: { date: "01.11.2023", time: "11:25", location: "factory4_day" },
};
const profile = { const profile = {
insurance: [{ insurance: [{
messageContent: { systemData: { date: "02.11.2023", time: "10:50", location: "factory4_night" } }, traderId: "54cb50c76803fa8b248b4571",
systemData: { date: "01.11.2023", time: "11:18", location: "factory4_day" },
}, {
traderId: "54cb57776803fa99248b456e",
systemData: { date: "01.11.2023", time: "10:51", location: "factory4_day" },
}], }],
}; };
@ -360,8 +368,8 @@ describe("InsuranceController", () =>
// Execute the method. // Execute the method.
insuranceController.removeInsurancePackageFromProfile(sessionID, packageToRemove); insuranceController.removeInsurancePackageFromProfile(sessionID, packageToRemove);
// Verify that no packages were removed. // Verify that the specified insurance package was removed.
expect(profile.insurance.length).toBe(1); expect(profile.insurance.length).toBe(2);
}); });
}); });
@ -373,7 +381,10 @@ describe("InsuranceController", () =>
insurancePackage.items = []; insurancePackage.items = [];
// Execute the method. // Execute the method.
const result = insuranceController.findItemsToDelete(insurancePackage); const result = insuranceController.findItemsToDelete(
insuranceController.hashUtil.generate(),
insurancePackage,
);
// Verify that the result is correct. // Verify that the result is correct.
expect(result.size).toBe(0); expect(result.size).toBe(0);
@ -387,7 +398,6 @@ describe("InsuranceController", () =>
const numberOfItems = insured.items.length; const numberOfItems = insured.items.length;
// Mock helper methods. // Mock helper methods.
const mockPopulateItemsMap = vi.spyOn(insuranceController, "populateItemsMap");
const mockPopulateParentAttachmentsMap = vi.spyOn(insuranceController, "populateParentAttachmentsMap"); const mockPopulateParentAttachmentsMap = vi.spyOn(insuranceController, "populateParentAttachmentsMap");
const mockIsAttachmentAttached = vi.spyOn(insuranceController.itemHelper, "isAttachmentAttached"); const mockIsAttachmentAttached = vi.spyOn(insuranceController.itemHelper, "isAttachmentAttached");
const mockProcessAttachments = vi.spyOn(insuranceController, "processAttachments").mockImplementation( const mockProcessAttachments = vi.spyOn(insuranceController, "processAttachments").mockImplementation(
@ -405,12 +415,11 @@ describe("InsuranceController", () =>
vi.spyOn(insuranceController, "processRegularItems").mockImplementation(mockProcessRegularItems); vi.spyOn(insuranceController, "processRegularItems").mockImplementation(mockProcessRegularItems);
// Execute the method. // Execute the method.
const result = insuranceController.findItemsToDelete(insured); const result = insuranceController.findItemsToDelete(insuranceController.hashUtil.generate(), insured);
// Verify that the correct methods were called. // Verify that the correct methods were called.
expect(mockPopulateItemsMap).toHaveBeenCalledTimes(1);
expect(mockPopulateParentAttachmentsMap).toHaveBeenCalledTimes(1); expect(mockPopulateParentAttachmentsMap).toHaveBeenCalledTimes(1);
expect(mockIsAttachmentAttached).toHaveBeenCalledTimes(numberOfItems + 1); // Once for each item, plus once more expect(mockIsAttachmentAttached).toHaveBeenCalled();
expect(mockProcessRegularItems).toHaveBeenCalledTimes(1); expect(mockProcessRegularItems).toHaveBeenCalledTimes(1);
expect(mockProcessAttachments).not.toHaveBeenCalled(); expect(mockProcessAttachments).not.toHaveBeenCalled();
@ -426,7 +435,6 @@ describe("InsuranceController", () =>
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
// Mock helper methods. // Mock helper methods.
const mockPopulateItemsMap = vi.spyOn(insuranceController, "populateItemsMap");
const mockProcessRegularItems = vi.spyOn(insuranceController, "processRegularItems"); const mockProcessRegularItems = vi.spyOn(insuranceController, "processRegularItems");
const mockProcessAttachments = vi.spyOn(insuranceController, "processAttachments"); const mockProcessAttachments = vi.spyOn(insuranceController, "processAttachments");
@ -440,10 +448,9 @@ describe("InsuranceController", () =>
); );
// Execute the method. // Execute the method.
const result = insuranceController.findItemsToDelete(insured); const result = insuranceController.findItemsToDelete(insuranceController.hashUtil.generate(), insured);
// Verify that the correct methods were called. // Verify that the correct methods were called.
expect(mockPopulateItemsMap).toHaveBeenCalled();
expect(mockPopulateParentAttachmentsMap).toHaveBeenCalled(); expect(mockPopulateParentAttachmentsMap).toHaveBeenCalled();
expect(mockProcessRegularItems).not.toHaveBeenCalled(); expect(mockProcessRegularItems).not.toHaveBeenCalled();
expect(mockProcessAttachments).not.toHaveBeenCalled(); expect(mockProcessAttachments).not.toHaveBeenCalled();
@ -459,7 +466,6 @@ describe("InsuranceController", () =>
const numberOfItems = insured.items.length; const numberOfItems = insured.items.length;
// Mock helper methods. // Mock helper methods.
const mockPopulateItemsMap = vi.spyOn(insuranceController, "populateItemsMap");
const mockPopulateParentAttachmentsMap = vi.spyOn(insuranceController, "populateParentAttachmentsMap"); const mockPopulateParentAttachmentsMap = vi.spyOn(insuranceController, "populateParentAttachmentsMap");
// Add all items to the toDelete set. Not realistic, but it's fine for this test. // Add all items to the toDelete set. Not realistic, but it's fine for this test.
@ -481,10 +487,9 @@ describe("InsuranceController", () =>
vi.spyOn(insuranceController, "processAttachments").mockImplementation(mockProcessAttachments); vi.spyOn(insuranceController, "processAttachments").mockImplementation(mockProcessAttachments);
// Execute the method. // Execute the method.
const result = insuranceController.findItemsToDelete(insured); const result = insuranceController.findItemsToDelete(insuranceController.hashUtil.generate(), insured);
// Verify that the correct methods were called. // Verify that the correct methods were called.
expect(mockPopulateItemsMap).toHaveBeenCalled();
expect(mockPopulateParentAttachmentsMap).toHaveBeenCalled(); expect(mockPopulateParentAttachmentsMap).toHaveBeenCalled();
expect(mockProcessRegularItems).toHaveBeenCalled(); expect(mockProcessRegularItems).toHaveBeenCalled();
expect(mockProcessAttachments).toHaveBeenCalled(); expect(mockProcessAttachments).toHaveBeenCalled();
@ -499,7 +504,6 @@ describe("InsuranceController", () =>
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
// Mock helper methods. // Mock helper methods.
const mockPopulateItemsMap = vi.spyOn(insuranceController, "populateItemsMap");
const mockPopulateParentAttachmentsMap = vi.spyOn(insuranceController, "populateParentAttachmentsMap"); const mockPopulateParentAttachmentsMap = vi.spyOn(insuranceController, "populateParentAttachmentsMap");
// Don't add any items to the toDelete set. // Don't add any items to the toDelete set.
@ -511,10 +515,9 @@ describe("InsuranceController", () =>
); );
// Execute the method. // Execute the method.
const result = insuranceController.findItemsToDelete(insured); const result = insuranceController.findItemsToDelete(insuranceController.hashUtil.generate(), insured);
// Verify that the correct methods were called. // Verify that the correct methods were called.
expect(mockPopulateItemsMap).toHaveBeenCalled();
expect(mockPopulateParentAttachmentsMap).toHaveBeenCalled(); expect(mockPopulateParentAttachmentsMap).toHaveBeenCalled();
expect(mockProcessRegularItems).toHaveBeenCalled(); expect(mockProcessRegularItems).toHaveBeenCalled();
expect(mockProcessAttachments).toHaveBeenCalled(); expect(mockProcessAttachments).toHaveBeenCalled();
@ -551,7 +554,7 @@ describe("InsuranceController", () =>
vi.spyOn(insuranceController, "processAttachments").mockImplementation(mockProcessAttachments); vi.spyOn(insuranceController, "processAttachments").mockImplementation(mockProcessAttachments);
// Execute the method. // Execute the method.
const result = insuranceController.findItemsToDelete(insured); const result = insuranceController.findItemsToDelete(insuranceController.hashUtil.generate(), insured);
// Verify that the result is the correct size, and the size is logged. // Verify that the result is the correct size, and the size is logged.
expect(result.size).toBe(numberOfItems); expect(result.size).toBe(numberOfItems);
@ -566,26 +569,54 @@ describe("InsuranceController", () =>
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
// Generate the items map. // Generate the items map.
const itemsMap = insuranceController.populateItemsMap(insured); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
// Execute the method. // Execute the method.
const result = insuranceController.populateParentAttachmentsMap(insured, itemsMap); const result = insuranceController.populateParentAttachmentsMap(
insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
// Verify that the map is populated correctly. // Verify that the map is populated correctly.
expect(result.size).toBe(6); // There are 6 base-level items in this insurance package. expect(result.size).toBe(9); // There are 9 base-level items in this insurance package.
const gun = result.get("911a0f04d5d9c7e239807ae0"); const gun = result.get("35111c9b72a87b6b7d95ad35");
expect(gun.length).toBe(7); // This AK has 7 attachments. expect(gun.length).toBe(31); // This gun has 31 attachments.
// The attachments should be mapped to the AK properly... // The attachments should be mapped to the gun properly...
const validAttachmentTemplates = [ const validAttachmentTemplates = [
"677c209ebb45445ebb42c405", "7c42d3dce0ddbc4806bce48b",
"4bd10f89836fd9f86aedcac1", "10b97872c5f4e0e1949a0369",
"8b1327270791b142ac341b03", "a6cd9986dde4cabddcd2dce2",
"da8cde1b3024c336f6e06152", "b65635b515712f990fdcc201",
"bc041c0011d76f714b898400", "0e11045873efe3625695c1ae",
"9f8d7880a6e0a47a211ec5d3", "94c4161abe8bf654fb986063",
"db2ef9442178910eba985b51", "9b284ccfd0d535acec1ff58b",
"d730caa83a11fd01250a7261",
"24291c7bcf91e362adb6d68b",
"0d98fd0769cce8e473bbe540",
"11b174510f039e8217fbd202",
"c435230e530574b1d7c32300",
"15666fe6fd2d95206612e418",
"a54de8b9014eee71fdf1d01d",
"c34555bc95a9a7a23150a36f",
"91cae4ae30d1366b87158238",
"48f23df4509164cf397b9ab5",
"a55f05f689978ac65c7da654",
"8ae4ea81a2d6074162d87a9c",
"312cc0f6687963305457235e",
"e1e5aaf474b7282a52ac9a14",
"bb9a34648e08f005db5d7484",
"dd9ac99d3ea4c9656221bcc9",
"b22748de8da5f3c1362dd8e0",
"e3cc1be8954c4889f94b435a",
"e73f05be5a306168e847da82",
"847cf35ec92d8af8e4814ea8",
"bb4b7a4475fea0f0135305f6",
"d0ac8e688a0bb17668589909",
"5dbcf8cbbb3f8ef669836320",
"f996645c809968f8033593a6",
]; ];
for (const value of validAttachmentTemplates) for (const value of validAttachmentTemplates)
{ {
@ -594,64 +625,37 @@ describe("InsuranceController", () =>
} }
}); });
it("should ignore gun accessories that cannot be modified in-raid", () =>
{
const insured = insuranceFixture[0];
// Generate the items map.
const itemsMap = insuranceController.populateItemsMap(insured);
// Execute the method.
const result = insuranceController.populateParentAttachmentsMap(insured, itemsMap);
// Verify that the map is populated correctly.
expect(result.size).toBe(6); // There are 6 base-level items in this insurance package.
const gun = result.get("911a0f04d5d9c7e239807ae0");
expect(gun.length).toBe(7); // This AK has 7 valid attachments.
// These are attachments for the AK, but they are not raid moddable, so they should not be mapped.
const invalidAttachmentTemplates = [
"1e0b177df108c0c117028812",
"c9278dd8251e99578bf7a274",
"402b4086535a50ef7d9cef88",
"566335b3df586f34b47f5e35",
];
for (const value of invalidAttachmentTemplates)
{
// Verify that each template is not present in the array of attachments.
expect(gun.every((item) => item._id !== value)).toBe(true);
}
});
it("should correctly map helmet to all of its attachments", () => it("should correctly map helmet to all of its attachments", () =>
{ {
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
// Generate the items map. // Generate the items map.
const itemsMap = insuranceController.populateItemsMap(insured); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
// Execute the method. // Execute the method.
const result = insuranceController.populateParentAttachmentsMap(insured, itemsMap); const result = insuranceController.populateParentAttachmentsMap(
insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
// Verify that the map is populated correctly. // Verify that the map is populated correctly.
expect(result.size).toBe(6); // There are 6 base-level items in this insurance package. expect(result.size).toBe(9); // There are 9 base-level items in this insurance package.
const gun = result.get("3679078e05f5b14466d6a730"); const helmet = result.get("b2405216e5730f3511884a10");
expect(gun.length).toBe(5); // This LShZ-2DTM has 5 valid attachments. expect(helmet.length).toBe(4); // This helmet has 2 valid attachments.
// The attachments should be mapped to the AK properly... // The attachments should be mapped to the helmet properly...
const validAttachmentTemplates = [ const validAttachmentTemplates = [
"a2b0c716162c5e31ec28c55a", "7a0675280dbbad69ce592d74",
"dc565f750342cb2d19eeda06", "c0c182942f54d3c183f0e179",
"e9ff62601669d9e2ea9c2fbb", "f7066fdfeefb29eca1d2dbeb",
"ac134d7cf6c9d8e25edd0015", "ee0ec86e9608abe773175e3a",
"22274b895ecc80d51c3cba1c",
]; ];
for (const value of validAttachmentTemplates) for (const value of validAttachmentTemplates)
{ {
// Verify that each template is present in the array of attachments. // Verify that each template is present in the array of attachments.
expect(gun.some((item) => item._id === value)).toBe(true); expect(helmet.some((item) => item._id === value)).toBe(true);
} }
}); });
@ -660,29 +664,41 @@ describe("InsuranceController", () =>
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
// Generate the items map. // Generate the items map.
const itemsMap = insuranceController.populateItemsMap(insured); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
// Execute the method. // Execute the method.
const result = insuranceController.populateParentAttachmentsMap(insured, itemsMap); const result = insuranceController.populateParentAttachmentsMap(
insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
// Verify that the map is populated correctly. // Verify that the map is populated correctly.
expect(result.size).toBe(6); // There are 6 base-level items in this insurance package. expect(result.size).toBe(9); // There are 9 base-level items in this insurance package.
const gun = result.get("351180f3248d45c71cb2ebdc"); const gun = result.get("26598f88d49198c4a0a9391c");
expect(insured.items.find((item) => item._id === "351180f3248d45c71cb2ebdc").slotId).toBe("main"); expect(insured.items.find((item) => item._id === "26598f88d49198c4a0a9391c").slotId).toBe("main");
expect(gun.length).toBe(14); // This AS VAL has 14 valid attachments. expect(gun.length).toBe(3);
}); });
it("should not map items that do not have a main-parent", () => it("should not map items that do not have a main-parent", () =>
{ {
// Remove regular items from the fixture.
insuranceFixture = new ProfileInsuranceFactory().adjustPackageDates().removeRegularItems().get(); insuranceFixture = new ProfileInsuranceFactory().adjustPackageDates().removeRegularItems().get();
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
// Generate the items map. // Generate the items map.
const itemsMap = insuranceController.populateItemsMap(insured); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
// Suppress warnings.
const mockLoggerWarning = vi.spyOn(insuranceController.logger, "warning").mockImplementation(vi.fn());
// Execute the method. // Execute the method.
const result = insuranceController.populateParentAttachmentsMap(insured, itemsMap); const result = insuranceController.populateParentAttachmentsMap(
insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
// Verify that the map is populated correctly. // Verify that the map is populated correctly.
expect(result.size).toBe(0); expect(result.size).toBe(0);
@ -690,31 +706,92 @@ describe("InsuranceController", () =>
it("should log a warning when an item does not have a main-parent", () => it("should log a warning when an item does not have a main-parent", () =>
{ {
// Remove regular items from the fixture.
insuranceFixture = new ProfileInsuranceFactory().adjustPackageDates().removeRegularItems().get(); insuranceFixture = new ProfileInsuranceFactory().adjustPackageDates().removeRegularItems().get();
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
// Generate the items map. // Generate the items map.
const itemsMap = insuranceController.populateItemsMap(insured); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
// Suppress warnings. // Suppress warnings.
const mockLoggerWarning = vi.spyOn(insuranceController.logger, "warning").mockImplementation(vi.fn()); const mockLoggerWarning = vi.spyOn(insuranceController.logger, "warning").mockImplementation(vi.fn());
// Execute the method. // Execute the method.
insuranceController.populateParentAttachmentsMap(insured, itemsMap); insuranceController.populateParentAttachmentsMap(
insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
// Verify that the warning was logged. // Verify that the warning was logged.
expect(mockLoggerWarning).toHaveBeenCalled(); expect(mockLoggerWarning).toHaveBeenCalled();
}); });
}); });
describe("removeNonModdableAttachments", () =>
{
it("should return a Map where each parent item ID is mapped to only moddable attachments", () =>
{
const insured = insuranceFixture[0];
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
// Execute the method.
const result = insuranceController.removeNonModdableAttachments(parentAttachmentsMap, itemsMap);
// Verify that the map is populated correctly.
for (const [parentId, attachments] of result)
{
for (const attachment of attachments)
{
// Verify that each attachment is moddable.
const attachmentParentItem = itemsMap.get(parentId);
expect(insuranceController.itemHelper.isRaidModdable(attachment, attachmentParentItem)).toBe(true);
}
}
});
it("should remove parents that do not have any moddable attachments", () =>
{
const insured = insuranceFixture[0];
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
// Mock isRaidModdable to return false for all attachments.
vi.spyOn(insuranceController.itemHelper, "isRaidModdable").mockReturnValue(false);
// Execute the method.
const result = insuranceController.removeNonModdableAttachments(parentAttachmentsMap, itemsMap);
// Verify that the map is now empty.
expect(result.size).toBe(0);
});
});
describe("processRegularItems", () => describe("processRegularItems", () =>
{ {
it("should process regular items and their non-attachment children", () => it("should process regular items and their non-attachment children", () =>
{ {
// Remove attachment items from the fixture.
insuranceFixture = new ProfileInsuranceFactory().adjustPackageDates().removeAttachmentItems().get(); insuranceFixture = new ProfileInsuranceFactory().adjustPackageDates().removeAttachmentItems().get();
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
const numberOfItems = insured.items.length; const numberOfItems = insured.items.length;
const toDelete = new Set<string>(); const toDelete = new Set<string>();
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
// Mock helper methods. // Mock helper methods.
const mockIsAttachmentAttached = vi.spyOn(insuranceController.itemHelper, "isAttachmentAttached"); const mockIsAttachmentAttached = vi.spyOn(insuranceController.itemHelper, "isAttachmentAttached");
@ -727,11 +804,11 @@ describe("InsuranceController", () =>
const mockRollForDelete = vi.spyOn(insuranceController, "rollForDelete").mockReturnValue(true); const mockRollForDelete = vi.spyOn(insuranceController, "rollForDelete").mockReturnValue(true);
// Execute the method. // Execute the method.
insuranceController.processRegularItems(insured, toDelete); insuranceController.processRegularItems(insured, toDelete, parentAttachmentsMap);
// Verify that the correct methods were called. // Verify that the correct methods were called.
expect(mockIsAttachmentAttached).toHaveBeenCalled(); expect(mockIsAttachmentAttached).toHaveBeenCalledTimes(numberOfItems);
expect(mockFindAndReturnChildrenAsItems).toHaveBeenCalled(); expect(mockFindAndReturnChildrenAsItems).not.toHaveBeenCalled();
expect(mockRollForDelete).toHaveBeenCalledTimes(numberOfItems); expect(mockRollForDelete).toHaveBeenCalledTimes(numberOfItems);
// Verify that all items were added to the toDelete set. // Verify that all items were added to the toDelete set.
@ -742,6 +819,12 @@ describe("InsuranceController", () =>
{ {
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
const toDelete = new Set<string>(); const toDelete = new Set<string>();
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
// Mock isAttachmentAttached to return true for all items. // Mock isAttachmentAttached to return true for all items.
vi.spyOn(insuranceController.itemHelper, "isAttachmentAttached").mockReturnValue(true); vi.spyOn(insuranceController.itemHelper, "isAttachmentAttached").mockReturnValue(true);
@ -750,7 +833,7 @@ describe("InsuranceController", () =>
const mockRollForDelete = vi.spyOn(insuranceController, "rollForDelete").mockReturnValue(true); const mockRollForDelete = vi.spyOn(insuranceController, "rollForDelete").mockReturnValue(true);
// Execute the method. // Execute the method.
insuranceController.processRegularItems(insured, toDelete); insuranceController.processRegularItems(insured, toDelete, parentAttachmentsMap);
// Verify that a roll was not made for any items. // Verify that a roll was not made for any items.
expect(mockRollForDelete).not.toHaveBeenCalled(); expect(mockRollForDelete).not.toHaveBeenCalled();
@ -761,20 +844,24 @@ describe("InsuranceController", () =>
it("should mark attachments for deletion when parent is marked for deletion", () => it("should mark attachments for deletion when parent is marked for deletion", () =>
{ {
const itemHelper = container.resolve<ItemHelper>("ItemHelper");
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
const toDelete = new Set<string>(); const toDelete = new Set<string>();
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
// Mock rollForDelete to return true for all base-parent items. // Mock rollForDelete to return true for all base-parent items.
const mockRollForDelete = vi.fn((traderId, insuredItem) => const mockRollForDelete = vi.fn((traderId, insuredItem) =>
{ {
return !itemHelper.isAttachmentAttached(insuredItem); return !insuranceController.itemHelper.isAttachmentAttached(insuredItem);
}); });
vi.spyOn(insuranceController, "rollForDelete").mockImplementation(mockRollForDelete); vi.spyOn(insuranceController, "rollForDelete").mockImplementation(mockRollForDelete);
// Execute the method. // Execute the method.
insuranceController.processRegularItems(insured, toDelete); insuranceController.processRegularItems(insured, toDelete, parentAttachmentsMap);
// Verify that all items were added to the toDelete set. // Verify that all items were added to the toDelete set.
expect(toDelete).toEqual(new Set(insured.items.map((item) => item._id))); expect(toDelete).toEqual(new Set(insured.items.map((item) => item._id)));
@ -786,44 +873,53 @@ describe("InsuranceController", () =>
it("should iterate over each parent item", () => it("should iterate over each parent item", () =>
{ {
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
const itemsMap = insuranceController.populateItemsMap(insured);
const parentToAttachmentMap = insuranceController.populateParentAttachmentsMap(insured, itemsMap);
const toDelete = new Set<string>(); const toDelete = new Set<string>();
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
// Mock helper methods. // Mock helper methods.
const mockProcessAttachmentByParent = vi.spyOn(insuranceController, "processAttachmentByParent"); const mockProcessAttachmentByParent = vi.spyOn(insuranceController, "processAttachmentByParent");
// Execute the method. // Execute the method.
insuranceController.processAttachments(parentToAttachmentMap, itemsMap, insured.traderId, toDelete); insuranceController.processAttachments(parentAttachmentsMap, itemsMap, insured.traderId, toDelete);
// Verify // Verify
expect(mockProcessAttachmentByParent).toHaveBeenCalledTimes(parentToAttachmentMap.size); expect(mockProcessAttachmentByParent).toHaveBeenCalledTimes(parentAttachmentsMap.size);
}); });
it("should log the name of each parent item", () => it("should log the name of each parent item", () =>
{ {
const itemHelper = container.resolve<ItemHelper>("ItemHelper");
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
const itemsMap = insuranceController.populateItemsMap(insured);
const parentToAttachmentMap = insuranceController.populateParentAttachmentsMap(insured, itemsMap);
const toDelete = new Set<string>(); const toDelete = new Set<string>();
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
// Mock helper methods. // Mock helper methods.
const mockLoggerDebug = vi.spyOn(insuranceController.logger, "debug"); const mockLoggerDebug = vi.spyOn(insuranceController.logger, "debug").mockImplementation(vi.fn());
// Mock processAttachmentByParent to prevent it from being called.
vi.spyOn(insuranceController, "processAttachmentByParent").mockImplementation(vi.fn());
// Execute the method. // Execute the method.
insuranceController.processAttachments(parentToAttachmentMap, itemsMap, insured.traderId, toDelete); insuranceController.processAttachments(parentAttachmentsMap, itemsMap, insured.traderId, toDelete);
// Verify that the name of each parent item is logged. // Verify that the name of each parent item is logged.
for (const [parentId] of parentToAttachmentMap) for (const [parentId] of parentAttachmentsMap)
{ {
const parentItem = itemsMap.get(parentId); const parentItem = itemsMap.get(parentId);
if (parentItem) if (parentItem)
{ {
const expectedMessage = `Processing attachments for parent item: ${ const expectedMessage = `Processing attachments of parent "${
itemHelper.getItemName(parentItem._tpl) insuranceController.itemHelper.getItemName(parentItem._tpl)
}`; }":`;
expect(mockLoggerDebug).toHaveBeenCalledWith(expectedMessage); expect(mockLoggerDebug).toHaveBeenCalledWith(expectedMessage);
} }
} }
@ -835,9 +931,13 @@ describe("InsuranceController", () =>
it("should handle sorting, rolling, and deleting attachments by calling helper methods", () => it("should handle sorting, rolling, and deleting attachments by calling helper methods", () =>
{ {
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
const itemsMap = insuranceController.populateItemsMap(insured); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
const parentToAttachmentMap = insuranceController.populateParentAttachmentsMap(insured, itemsMap); const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
const attachments = parentToAttachmentMap.entries().next().value; insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
const attachments = parentAttachmentsMap.entries().next().value;
const toDelete = new Set<string>(); const toDelete = new Set<string>();
// Mock helper methods. // Mock helper methods.
@ -854,12 +954,16 @@ describe("InsuranceController", () =>
expect(mockAttachmentDeletionByValue).toHaveBeenCalled(); expect(mockAttachmentDeletionByValue).toHaveBeenCalled();
}); });
it("should log attachment details and number of successful rolls", () => it("should log attachment details and number of attachments to be deleted", () =>
{ {
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
const itemsMap = insuranceController.populateItemsMap(insured); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
const parentToAttachmentMap = insuranceController.populateParentAttachmentsMap(insured, itemsMap); const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
const attachments = parentToAttachmentMap.values().next().value; insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
const attachments = parentAttachmentsMap.entries().next().value;
const toDelete = new Set<string>(); const toDelete = new Set<string>();
const successfulRolls = 4; const successfulRolls = 4;
@ -873,7 +977,7 @@ describe("InsuranceController", () =>
// Verify that the logs were called/written. // Verify that the logs were called/written.
expect(mockLogAttachmentsDetails).toBeCalled(); expect(mockLogAttachmentsDetails).toBeCalled();
expect(mockLoggerDebug).toHaveBeenCalledWith(`Number of successful rolls: ${successfulRolls}`); expect(mockLoggerDebug).toHaveBeenCalledWith(`Number of attachments to be deleted: ${successfulRolls}`);
}); });
}); });
@ -882,15 +986,20 @@ describe("InsuranceController", () =>
it("should sort the attachments array by maxPrice in descending order", () => it("should sort the attachments array by maxPrice in descending order", () =>
{ {
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
const itemsMap = insuranceController.populateItemsMap(insured); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
const parentToAttachmentMap = insuranceController.populateParentAttachmentsMap(insured, itemsMap); const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
const attachments = parentToAttachmentMap.values().next().value; insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
const attachments = parentAttachmentsMap.entries().next().value;
const attachmentCount = attachments.length;
// Execute the method. // Execute the method.
const sortedAttachments = insuranceController.sortAttachmentsByPrice(attachments); const sortedAttachments = insuranceController.sortAttachmentsByPrice(attachments);
// Verify the length of the sorted attachments array // Verify the length of the sorted attachments array is unchanged
expect(sortedAttachments.length).toBe(5); expect(sortedAttachments.length).toBe(attachmentCount);
// Verify that the attachments are sorted by maxPrice in descending order // Verify that the attachments are sorted by maxPrice in descending order
for (let i = 1; i < sortedAttachments.length; i++) for (let i = 1; i < sortedAttachments.length; i++)
@ -902,20 +1011,22 @@ describe("InsuranceController", () =>
it("should place attachments with null maxPrice at the bottom of the sorted list", () => it("should place attachments with null maxPrice at the bottom of the sorted list", () =>
{ {
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
const itemsMap = insuranceController.populateItemsMap(insured); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
const parentToAttachmentMap = insuranceController.populateParentAttachmentsMap(insured, itemsMap); const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
const attachments = parentToAttachmentMap.values().next().value; insuranceController.hashUtil.generate(),
insured,
// Set the maxPrice of the first two attachments to null. itemsMap,
vi.spyOn(insuranceController.itemHelper, "getItemMaxPrice").mockReturnValueOnce(null).mockReturnValueOnce(
null,
); );
const attachments = parentAttachmentsMap.entries().next().value;
// Set the maxPrice of the first attachment to null.
vi.spyOn(insuranceController.itemHelper, "getItemMaxPrice").mockReturnValue(666).mockReturnValueOnce(null);
// Execute the method. // Execute the method.
const sortedAttachments = insuranceController.sortAttachmentsByPrice(attachments); const sortedAttachments = insuranceController.sortAttachmentsByPrice(attachments);
// Verify that the attachments with null maxPrice are at the bottom of the list // Verify that the attachments with null maxPrice are at the bottom of the list
const nullPriceAttachments = sortedAttachments.slice(-2); const nullPriceAttachments = sortedAttachments.slice(-1);
for (const attachment of nullPriceAttachments) for (const attachment of nullPriceAttachments)
{ {
expect(attachment.maxPrice).toBeNull(); expect(attachment.maxPrice).toBeNull();
@ -947,8 +1058,8 @@ describe("InsuranceController", () =>
// Verify that logger.debug was called correctly. // Verify that logger.debug was called correctly.
expect(loggerDebugSpy).toHaveBeenCalledTimes(2); expect(loggerDebugSpy).toHaveBeenCalledTimes(2);
expect(loggerDebugSpy).toHaveBeenNthCalledWith(1, "Child Item - Name: Item 1, Max Price: 100"); expect(loggerDebugSpy).toHaveBeenNthCalledWith(1, "Attachment 1: \"Item 1\" - Price: 100");
expect(loggerDebugSpy).toHaveBeenNthCalledWith(2, "Child Item - Name: Item 2, Max Price: 200"); expect(loggerDebugSpy).toHaveBeenNthCalledWith(2, "Attachment 2: \"Item 2\" - Price: 200");
}); });
it("should not log anything when there are no attachments", () => it("should not log anything when there are no attachments", () =>
@ -968,12 +1079,16 @@ describe("InsuranceController", () =>
describe("countSuccessfulRolls", () => describe("countSuccessfulRolls", () =>
{ {
it("should count the number of successful rolls based on the rollForDelete method", () => it("should count the number of successful rolls made in the rollForDelete method", () =>
{ {
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
const itemsMap = insuranceController.populateItemsMap(insured); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
const parentToAttachmentMap = insuranceController.populateParentAttachmentsMap(insured, itemsMap); const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
const attachments = parentToAttachmentMap.values().next().value; insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
const attachments = parentAttachmentsMap.values().next().value;
// Mock rollForDelete to return true for the first two attachments. // Mock rollForDelete to return true for the first two attachments.
const mockRollForDelete = vi.spyOn(insuranceController, "rollForDelete").mockReturnValue(false) const mockRollForDelete = vi.spyOn(insuranceController, "rollForDelete").mockReturnValue(false)
@ -987,12 +1102,16 @@ describe("InsuranceController", () =>
expect(result).toBe(2); expect(result).toBe(2);
}); });
it("should count the number of successful rolls based on the rollForDelete method", () => it("should return zero if no successful rolls were made in the rollForDelete method", () =>
{ {
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
const itemsMap = insuranceController.populateItemsMap(insured); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
const parentToAttachmentMap = insuranceController.populateParentAttachmentsMap(insured, itemsMap); const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
const attachments = parentToAttachmentMap.values().next().value; insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
const attachments = parentAttachmentsMap.values().next().value;
// Mock rollForDelete to return false. // Mock rollForDelete to return false.
const mockRollForDelete = vi.spyOn(insuranceController, "rollForDelete").mockReturnValue(false); const mockRollForDelete = vi.spyOn(insuranceController, "rollForDelete").mockReturnValue(false);
@ -1027,9 +1146,13 @@ describe("InsuranceController", () =>
it("should add the correct number of attachments to the toDelete set", () => it("should add the correct number of attachments to the toDelete set", () =>
{ {
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
const itemsMap = insuranceController.populateItemsMap(insured); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
const parentToAttachmentMap = insuranceController.populateParentAttachmentsMap(insured, itemsMap); const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
const attachments = parentToAttachmentMap.values().next().value; insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
const attachments = parentAttachmentsMap.values().next().value;
const successfulRolls = 2; const successfulRolls = 2;
const toDelete = new Set<string>(); const toDelete = new Set<string>();
@ -1044,9 +1167,13 @@ describe("InsuranceController", () =>
it("should not add any attachments to toDelete if successfulRolls is zero", () => it("should not add any attachments to toDelete if successfulRolls is zero", () =>
{ {
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
const itemsMap = insuranceController.populateItemsMap(insured); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
const parentToAttachmentMap = insuranceController.populateParentAttachmentsMap(insured, itemsMap); const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
const attachments = parentToAttachmentMap.values().next().value; insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
const attachments = parentAttachmentsMap.values().next().value;
const successfulRolls = 0; const successfulRolls = 0;
const toDelete = new Set<string>(); const toDelete = new Set<string>();
@ -1061,9 +1188,13 @@ describe("InsuranceController", () =>
it("should add all attachments to toDelete if successfulRolls is greater than the number of attachments", () => it("should add all attachments to toDelete if successfulRolls is greater than the number of attachments", () =>
{ {
const insured = insuranceFixture[0]; const insured = insuranceFixture[0];
const itemsMap = insuranceController.populateItemsMap(insured); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
const parentToAttachmentMap = insuranceController.populateParentAttachmentsMap(insured, itemsMap); const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap(
const attachments = parentToAttachmentMap.values().next().value; insuranceController.hashUtil.generate(),
insured,
itemsMap,
);
const attachments = parentAttachmentsMap.values().next().value;
const successfulRolls = 999; const successfulRolls = 999;
const toDelete = new Set<string>(); const toDelete = new Set<string>();
@ -1134,125 +1265,10 @@ describe("InsuranceController", () =>
}); });
}); });
describe("adoptOrphanedItems", () =>
{
it("should adopt orphaned items by resetting them as base-level items", () =>
{
// Get all of the items, so that we can dynamically find the hideout item.
const insured = insuranceFixture[0];
const hideoutParentId = insuranceController.fetchHideoutItemParent(insured.items);
// Manually set one of the items to be orphaned.
insured.items[0].parentId = "9999"; // Should not exist in the items array.
insured.items[0].slotId = "main"; // Should not be "hideout".
// Iterate over the items and find an individual orphaned item.
const orphanedItem = insured.items.find((item) =>
!insured.items.some((parent) => parent._id === item.parentId)
);
// Setup tests to verify that the orphaned item we added is in fact orphaned.
expect(orphanedItem.parentId).toBe(insured.items[0].parentId);
expect(orphanedItem.slotId).toBe(insured.items[0].slotId);
// Execute the method.
insuranceController.adoptOrphanedItems(insured);
// Verify that the orphaned items have been adopted.
expect(orphanedItem.parentId).toBe(hideoutParentId);
expect(orphanedItem.slotId).toBe("hideout");
});
it("should not adopt items that are not orphaned", () =>
{
const unmodified = insuranceFixture[0];
// Create a deep copy of the insured items array.
const insured = JSON.parse(JSON.stringify(insuranceFixture[0]));
// Execute the method.
insuranceController.adoptOrphanedItems(insured);
// Verify that the orphaned items have been adopted.
expect(insured).toStrictEqual(unmodified);
});
it("should remove location data from adopted items", () =>
{
const insured = insuranceFixture[0];
// Manually set one of the items to be orphaned.
insured.items[0].parentId = "9999"; // Should not exist in the items array.
insured.items[0].slotId = "main"; // Should not be "hideout".
insured.items[0].location = { x: 1, y: 2, r: 3, isSearched: true }; // Should be removed.
// Iterate over the items and find an individual orphaned item.
const orphanedItem = insured.items.find((item) =>
!insured.items.some((parent) => parent._id === item.parentId)
);
// Setup tests to verify that the orphaned item we added is in fact orphaned.
expect(orphanedItem.parentId).toBe(insured.items[0].parentId);
expect(orphanedItem.slotId).toBe(insured.items[0].slotId);
// Execute the method.
insuranceController.adoptOrphanedItems(insured);
// Verify that the orphaned items have been adopted.
expect(orphanedItem).not.toHaveProperty("location");
});
});
describe("fetchHideoutItemParent", () =>
{
it("should return the parentId value of an item that has a slotId of 'hideout'", () =>
{
const insured = insuranceFixture[0];
const hideoutParentId = insuranceController.fetchHideoutItemParent(insured.items);
// Execute the method.
const result = insuranceController.fetchHideoutItemParent(insured.items);
// Verify that the hideout item parentId is returned.
expect(result).toBe(hideoutParentId);
});
it("should return an empty string if no item with a slotId of 'hideout' could be found", () =>
{
// Fetch a bunch of orphaned items that don't have a hideout parent.
const insuranceFixture = new ProfileInsuranceFactory().adjustPackageDates().removeRegularItems().get();
const insured = insuranceFixture[0];
// Execute the method.
const result = insuranceController.fetchHideoutItemParent(insured.items);
// Verify that the hideout item parentId is returned.
expect(result).toBe("");
});
it("should log a warning if the base-level item does not exist", () =>
{
// Fetch a bunch of orphaned items that don't have a hideout parent.
const insuranceFixture = new ProfileInsuranceFactory().adjustPackageDates().removeRegularItems().get();
const insured = insuranceFixture[0];
// Spy on the logger.
const loggerWarningSpy = vi.spyOn(insuranceController.logger, "warning");
// Execute the method.
insuranceController.fetchHideoutItemParent(insured.items);
// Verify that the hideout item parentId is returned.
expect(loggerWarningSpy).toHaveBeenCalled();
});
});
describe("sendMail", () => describe("sendMail", () =>
{ {
it("should send insurance failed message when no items are present", () => it("should send insurance failed message when no items are present", () =>
{ {
const traderHelper = container.resolve<TraderHelper>("TraderHelper");
const insurance = insuranceFixture[0]; const insurance = insuranceFixture[0];
insurance.items = []; // Empty the items array insurance.items = []; // Empty the items array
const sessionID = "session-id"; const sessionID = "session-id";
@ -1276,19 +1292,17 @@ describe("InsuranceController", () =>
// Verify that the insurance failed message was sent. // Verify that the insurance failed message was sent.
expect(sendMessageSpy).toHaveBeenCalledWith( expect(sendMessageSpy).toHaveBeenCalledWith(
sessionID, sessionID,
traderHelper.getTraderById(insurance.traderId), insuranceController.traderHelper.getTraderById(insurance.traderId),
MessageType.INSURANCE_RETURN, MessageType.INSURANCE_RETURN,
insuranceFailedTpl, insuranceFailedTpl,
insurance.items, insurance.items,
insurance.messageContent.maxStorageTime, insurance.maxStorageTime,
insurance.messageContent.systemData, insurance.systemData,
); );
}); });
it("should not send insurance failed message when items are present", () => it("should not send insurance failed message when items are present", () =>
{ {
const traderHelper = container.resolve<TraderHelper>("TraderHelper");
const insurance = insuranceFixture[0]; const insurance = insuranceFixture[0];
const sessionID = "session-id"; const sessionID = "session-id";
const insuranceFailedTpl = "failed-message-template"; const insuranceFailedTpl = "failed-message-template";
@ -1311,12 +1325,12 @@ describe("InsuranceController", () =>
// Verify that the insurance failed message was not sent. // Verify that the insurance failed message was not sent.
expect(sendMessageSpy).toHaveBeenCalledWith( expect(sendMessageSpy).toHaveBeenCalledWith(
sessionID, sessionID,
traderHelper.getTraderById(insurance.traderId), insuranceController.traderHelper.getTraderById(insurance.traderId),
MessageType.INSURANCE_RETURN, MessageType.INSURANCE_RETURN,
insurance.messageContent.templateId, insurance.messageTemplateId,
insurance.items, insurance.items,
insurance.messageContent.maxStorageTime, insurance.maxStorageTime,
insurance.messageContent.systemData, insurance.systemData,
); );
}); });
}); });
@ -1519,14 +1533,17 @@ describe("InsuranceController", () =>
expect(pmcData.InsuredItems.length).toBe(body.items.length); expect(pmcData.InsuredItems.length).toBe(body.items.length);
}); });
it("should return the output with warnings if payment fails", () => it("should update output with warnings if payment fails", () =>
{ {
// Override the payMoney mock to simulate a payment failure with a warning. // Override the payMoney mock to simulate a payment failure with a warning.
const expectedPayMoneyReturn = { const expectedPayMoneyReturn = {
warnings: [{ index: 0, errmsg: "Not enough money to complete transaction", code: 500 }], warnings: [{ index: 0, errmsg: "You broke.", code: 500 }],
otherProperty: "property-value", otherProperty: "property-value",
}; };
mockPayMoney.mockReturnValue(expectedPayMoneyReturn); mockPayMoney.mockImplementation((pmcData, request, sessionID, output) =>
{
output.warnings = expectedPayMoneyReturn.warnings;
});
// Execute the method. // Execute the method.
const response = insuranceController.insure(pmcData, body, sessionId); const response = insuranceController.insure(pmcData, body, sessionId);
@ -1542,10 +1559,13 @@ describe("InsuranceController", () =>
{ {
// Override the payMoney mock to simulate a payment failure with a warning. // Override the payMoney mock to simulate a payment failure with a warning.
const expectedPayMoneyReturn = { const expectedPayMoneyReturn = {
warnings: [{ index: 0, errmsg: "Not enough money to complete transaction", code: 500 }], warnings: [{ index: 0, errmsg: "You broke.", code: 500 }],
otherProperty: "property-value", otherProperty: "property-value",
}; };
mockPayMoney.mockReturnValue(expectedPayMoneyReturn); mockPayMoney.mockImplementation((pmcData, request, sessionID, output) =>
{
output.warnings = expectedPayMoneyReturn.warnings;
});
// Execute the method. // Execute the method.
insuranceController.insure(pmcData, body, sessionId); insuranceController.insure(pmcData, body, sessionId);

View File

@ -55,94 +55,157 @@ describe("BotGenerator", () =>
describe("generateBotNickname", () => describe("generateBotNickname", () =>
{ {
it("should return single name `test` for non pscav assault bot name ", () => it("should choose random firstname for non player scav assault bot", () =>
{ {
const botJsonTemplate = { firstName: ["one", "two"], lastName: [] };
const botGenerationDetails = { isPlayerScav: false, isPmc: true, allPmcsHaveSameNameAsPlayer: false };
const botRole = "assault";
botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 0; botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 0;
const mockPlayerProfile = { Info: { Nickname: "Player Nickname", Level: 1 } }; const mockPlayerProfile = { Info: { Nickname: "Player Nickname", Level: 1 } };
vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(<IPmcData>mockPlayerProfile); vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(<IPmcData>mockPlayerProfile);
const botJsonTemplate = { firstName: ["test"], lastName: [] }; const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole);
const sessionId = "sessionId"; expect(result).toMatch(/(one|two)/);
const isPlayerScav = false;
const botRole = "assault";
const result = botGenerator.generateBotNickname(botJsonTemplate, isPlayerScav, botRole, sessionId);
expect(result).toBe("test");
}); });
it("should return `test assault` for non pscav assault bot with `showTypeInNickname` enabled ", () => it("should choose random lastname for non player scav assault bot", () =>
{ {
const botJsonTemplate = { firstName: [], lastName: [["one", "two"]] };
const botGenerationDetails = { isPlayerScav: false, isPmc: true, allPmcsHaveSameNameAsPlayer: false };
const botRole = "assault";
botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 0;
const mockPlayerProfile = { Info: { Nickname: "Player Nickname", Level: 1 } };
vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(<IPmcData>mockPlayerProfile);
const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole);
expect(result).toMatch(/(one|two)/);
});
it("should choose random firstname and lastname for non player scav assault bot", () =>
{
const botJsonTemplate = { firstName: ["first-one", "first-two"], lastName: [["last-one", "last-two"]] };
const botGenerationDetails = { isPlayerScav: false, isPmc: true, allPmcsHaveSameNameAsPlayer: false };
const botRole = "assault";
botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 0;
const mockPlayerProfile = { Info: { Nickname: "Player Nickname", Level: 1 } };
vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(<IPmcData>mockPlayerProfile);
const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole);
expect(result).toMatch(/first-(one|two) last-(one|two)/);
});
it("should choose random firstname for player scav assault bot", () =>
{
const botJsonTemplate = { firstName: ["one", "two"], lastName: [] };
const botGenerationDetails = { isPlayerScav: true, isPmc: false, allPmcsHaveSameNameAsPlayer: false };
const botRole = "assault";
botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 0;
const mockPlayerProfile = { Info: { Nickname: "Player Nickname", Level: 1 } };
vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(<IPmcData>mockPlayerProfile);
const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole);
expect(result).toMatch(/(one|two)/);
});
it("should choose random lastname for player scav assault bot", () =>
{
const botJsonTemplate = { firstName: [], lastName: [["one", "two"]] };
const botGenerationDetails = { isPlayerScav: true, isPmc: false, allPmcsHaveSameNameAsPlayer: false };
const botRole = "assault";
botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 0;
const mockPlayerProfile = { Info: { Nickname: "Player Nickname", Level: 1 } };
vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(<IPmcData>mockPlayerProfile);
const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole);
expect(result).toMatch(/(one|two)/);
});
it("should choose random firstname and lastname for player scav assault bot", () =>
{
const botJsonTemplate = { firstName: ["first-one", "first-two"], lastName: [["last-one", "last-two"]] };
const botGenerationDetails = { isPlayerScav: true, isPmc: false, allPmcsHaveSameNameAsPlayer: false };
const botRole = "assault";
botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 0;
const mockPlayerProfile = { Info: { Nickname: "Player Nickname", Level: 1 } };
vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(<IPmcData>mockPlayerProfile);
const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole);
expect(result).toMatch(/first-(one|two) last-(one|two)/);
});
it("should append bot type to end of name when showTypeInNickname option is enabled ", () =>
{
const botJsonTemplate = { firstName: ["firstname"], lastName: ["lastname"] };
const botGenerationDetails = { isPlayerScav: false, isPmc: true, allPmcsHaveSameNameAsPlayer: false };
const botRole = "assault";
botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 0;
botGenerator.botConfig.showTypeInNickname = true; botGenerator.botConfig.showTypeInNickname = true;
const mockPlayerProfile = { Info: { Nickname: "Player Nickname", Level: 1 } }; const mockPlayerProfile = { Info: { Nickname: "Player Nickname", Level: 1 } };
vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(<IPmcData>mockPlayerProfile); vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(<IPmcData>mockPlayerProfile);
const botJsonTemplate = { firstName: ["test"], lastName: [] }; const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole);
const sessionId = "sessionId"; expect(result).toBe("firstname lastname assault");
const isPlayerScav = false;
const botRole = "assault";
const result = botGenerator.generateBotNickname(botJsonTemplate, isPlayerScav, botRole, sessionId);
expect(result).toBe("test assault");
}); });
it("should return name `test Player` for bot with same name as player and `addPrefixToSameNamePMCAsPlayerChance` 100%", () => it("should return name prefix for PMC bot with same name as player if allPmcsHaveSameNameAsPlayer is enabled", () =>
{ {
const botJsonTemplate = { firstName: ["player"], lastName: [] };
const botGenerationDetails = { isPlayerScav: false, isPmc: true, allPmcsHaveSameNameAsPlayer: true };
const botRole = "assault";
botGenerator.botConfig.showTypeInNickname = false; botGenerator.botConfig.showTypeInNickname = false;
botGenerator.pmcConfig.addPrefixToSameNamePMCAsPlayerChance = 100; botGenerator.pmcConfig.addPrefixToSameNamePMCAsPlayerChance = 100;
const mockPlayerProfile = { Info: { Nickname: "Player", Level: 1 } }; const mockPlayerProfile = { Info: { Nickname: "player", Level: 1 } };
vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(<IPmcData>mockPlayerProfile);
vi.spyOn(botGenerator.localisationService, "getRandomTextThatMatchesPartialKey").mockReturnValue("test");
const botJsonTemplate = { firstName: ["Player"], lastName: [] };
const sessionId = "sessionId";
const isPlayerScav = false;
const botRole = "assault";
const result = botGenerator.generateBotNickname(botJsonTemplate, isPlayerScav, botRole, sessionId);
expect(result).toBe("test Player");
});
it("should return name `test` for player scav bot", () =>
{
botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 100;
const mockPlayerProfile = { Info: { Nickname: "Player", Level: 1 } };
vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(<IPmcData>mockPlayerProfile); vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(<IPmcData>mockPlayerProfile);
const botJsonTemplate = { firstName: ["test"], lastName: [] }; const getRandomTextThatMatchesPartialKeySpy = vi.spyOn(
(botGenerator as any).localisationService,
"getRandomTextThatMatchesPartialKey",
).mockReturnValue("test");
const sessionId = "sessionId"; const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole);
const isPlayerScav = true;
const botRole = "assault";
const result = botGenerator.generateBotNickname(botJsonTemplate, isPlayerScav, botRole, sessionId); expect(getRandomTextThatMatchesPartialKeySpy).toHaveBeenCalled();
expect(result).toBe("test"); expect(result).toBe("test player");
}); });
it("should return name `test (usec)` for player scav bot", () => it("should generate PMC name in brackets behind scav name when chanceAssaultScavHasPlayerScavName is enabled", () =>
{ {
const botJsonTemplate = { firstName: ["scav"], lastName: [] };
const botGenerationDetails = { isPlayerScav: false, isPmc: true, allPmcsHaveSameNameAsPlayer: false };
const botRole = "assault";
botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 100; botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 100;
botGenerator.databaseServer.getTables().bots.types.usec.firstName = ["usec"]; botGenerator.databaseServer.getTables().bots.types.usec.firstName = ["player"];
botGenerator.databaseServer.getTables().bots.types.bear.firstName = []; botGenerator.databaseServer.getTables().bots.types.bear.firstName = [];
const mockPlayerProfile = { Info: { Nickname: "Player", Level: 1 } }; const mockPlayerProfile = { Info: { Nickname: "Player", Level: 1 } };
vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(<IPmcData>mockPlayerProfile); vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(<IPmcData>mockPlayerProfile);
const botJsonTemplate = { firstName: ["test"], lastName: [] }; const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole);
expect(result).toBe("scav (player)");
const sessionId = "sessionId";
const isPlayerScav = false;
const botRole = "assault";
const result = botGenerator.generateBotNickname(botJsonTemplate, isPlayerScav, botRole, sessionId);
expect(result).toBe("test (usec)");
}); });
}); });
}); });

View File

@ -35,6 +35,7 @@ describe("BotLevelGenerator", () =>
side: "", side: "",
playerLevel: 5, playerLevel: 5,
botRelativeLevelDeltaMax: 0, botRelativeLevelDeltaMax: 0,
botRelativeLevelDeltaMin: 0,
botCountToGenerate: 0, botCountToGenerate: 0,
botDifficulty: "", botDifficulty: "",
isPlayerScav: false, isPlayerScav: false,

View File

@ -88,10 +88,26 @@ describe("HandbookHelper", () =>
expect(result).toBe(0); expect(result).toBe(0);
}); });
it("should return roughly 1380 roubles when given 10 euros ", () => it("should lookup currency value and multiply the input by the value", () =>
{ {
const result = handbookHelper.inRUB(10, Money.EUROS); // Mock the getTemplatePrice method to return a value of 100 roubles
expect(result).closeTo(1379, 10); const getTemplatePriceSpy = vi.spyOn(handbookHelper, "getTemplatePrice").mockReturnValue(100);
const result = handbookHelper.inRUB(5, Money.EUROS);
expect(getTemplatePriceSpy).toHaveBeenCalled();
expect(result).toBe(500);
});
it("should always return a whole number", () =>
{
// Mock the getTemplatePrice method to return a value of 100 roubles
const getTemplatePriceSpy = vi.spyOn(handbookHelper, "getTemplatePrice").mockReturnValue(123.321);
const result = handbookHelper.inRUB(12.21, Money.EUROS);
expect(getTemplatePriceSpy).toHaveBeenCalled();
expect(result).toBe(1506);
}); });
}); });

View File

@ -3,7 +3,8 @@ import { container } from "tsyringe";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { InRaidHelper } from "@spt-aki/helpers/InRaidHelper"; import { InRaidHelper } from "@spt-aki/helpers/InRaidHelper";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
describe("InRaidHelper", () => describe("InRaidHelper", () =>
{ {
@ -19,67 +20,32 @@ describe("InRaidHelper", () =>
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
describe("calculateFenceStandingChangeFromKills", () => describe("resetSkillPointsEarnedDuringRaid", () =>
{ {
it("should return negative value when player kills 2 scavs as scav", () => it("should reset PointsEarnedDuringSession for each skill in profile", () =>
{ {
const fenceStanding = 0; const mockProfile = {
const postRaidPlayerVictims = [{ Side: "Savage", Role: "assault" }, { Side: "Savage", Role: "assault" }]; // Kills Skills: {
Common: [
const databaseServer = container.resolve<DatabaseServer>("DatabaseServer"); { Id: "BotReload", Progress: 160.543, PointsEarnedDuringSession: 42, LastAccess: 1712633904 },
const scavStandingChangeOnKill = databaseServer.getTables().bots.types.assault.experience.standingForKill; { Id: "BotSound", Progress: 145.6547, PointsEarnedDuringSession: 42, LastAccess: 1712633904 },
const result = inraidHelper.calculateFenceStandingChangeFromKills(fenceStanding, postRaidPlayerVictims);
expect(result).toBe(scavStandingChangeOnKill * postRaidPlayerVictims.length); // Scav rep loss times number of scav kills
expect(result).lessThan(0);
});
it("should return positive value when player kills 2 PMCs of different sides as scav", () =>
{ {
const fenceStanding = 0; Id: "Endurance",
const postRaidPlayerVictims = [{ Side: "Usec", Role: "sptUsec" }, { Side: "Bear", Role: "sptBear" }]; // Kills Progress: 223.951157,
PointsEarnedDuringSession: 42,
LastAccess: 1712633904,
},
{ Id: "Strength", Progress: 141.2618, PointsEarnedDuringSession: 42, LastAccess: 1712633904 },
],
},
};
const databaseServer = container.resolve<DatabaseServer>("DatabaseServer"); (inraidHelper as any).resetSkillPointsEarnedDuringRaid(<IPmcData>mockProfile);
const bearStandingChangeOnKill = databaseServer.getTables().bots.types.bear.experience.standingForKill;
const usecStandingChangeOnKill = databaseServer.getTables().bots.types.bear.experience.standingForKill;
const result = inraidHelper.calculateFenceStandingChangeFromKills(fenceStanding, postRaidPlayerVictims); for (const skill of mockProfile.Skills.Common)
expect(result).toBe(bearStandingChangeOnKill + usecStandingChangeOnKill);
expect(result).greaterThan(0);
});
it("should return negative value when player kills 1 PMC, 1 boss and 2 scavs as scav", () =>
{ {
const fenceStanding = 0; expect(skill.PointsEarnedDuringSession).toBe(0);
const postRaidPlayerVictims = [{ Side: "Usec", Role: "sptUsec" }, { Side: "savage", Role: "assault" }, { }
Side: "savage",
Role: "bossBoar",
}, { Side: "savage", Role: "assault" }]; // Kills
const databaseServer = container.resolve<DatabaseServer>("DatabaseServer");
const usecStandingChangeOnKill = databaseServer.getTables().bots.types.bear.experience.standingForKill;
const scavStandingChangeOnKill = databaseServer.getTables().bots.types.assault.experience.standingForKill;
const bossBoarStandingChangeOnKill =
databaseServer.getTables().bots.types.bossboar.experience.standingForKill;
const result = inraidHelper.calculateFenceStandingChangeFromKills(fenceStanding, postRaidPlayerVictims);
expect(result).toBe(
usecStandingChangeOnKill + (scavStandingChangeOnKill * 2) + bossBoarStandingChangeOnKill,
);
expect(result).lessThan(0);
});
it("should return 0 when player kills bot with undefined standing as scav", () =>
{
const fenceStanding = 0;
const postRaidPlayerVictims = [{ Side: "savage", Role: "testRole" }]; // Kills
// Fake getFenceStandingChangeForKillAsScav() returning null
vi.spyOn(inraidHelper, "getFenceStandingChangeForKillAsScav").mockReturnValueOnce(null).mockReturnValueOnce(
null,
);
const result = inraidHelper.calculateFenceStandingChangeFromKills(fenceStanding, postRaidPlayerVictims);
expect(result).toBe(0);
}); });
}); });
}); });

View File

@ -275,44 +275,6 @@ describe("ItemHelper", () =>
}); });
}); });
describe("generateItemsFromStackSlot", () =>
{
it("should generate valid StackSlot item for an AmmoBox", () =>
{
const ammoBox = itemHelper.getItem("57372c89245977685d4159b1"); // "5.45x39mm BT gs ammo pack (30 pcs)"
const parentId = container.resolve<HashUtil>("HashUtil").generate();
const result = itemHelper.generateItemsFromStackSlot(ammoBox[1], parentId);
expect(result.length).toBe(1);
expect(result[0]._id).toBeDefined();
expect(result[0]._tpl).toBe(ammoBox[1]._props.StackSlots[0]._props.filters[0].Filter[0]);
expect(result[0].parentId).toBe(parentId);
expect(result[0].slotId).toBe("cartridges");
expect(result[0].location).toBe(0);
expect(result[0].upd.StackObjectsCount).toBe(ammoBox[1]._props.StackSlots[0]._max_count);
});
it("should log a warning if no IDs are found in Filter", () =>
{
const ammoBox = itemHelper.getItem("57372c89245977685d4159b1"); // "5.45x39mm BT gs ammo pack (30 pcs)"
ammoBox[1]._props.StackSlots[0]._props.filters[0].Filter = []; // Empty the Filter array.
const parentId = container.resolve<HashUtil>("HashUtil").generate();
// Spy on the logger's warning method and mock its implementation to prevent it from being actually called.
const loggerWarningSpy = vi.spyOn((itemHelper as any).logger, "warning").mockImplementation(() =>
{});
itemHelper.generateItemsFromStackSlot(ammoBox[1], parentId);
expect(loggerWarningSpy).toHaveBeenCalled();
// Restore the original behavior
loggerWarningSpy.mockRestore();
});
});
describe("getItems", () => describe("getItems", () =>
{ {
it("should call databaseServer.getTables() and jsonUtil.clone() methods", () => it("should call databaseServer.getTables() and jsonUtil.clone() methods", () =>
@ -451,12 +413,16 @@ describe("ItemHelper", () =>
const itemId = container.resolve<HashUtil>("HashUtil").generate(); const itemId = container.resolve<HashUtil>("HashUtil").generate();
const item: Item = { const item: Item = {
_id: itemId, _id: itemId,
_tpl: "5b40e1525acfc4771e1c6611", // "HighCom Striker ULACH IIIA helmet (Black)" _tpl: "5b40e1525acfc4771e1c6611",
upd: { Repairable: { Durability: 19, MaxDurability: 38 } }, upd: { Repairable: { Durability: 19, MaxDurability: 38 } },
}; };
const getRepairableItemQualityValueSpt = vi.spyOn(itemHelper as any, "getRepairableItemQualityValue")
.mockReturnValue(0.5);
const result = itemHelper.getItemQualityModifier(item); const result = itemHelper.getItemQualityModifier(item);
expect(getRepairableItemQualityValueSpt).toHaveBeenCalled();
expect(result).toBe(0.5); expect(result).toBe(0.5);
}); });
@ -556,40 +522,7 @@ describe("ItemHelper", () =>
describe("getRepairableItemQualityValue", () => describe("getRepairableItemQualityValue", () =>
{ {
it("should return the correct quality value for armor items", () => it("should return the correct quality value", () =>
{
const armor = itemHelper.getItem("5648a7494bdc2d9d488b4583")[1]; // "PACA Soft Armor"
const repairable: Repairable = { Durability: 25, MaxDurability: 50 };
const item: Item = { // Not used for armor, but required for the method.
_id: "",
_tpl: "",
};
// Cast the method to any to allow access to private/protected method.
const result = (itemHelper as any).getRepairableItemQualityValue(armor, repairable, item);
expect(result).toBe(0.5);
});
it("should not use the Repairable MaxDurability property for armor", () =>
{
const armor = itemHelper.getItem("5648a7494bdc2d9d488b4583")[1]; // "PACA Soft Armor"
const repairable: Repairable = {
Durability: 25,
MaxDurability: 1000, // This should be ignored.
};
const item: Item = { // Not used for armor, but required for the method.
_id: "",
_tpl: "",
};
// Cast the method to any to allow access to private/protected method.
const result = (itemHelper as any).getRepairableItemQualityValue(armor, repairable, item);
expect(result).toBe(0.5);
});
it("should return the correct quality value for weapon items", () =>
{ {
const weapon = itemHelper.getItem("5a38e6bac4a2826c6e06d79b")[1]; // "TOZ-106 20ga bolt-action shotgun" const weapon = itemHelper.getItem("5a38e6bac4a2826c6e06d79b")[1]; // "TOZ-106 20ga bolt-action shotgun"
const repairable: Repairable = { Durability: 50, MaxDurability: 100 }; const repairable: Repairable = { Durability: 50, MaxDurability: 100 };
@ -601,7 +534,7 @@ describe("ItemHelper", () =>
expect(result).toBe(Math.sqrt(0.5)); expect(result).toBe(Math.sqrt(0.5));
}); });
it("should fall back to using Repairable MaxDurability for weapon items", () => it("should fall back to using Repairable MaxDurability", () =>
{ {
const weapon = itemHelper.getItem("5a38e6bac4a2826c6e06d79b")[1]; // "TOZ-106 20ga bolt-action shotgun" const weapon = itemHelper.getItem("5a38e6bac4a2826c6e06d79b")[1]; // "TOZ-106 20ga bolt-action shotgun"
weapon._props.MaxDurability = undefined; // Remove the MaxDurability property. weapon._props.MaxDurability = undefined; // Remove the MaxDurability property.
@ -941,6 +874,74 @@ describe("ItemHelper", () =>
}); });
}); });
describe("adoptOrphanedItems", () =>
{
it("should adopt orphaned items by resetting them as base-level items", () =>
{
const rootId = "root-id";
const items = [
{ _id: "first-id", _tpl: "anything1", parentId: "does-not-exist", slotId: "main" },
{ _id: "second-id", _tpl: "anything2", parentId: "first-id", slotId: "slot-id" },
{ _id: "third-id", _tpl: "anything3", parentId: "second-id", slotId: "slot-id" },
{ _id: "forth-id", _tpl: "anything4", parentId: "third-id", slotId: "slot-id" },
];
// Iterate over the items and find the individual orphaned item.
const orphanedItem = items.find((item) => !items.some((parent) => parent._id === item.parentId));
// Setup tests to verify that the orphaned item is in fact orphaned.
expect(orphanedItem.parentId).toBe(items[0].parentId);
expect(orphanedItem.slotId).toBe(items[0].slotId);
// Execute the method.
(itemHelper as any).adoptOrphanedItems(rootId, items);
// Verify that the orphaned items have been adopted.
expect(orphanedItem.parentId).toBe(rootId);
expect(orphanedItem.slotId).toBe("hideout");
});
it("should not adopt items that are not orphaned", () =>
{
const rootId = "root-id";
const items = [
{ _id: "first-id", _tpl: "anything1", parentId: rootId, slotId: "hideout" },
{ _id: "second-id", _tpl: "anything2", parentId: "first-id", slotId: "slot-id" },
{ _id: "third-id", _tpl: "anything3", parentId: "second-id", slotId: "slot-id" },
{ _id: "forth-id", _tpl: "anything4", parentId: "third-id", slotId: "slot-id" },
];
// Execute the method.
const adopted = (itemHelper as any).adoptOrphanedItems(rootId, items);
// Verify that the orphaned items have been adopted.
expect(adopted).toStrictEqual(items);
});
it("should remove location data from adopted items", () =>
{
const rootId = "root-id";
const items = [
{
_id: "first-id",
_tpl: "anything1",
parentId: "does-not-exist",
slotId: "main",
location: { x: 1, y: 2, r: 3, isSearched: true }, // Should be removed.
},
{ _id: "second-id", _tpl: "anything2", parentId: "first-id", slotId: "slot-id" },
{ _id: "third-id", _tpl: "anything3", parentId: "second-id", slotId: "slot-id" },
{ _id: "forth-id", _tpl: "anything4", parentId: "third-id", slotId: "slot-id" },
];
// Execute the method.
(itemHelper as any).adoptOrphanedItems(rootId, items);
// Verify that the location property has been removed.
expect(items).not.toHaveProperty("location");
});
});
describe("splitStack", () => describe("splitStack", () =>
{ {
it("should return array of two items when provided item over its natural stack size limit", () => it("should return array of two items when provided item over its natural stack size limit", () =>

View File

@ -42,23 +42,23 @@ describe("ItemBaseClassService", () =>
it("should return false when the base item type is passed in", () => it("should return false when the base item type is passed in", () =>
{ {
// Remove item from base cache // Remove item from base cache
const result = itemBaseClassService.itemHasBaseClass("54009119af1c881c07000029", []); // "Base item" const result = itemBaseClassService.itemHasBaseClass("54009119af1c881c07000029", []);
expect(result).toBe(false); expect(result).toBe(false);
}); });
it("should return true when an item is passed in", () => it("should return true when a med item is passed in with the meds base class", () =>
{ {
const salewaTpl = "544fb45d4bdc2dee738b4568"; const salewaTpl = "544fb45d4bdc2dee738b4568";
// Remove item from base cache // Remove item from base cache
delete itemBaseClassService.itemBaseClassesCache[salewaTpl]; delete itemBaseClassService.itemBaseClassesCache[salewaTpl];
const result = itemBaseClassService.itemHasBaseClass(salewaTpl, ["543be5664bdc2dd4348b4569"]); // "Meds" type const result = itemBaseClassService.itemHasBaseClass(salewaTpl, ["543be5664bdc2dd4348b4569"]);
expect(result).toBe(true); expect(result).toBe(true);
}); });
it("should return true when an item and 2 matching base classes are passed in", () => it("should return true when an item and two matching base classes are passed in", () =>
{ {
const salewaTpl = "544fb45d4bdc2dee738b4568"; const salewaTpl = "544fb45d4bdc2dee738b4568";
@ -67,7 +67,7 @@ describe("ItemBaseClassService", () =>
const result = itemBaseClassService.itemHasBaseClass(salewaTpl, [ const result = itemBaseClassService.itemHasBaseClass(salewaTpl, [
"543be5664bdc2dd4348b4569", "543be5664bdc2dd4348b4569",
"54009119af1c881c07000029", "54009119af1c881c07000029",
]); // "Meds" and "Item" type ]); // "Meds" and "Item" base classes
expect(result).toBe(true); expect(result).toBe(true);
}); });
@ -84,15 +84,28 @@ describe("ItemBaseClassService", () =>
delete itemBaseClassService.itemBaseClassesCache[salewaTpl]; delete itemBaseClassService.itemBaseClassesCache[salewaTpl];
// Perform check // Perform check
const result = itemBaseClassService.itemHasBaseClass(salewaTpl, ["543be5664bdc2dd4348b4569"]); // "Meds" type const result = itemBaseClassService.itemHasBaseClass(salewaTpl, ["543be5664bdc2dd4348b4569"]);
expect(result).toBe(true); expect(result).toBe(true);
expect(hydrateItemBaseClassCacheSpy).toHaveBeenCalled(); expect(hydrateItemBaseClassCacheSpy).toHaveBeenCalled();
}); });
it("should throw an exception when an invalid item is passed in", () => it("should return false for any item template ID that does not exist", () =>
{ {
expect(() => itemBaseClassService.itemHasBaseClass("fakeTpl", ["543be5664bdc2dd4348b4569"])).toThrowError(); const result = itemBaseClassService.itemHasBaseClass("not-a-valid-template-id", [
"543be5664bdc2dd4348b4569",
]);
expect(result).toBe(false);
});
it("should return false for any item template ID without the Item type ", () =>
{
const result = itemBaseClassService.itemHasBaseClass("54009119af1c881c07000029", [
"543be5664bdc2dd4348b4569",
]);
expect(result).toBe(false);
}); });
}); });

View File

@ -71,48 +71,50 @@ describe("PaymentService", () =>
const itemEventRouterResponse = { const itemEventRouterResponse = {
warnings: [], warnings: [],
profileChanges: { sessionID: { _id: sessionID, items: { new: [], change: [], del: [] } } }, profileChanges: { [sessionID]: { _id: sessionID, items: { new: [], change: [], del: [] } } },
} as unknown as IItemEventRouterResponse; } as unknown as IItemEventRouterResponse;
// Mock the logger debug method to return void. // Mock the logger debug method to return void.
vi.spyOn((paymentService as any).logger, "debug").mockImplementation(() => vi.spyOn((paymentService as any).logger, "debug").mockResolvedValue(undefined);
{});
// Mock the trader helper to return a trader with the currency of Roubles. // Mock the trader helper to return a trader with the currency of Roubles.
const traderHelperGetTraderSpy = vi.spyOn((paymentService as any).traderHelper, "getTrader") const getTraderSpy = vi.spyOn((paymentService as any).traderHelper, "getTrader").mockReturnValue(
.mockReturnValue({ tid: traderId, currency: "RUB" } as unknown as ITraderBase); { tid: traderId, currency: "RUB" } as unknown as ITraderBase,
);
// Mock the addPaymentToOutput method to subtract the item cost from the money stack. // Mock the addPaymentToOutput method to subtract the item cost from the money stack.
const addPaymentToOutputSpy = vi.spyOn(paymentService as any, "addPaymentToOutput").mockImplementation(() => const addPaymentToOutputSpy = vi.spyOn(paymentService as any, "addPaymentToOutput").mockImplementation(
(
pmcData: IPmcData,
currencyTpl: string,
amountToPay: number,
sessionIdentifier: string,
output: IItemEventRouterResponse,
) =>
{ {
moneyItem.upd.StackObjectsCount -= costAmount; moneyItem.upd.StackObjectsCount -= costAmount;
return { warnings: [], profileChanges: { [sessionID]: { items: { change: [moneyItem] } } } }; output.profileChanges[sessionIdentifier].items.change.push(moneyItem);
}); },
);
// Mock the traderHelper lvlUp method to return void. // Mock the traderHelper lvlUp method to return void.
const traderHelperLvlUpSpy = vi.spyOn((paymentService as any).traderHelper, "lvlUp").mockImplementation( const lvlUpSpy = vi.spyOn((paymentService as any).traderHelper, "lvlUp").mockResolvedValue(undefined);
() =>
{},
);
const output = paymentService.payMoney( paymentService.payMoney(pmcData, processBuyTradeRequestData, sessionID, itemEventRouterResponse);
pmcData,
processBuyTradeRequestData,
sessionID,
itemEventRouterResponse,
);
// Check for absence of output warnings. // Check for absence of output warnings.
expect(output.warnings).toHaveLength(0); expect(itemEventRouterResponse.warnings).toHaveLength(0);
// Check that the currency change was correctly handled. // Check that the currency change was correctly handled.
expect(output.profileChanges[sessionID].items.change).toHaveLength(1); expect(itemEventRouterResponse.profileChanges[sessionID].items.change).toHaveLength(1);
expect(output.profileChanges[sessionID].items.change[0]._id).toBe(costItemId); expect(itemEventRouterResponse.profileChanges[sessionID].items.change[0]._id).toBe(costItemId);
expect(output.profileChanges[sessionID].items.change[0]._tpl).toBe(costItemTpl); expect(itemEventRouterResponse.profileChanges[sessionID].items.change[0]._tpl).toBe(costItemTpl);
expect(output.profileChanges[sessionID].items.change[0].upd.StackObjectsCount).toBe(costAmount * 3); expect(itemEventRouterResponse.profileChanges[sessionID].items.change[0].upd.StackObjectsCount).toBe(
costAmount * 3,
);
// Check if mocked methods were called as expected. // Check if mocked methods were called as expected.
expect(traderHelperGetTraderSpy).toBeCalledTimes(1); expect(getTraderSpy).toBeCalledTimes(1);
expect(addPaymentToOutputSpy).toBeCalledWith( expect(addPaymentToOutputSpy).toBeCalledWith(
expect.anything(), expect.anything(),
costItemTpl, costItemTpl,
@ -120,7 +122,7 @@ describe("PaymentService", () =>
sessionID, sessionID,
expect.anything(), expect.anything(),
); );
expect(traderHelperLvlUpSpy).toBeCalledTimes(1); expect(lvlUpSpy).toBeCalledTimes(1);
}); });
}); });

View File

@ -61,11 +61,11 @@ describe("PlayerService", () =>
expect(result).toBe(25); expect(result).toBe(25);
}); });
it("should return 79 when player xp is 68,206,066", () => it("should return 79 when player xp is 81,126,895", () =>
{ {
const playerProfile = { const playerProfile = {
Info: { Info: {
Experience: 68206066, // Via wiki: https://escapefromtarkov.fandom.com/wiki/Character_skills#Levels Experience: 81126895, // Via wiki: https://escapefromtarkov.fandom.com/wiki/Character_skills#Levels
}, },
}; };

View File

@ -0,0 +1,501 @@
/* eslint-disable @typescript-eslint/naming-convention */
import "reflect-metadata";
import { container } from "tsyringe";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { RagfairPriceService } from "@spt-aki/services/RagfairPriceService";
import { MinMax } from "@spt-aki/models/common/MinMax";
import { Money } from "@spt-aki/models/enums/Money";
describe("RagfairPriceService", () =>
{
let ragfairPriceService: any; // Using "any" to access private/protected methods without type errors.
beforeEach(() =>
{
ragfairPriceService = container.resolve<RagfairPriceService>("RagfairPriceService");
});
afterEach(() =>
{
vi.restoreAllMocks();
});
describe("getDynamicOfferPriceForOffer", () =>
{
it("should return zero when empty offerItems array is passed", () =>
{
const offerItems = [];
const desiredCurrency = Money.ROUBLES;
const isPackOffer = false;
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
expect(price).toEqual(0);
});
it("should return non-zero number when valid item is passed", () =>
{
const offerItems = [{
_id: "d445ea263cdfc5f278334264",
_tpl: "57e3dba62459770f0c32322b",
parentId: "631abbff398cc0170cbd3089",
slotId: "mod_pistol_grip",
}];
const desiredCurrency = Money.ROUBLES;
const isPackOffer = false;
const expectedPrice = 42069;
// Mock the getDynamicItemPrice method to return a static price.
vi.spyOn(ragfairPriceService, "getDynamicItemPrice").mockReturnValue(expectedPrice);
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
expect(price).toBe(expectedPrice);
});
it("should always return a whole number", () =>
{
const offerItems = [{
_id: "d445ea263cdfc5f278334264",
_tpl: "57e3dba62459770f0c32322b",
parentId: "631abbff398cc0170cbd3089",
slotId: "mod_pistol_grip",
}];
const desiredCurrency = Money.ROUBLES;
const isPackOffer = false;
const originalPrice = 42069.999999999;
// Mock the getDynamicItemPrice method to return a static price.
vi.spyOn(ragfairPriceService, "getDynamicItemPrice").mockReturnValue(originalPrice);
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
expect(price).toBeGreaterThan(originalPrice);
expect(price).toBe(Math.round(originalPrice));
});
it("should skip prices for soft armour inserts", () =>
{
const offerItems = [{
_id: "d445ea263cdfc5f278334264",
_tpl: "657080a212755ae0d907ad04",
parentId: "631abbff398cc0170cbd3089",
slotId: "Soft_armor_front",
}];
const desiredCurrency = Money.ROUBLES;
const isPackOffer = false;
// Mock the getDynamicItemPrice method.
const getDynamicItemPriceSpy = vi.spyOn(ragfairPriceService, "getDynamicItemPrice");
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
expect(price).toBe(0);
expect(getDynamicItemPriceSpy).not.toHaveBeenCalled();
});
it("should not add value of mods to weapon preset", () =>
{
const offerItems = [{
_id: "344d02bbf2102ce4e145bf35",
_tpl: "579204f224597773d619e051",
upd: {
StackObjectsCount: 1,
UnlimitedCount: true,
sptPresetId: "5841499024597759f825ff3e",
Repairable: { Durability: 90, MaxDurability: 90 },
},
}, {
_id: "59c6897a59ed48f1ca02f659",
_tpl: "5448c12b4bdc2d02308b456f",
parentId: "344d02bbf2102ce4e145bf35",
slotId: "mod_magazine",
}, {
_id: "7e8062d4bc57b56927c2d117",
_tpl: "6374a822e629013b9c0645c8",
parentId: "344d02bbf2102ce4e145bf35",
slotId: "mod_reciever",
}, {
_id: "3b09149e8b7833dc5fdd32a4",
_tpl: "63c6adcfb4ba094317063742",
parentId: "7e8062d4bc57b56927c2d117",
slotId: "mod_sight_rear",
}, {
_id: "e833a5c26af29870df9cdd2e",
_tpl: "6374a7e7417239a7bf00f042",
parentId: "344d02bbf2102ce4e145bf35",
slotId: "mod_pistolgrip",
}];
const desiredCurrency = Money.ROUBLES;
const isPackOffer = false;
const expectedPrice = 10000;
// Mock the getDynamicItemPrice method to return a static price.
const getDynamicItemPriceSpy = vi.spyOn(ragfairPriceService, "getDynamicItemPrice").mockReturnValue(
expectedPrice,
);
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
expect(price).toBe(expectedPrice);
expect(getDynamicItemPriceSpy).toHaveBeenCalledTimes(1);
});
it("should sum value of all offer items", () =>
{
const offerItems = [{
_id: "59c6897a59ed48f1ca02f659",
_tpl: "5448c12b4bdc2d02308b456f",
parentId: "344d02bbf2102ce4e145bf35",
slotId: "mod_magazine",
}, {
_id: "7e8062d4bc57b56927c2d117",
_tpl: "6374a822e629013b9c0645c8",
parentId: "344d02bbf2102ce4e145bf35",
slotId: "mod_reciever",
}, {
_id: "3b09149e8b7833dc5fdd32a4",
_tpl: "63c6adcfb4ba094317063742",
parentId: "7e8062d4bc57b56927c2d117",
slotId: "mod_sight_rear",
}, {
_id: "e833a5c26af29870df9cdd2e",
_tpl: "6374a7e7417239a7bf00f042",
parentId: "344d02bbf2102ce4e145bf35",
slotId: "mod_pistolgrip",
}];
const desiredCurrency = Money.ROUBLES;
const isPackOffer = false;
const expectedPrice = 10000;
// Mock the getDynamicItemPrice method to return a static price.
const getDynamicItemPriceSpy = vi.spyOn(ragfairPriceService, "getDynamicItemPrice").mockReturnValue(
expectedPrice,
);
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
expect(price).toBe(expectedPrice * offerItems.length);
expect(getDynamicItemPriceSpy).toHaveBeenCalledTimes(offerItems.length);
});
});
describe("getDynamicItemPrice", () =>
{
it("should not return zero for a valid template ID", () =>
{
const itemTemplateId = "5e54f6af86f7742199090bf3";
const desiredCurrency = Money.ROUBLES;
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
expect(price).not.toBe(0);
});
it("should use trader price if it is higher than flea price and configuration allows it", () =>
{
const itemTemplateId = "5e54f6af86f7742199090bf3";
const desiredCurrency = Money.ROUBLES;
const mockTraderPrice = 20000;
const mockFleaPrice = 15000;
const getOfferTypeRangeValues = { max: 1, min: 1 };
// Mock the configs to allow using trader price if higher. Disable other adjustments for isolation.
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = true;
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
// Mock the getFleaPriceForItem method to return a static price.
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockFleaPrice);
// Mock the getHighestSellToTraderPrice method to return a higher static price.
vi.spyOn((ragfairPriceService as any).traderHelper, "getHighestSellToTraderPrice").mockReturnValue(
mockTraderPrice,
);
// Mock the getOfferTypeRangeValues method to return a static minMax.
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
// Call the method.
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
expect(price).toBe(mockTraderPrice);
});
it("should adjust flea price when below handbook price and configuration allows it", () =>
{
const itemTemplateId = "5e54f6af86f7742199090bf3";
const desiredCurrency = Money.ROUBLES;
const mockFleaPrice = 1;
const handbookPrice = 10000;
const adjustedPrice = 9000;
const getOfferTypeRangeValues = { max: 1, min: 1 };
// Enable adjustment for prices below handbook price. Disable other adjustments for isolation.
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = true;
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
// Mock the getFleaPriceForItem method to return a static price below the handbook price.
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockFleaPrice);
// Mock the adjustPriceIfBelowHandbook method to simulate price adjustment.
vi.spyOn(ragfairPriceService, "adjustPriceIfBelowHandbook").mockImplementation(
(price: number, templateId) =>
{
return price < handbookPrice ? adjustedPrice : price;
},
);
// Mock the getOfferTypeRangeValues method to return a static minMax.
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
// Call the method.
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
// Verify the price is adjusted correctly according to the mocked handbook price adjustment logic.
expect(price).toBe(adjustedPrice);
});
it("should handle weapon preset prices correctly", () =>
{
const itemTemplateId = "579204f224597773d619e051";
const desiredCurrency = Money.ROUBLES;
const mockPresetPrice = 25000;
const getOfferTypeRangeValues = { max: 1, min: 1 };
const offerItems = [{
_id: "344d02bbf2102ce4e145bf35",
_tpl: "579204f224597773d619e051",
upd: {
StackObjectsCount: 1,
UnlimitedCount: true,
sptPresetId: "5841499024597759f825ff3e",
Repairable: { Durability: 90, MaxDurability: 90 },
},
}, {
_id: "7e8062d4bc57b56927c2d117",
_tpl: "6374a822e629013b9c0645c8",
parentId: "344d02bbf2102ce4e145bf35",
slotId: "mod_reciever",
}];
const item = offerItems[0];
// Disable other adjustments for isolation.
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
// Mock getFleaPriceForItem to bypass initial flea price fetch
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(0);
// Mock the isPresetBaseClass method to return true for the item
vi.spyOn((ragfairPriceService as any).presetHelper, "isPresetBaseClass").mockReturnValue(true);
// Mock the getWeaponPresetPrice method to return a specific preset price
const getWeaponPresetPriceSpy = vi.spyOn(ragfairPriceService, "getWeaponPresetPrice").mockReturnValue(
mockPresetPrice,
);
// Mock the getOfferTypeRangeValues method to return a static minMax.
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
// Mock the getItemQualityModifier method to return 1 (no change)
vi.spyOn((ragfairPriceService as any).itemHelper, "getItemQualityModifier").mockReturnValue(1);
// Call the method with the mock item and offer items
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency, item, offerItems);
// Call the method.
expect(price).toBe(mockPresetPrice);
// Additionally, you can verify that getWeaponPresetPrice was called with the correct parameters
expect(getWeaponPresetPriceSpy).toHaveBeenCalledWith(item, offerItems, expect.any(Number));
});
it("should update price based on the ragfair config item price multiplier values", () =>
{
const itemTemplateId = "5e54f6af86f7742199090bf3";
const desiredCurrency = Money.ROUBLES;
const mockFleaPrice = 20000;
const itemPriceMultiplier = 2;
const getOfferTypeRangeValues = { max: 1, min: 1 };
// Mock the ragfair config to have a price multiplier of 2. Disable other adjustments for isolation.
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = itemPriceMultiplier;
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
// Mock the getFleaPriceForItem method to return a static price.
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockFleaPrice);
// Mock the getOfferTypeRangeValues method to return a static minMax.
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
// Call the method.
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
expect(price).toBe(mockFleaPrice * itemPriceMultiplier);
});
it("should adjust price when durability is not perfect", () =>
{
const itemTemplateId = "579204f224597773d619e051";
const desiredCurrency = Money.ROUBLES;
const mockPrice = 25000;
const mockDurabilityMulti = 0.5;
const getOfferTypeRangeValues = { max: 1, min: 1 };
const offerItems = [{
_id: "344d02bbf2102ce4e145bf35",
_tpl: "579204f224597773d619e051",
upd: {
StackObjectsCount: 1,
UnlimitedCount: true,
sptPresetId: "5841499024597759f825ff3e",
Repairable: { Durability: 40, MaxDurability: 90 },
},
}, {
_id: "7e8062d4bc57b56927c2d117",
_tpl: "6374a822e629013b9c0645c8",
parentId: "344d02bbf2102ce4e145bf35",
slotId: "mod_reciever",
}];
const item = offerItems[0];
// Disable other adjustments for isolation.
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
// Mock getFleaPriceForItem to bypass initial flea price fetch
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(0);
// Mock the isPresetBaseClass method to return true for the item
vi.spyOn((ragfairPriceService as any).presetHelper, "isPresetBaseClass").mockReturnValue(true);
// Mock the getWeaponPresetPrice method to return a specific preset price
vi.spyOn(ragfairPriceService, "getWeaponPresetPrice").mockReturnValue(mockPrice);
// Mock the getOfferTypeRangeValues method to return a static minMax.
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
// Mock the getItemQualityModifier method to return 1 (no change)
const getItemQualityModifierSpy = vi.spyOn(
(ragfairPriceService as any).itemHelper,
"getItemQualityModifier",
).mockReturnValue(mockDurabilityMulti);
// Call the method.
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency, item, offerItems);
expect(getItemQualityModifierSpy).toHaveBeenCalled();
expect(price).toBe(mockPrice * mockDurabilityMulti);
});
it("should adjust unreasonable prices based on ragfair config unreasonable price values", () =>
{
const itemTemplateId = "5c052f6886f7746b1e3db148";
const desiredCurrency = Money.ROUBLES;
const mockFleaPrice = 9999999;
const getOfferTypeRangeValues = { max: 1, min: 1 };
const mockBaseClassTemplateId = "57864a66245977548f04a81f";
const mockUnreasonableModPrices = {
itemType: "Electronics",
enabled: true,
handbookPriceOverMultiplier: 11,
newPriceHandbookMultiplier: 11,
};
// Mock the Disable unreasonableModPrices config. Disable other adjustments for isolation.
ragfairPriceService.ragfairConfig.dynamic.unreasonableModPrices[mockBaseClassTemplateId] =
mockUnreasonableModPrices;
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
// Mock getFleaPriceForItem to bypass initial flea price fetch
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockFleaPrice);
// Mock isOfBaseclass to ensure that the item is always of the base class
const isOfBaseclassSpy = vi.spyOn((ragfairPriceService as any).itemHelper, "isOfBaseclass").mockReturnValue(
true,
);
// Mock the adjustUnreasonablePrice method to ensure it was called
const adjustUnreasonablePriceSpy = vi.spyOn(ragfairPriceService, "adjustUnreasonablePrice");
// Mock the getOfferTypeRangeValues method to return a static minMax
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
// Call the method.
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
expect(isOfBaseclassSpy).toHaveBeenCalled();
expect(adjustUnreasonablePriceSpy).toHaveBeenCalled();
expect(price).toBeLessThan(mockFleaPrice);
});
it("should vary the price within a random range", () =>
{
const itemTemplateId = "5e54f6af86f7742199090bf3";
const desiredCurrency = Money.ROUBLES;
const mockFleaPrice = 10000;
const mockRandomiseOfferPrice = 9500;
// Mock the configs to allow using trader price if higher. Disable other adjustments for isolation.
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
// Mock the getFleaPriceForItem method to return a static price
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockFleaPrice);
// Mock the isPresetBaseClass method to return false
vi.spyOn((ragfairPriceService as any).presetHelper, "isPresetBaseClass").mockReturnValue(false);
// Mock the randomiseOfferPrice method to have a simplified implementation
const randomiseOfferPriceSpy = vi.spyOn(ragfairPriceService, "randomiseOfferPrice").mockReturnValue(
mockRandomiseOfferPrice,
);
// Call the method.
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
expect(randomiseOfferPriceSpy).toHaveBeenCalled();
expect(price).toBe(mockRandomiseOfferPrice);
});
it("should convert currency", () =>
{
const itemTemplateId = "5e54f6af86f7742199090bf3";
const desiredCurrency = Money.DOLLARS;
const mockRoublePrice = 10000;
const mockDollarPrice = 500;
const getOfferTypeRangeValues = { max: 1, min: 1 };
// Mock the configs to allow using trader price if higher. Disable other adjustments for isolation.
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
// Mock the getFleaPriceForItem method to return a static price.
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockRoublePrice);
// Mock the getOfferTypeRangeValues method to return a static minMax
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
// Mock the fromRUB method to convert the price to a different currency
const fromRUBSpy = vi.spyOn((ragfairPriceService as any).handbookHelper, "fromRUB").mockReturnValue(
mockDollarPrice,
);
// Call the method.
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
expect(fromRUBSpy).toHaveBeenCalledWith(mockRoublePrice, desiredCurrency);
expect(price).not.toBe(mockRoublePrice);
expect(price).toBe(mockDollarPrice);
});
});
});