0
0
mirror of https://github.com/sp-tarkov/server.git synced 2025-02-13 09:50:43 -05:00

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: SPT-AKI/Server#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,
"5c94bbff86f7747ee735c08f": 1
},
"bosssanitar": {},
"bosssanitar": {
"5efde6b4f5448336730dbd61": 1
},
"bosstagilla": {},
"bossknight": {},
"bosszryachiy": {},
@ -387,8 +389,12 @@
"pmcbot": {
"60098ad7c2240c0fe85c570a": 2
},
"arenafighterevent": {},
"arenafighter": {},
"arenafighterevent": {
"5734758f24597738025ee253": 1
},
"arenafighter": {
"5734758f24597738025ee253": 1
},
"crazyassaultevent": {},
"assaultgroup": {},
"gifter": {},

View File

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

View File

@ -1,9 +1,12 @@
{
"runIntervalSeconds": 10,
"hoursForSkillCrafting": 28800,
"runIntervalValues": {
"runIntervalValues": {
"inRaid": 60,
"outOfRaid": 10
},
"expCraftAmount": 10
"expCraftAmount": 10,
"overrideCraftTimeSeconds": -1,
"overrideBuildTimeSeconds": -1,
"updateProfileHideoutWhenActiveWithinMinutes": 90
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2004,33 +2004,108 @@
],
"generation": {
"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": {
"max": 1,
"min": 0
"weights": {
"0": 1,
"1": 2,
"2": 0
},
"whitelist": []
},
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": {
"max": 5,
"min": 0
"weights": {
"0": 1,
"1": 2,
"2": 1,
"3": 1,
"4": 0,
"5": 0
},
"whitelist": []
},
"healing": {
"max": 2,
"min": 1
"weights": {
"0": 1,
"1": 2,
"2": 1
},
"looseLoot": {
"max": 3,
"min": 0
"whitelist": []
},
"magazines": {
"max": 4,
"min": 2
"weights": {
"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": {
"max": 0,
"min": 0
"weights": {
"0": 1,
"1": 0
},
"whitelist": []
},
"stims": {
"max": 1,
"min": 0
"weights": {
"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": []
},
"drink": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"drugs": {
"weights": {
"0": 1,
@ -2127,6 +2135,14 @@
},
"whitelist": []
},
"food": {
"weights": {
"0": 10,
"1": 5,
"2": 2
},
"whitelist": []
},
"grenades": {
"weights": {
"0": 1,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@
"dependencies": {
"atomically": "~1.7",
"buffer-crc32": "^1.0.0",
"closest-match": "~1.3",
"date-fns": "~2.30",
"date-fns-tz": "~2.0",
"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 { IEmptyRequestData } from "@spt-aki/models/eft/common/IEmptyRequestData";
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 { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil";
@ -44,6 +45,15 @@ export class BotCallbacks
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
* @returns IGetBodyResponseData

View File

@ -65,15 +65,6 @@ export class InraidCallbacks
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
* @returns JSON as string

View File

@ -83,7 +83,7 @@ export class BotController
/**
* 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 diffLevel difficulty level server requested settings for
* @returns Difficulty object
@ -104,7 +104,7 @@ export class BotController
// Check value chosen in pre-raid difficulty dropdown
// If value is not 'asonline', change requested difficulty to be what was chosen in dropdown
const botDifficultyDropDownValue = raidConfig.wavesSettings.botDifficulty.toLowerCase();
const botDifficultyDropDownValue = raidConfig?.wavesSettings.botDifficulty.toLowerCase() ?? "asonline";
if (botDifficultyDropDownValue !== "asonline")
{
difficulty = this.botDifficultyHelper.convertBotDifficultyDropdownToBotDifficulty(
@ -140,6 +140,31 @@ export class BotController
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
* @param sessionId Session id

View File

@ -147,7 +147,7 @@ export class BuildController
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 weaponBuilds = profile.userbuilds.weaponBuilds;
@ -155,7 +155,7 @@ export class BuildController
const magazineBuilds = profile.userbuilds.magazineBuilds;
// 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)
{
weaponBuilds.splice(weaponBuilds.indexOf(matchingWeaponBuild), 1);
@ -164,7 +164,7 @@ export class BuildController
}
// 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)
{
equipmentBuilds.splice(equipmentBuilds.indexOf(matchingEquipmentBuild), 1);
@ -173,7 +173,7 @@ export class BuildController
}
// 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)
{
magazineBuilds.splice(magazineBuilds.indexOf(matchingMagazineBuild), 1);
@ -182,7 +182,9 @@ export class BuildController
}
// 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 { LocalisationService } from "@spt-aki/services/LocalisationService";
import { OpenZoneService } from "@spt-aki/services/OpenZoneService";
import { ProfileActivityService } from "@spt-aki/services/ProfileActivityService";
import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService";
import { RaidTimeAdjustmentService } from "@spt-aki/services/RaidTimeAdjustmentService";
import { SeasonalEventService } from "@spt-aki/services/SeasonalEventService";
@ -78,6 +79,7 @@ export class GameController
@inject("ItemBaseClassService") protected itemBaseClassService: ItemBaseClassService,
@inject("GiftService") protected giftService: GiftService,
@inject("RaidTimeAdjustmentService") protected raidTimeAdjustmentService: RaidTimeAdjustmentService,
@inject("ProfileActivityService") protected profileActivityService: ProfileActivityService,
@inject("ApplicationContext") protected applicationContext: ApplicationContext,
@inject("ConfigServer") protected configServer: ConfigServer,
)
@ -109,6 +111,8 @@ export class GameController
// Store client start time in app context
this.applicationContext.addValue(ContextVariableType.CLIENT_START_TIMESTAMP, startTimeStampMS);
this.profileActivityService.setActivityTimestamp(sessionID);
if (this.coreConfig.fixes.fixShotgunDispersion)
{
this.fixShotgunDispersions();
@ -203,12 +207,16 @@ export class GameController
this.hideoutHelper.setHideoutImprovementsToCompleted(pmcProfile);
this.hideoutHelper.unlockHideoutWallInProfile(pmcProfile);
this.profileFixerService.addMissingIdsToBonuses(pmcProfile);
this.profileFixerService.fixBitcoinProductionTime(pmcProfile);
}
this.logProfileDetails(fullProfile);
this.adjustLabsRaiderSpawnRate();
this.adjustHideoutCraftTimes();
this.adjustHideoutBuildTimes();
this.removePraporTestMessage();
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
{
const mapsDb = this.databaseServer.getTables().locations;
@ -460,6 +508,7 @@ export class GameController
*/
public getKeepAlive(sessionId: string): IGameKeepAliveResponse
{
this.profileActivityService.setActivityTimestamp(sessionId);
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 { LocalisationService } from "@spt-aki/services/LocalisationService";
import { PlayerService } from "@spt-aki/services/PlayerService";
import { ProfileActivityService } from "@spt-aki/services/ProfileActivityService";
import { HashUtil } from "@spt-aki/utils/HashUtil";
import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil";
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
@ -79,6 +80,7 @@ export class HideoutController
@inject("HideoutHelper") protected hideoutHelper: HideoutHelper,
@inject("ScavCaseRewardGenerator") protected scavCaseRewardGenerator: ScavCaseRewardGenerator,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ProfileActivityService") protected profileActivityService: ProfileActivityService,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("FenceService") protected fenceService: FenceService,
@ -1323,7 +1325,13 @@ export class HideoutController
{
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);
}

View File

@ -196,7 +196,7 @@ export class InsuranceController
!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)
{
this.processRegularItems(insured, toDelete, parentAttachmentsMap);

View File

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

View File

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

View File

@ -258,13 +258,15 @@ export class BotGenerator
* @param botJsonTemplate x.json from database
* @param botGenerationDetails
* @param botRole role of bot e.g. assault
* @param sessionId profile session id
* @returns Nickname for bot
*/
// TODO: Remove sessionId parameter from this function in v3.9.0
protected generateBotNickname(
botJsonTemplate: IBotType,
botGenerationDetails: BotGenerationDetails,
botRole: string,
sessionId: string,
sessionId?: string, // @deprecated as of v3.8.1
): string
{
const isPlayerScav = botGenerationDetails.isPlayerScav;
@ -273,9 +275,9 @@ export class BotGenerator
this.randomUtil.getArrayValue(botJsonTemplate.lastName) || ""
}`;
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 (isPlayerScav)
@ -300,7 +302,7 @@ export class BotGenerator
if (botGenerationDetails.isPmc && botGenerationDetails.allPmcsHaveSameNameAsPlayer)
{
const prefix = this.localisationService.getRandomTextThatMatchesPartialKey("pmc-name_prefix_");
name = `${prefix} ${botGenerationDetails.playerName}`;
name = `${prefix} ${name}`;
}
return name;

View File

@ -98,6 +98,10 @@ export class BotLootGenerator
);
const healingItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.healing.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 grenadeCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.grenades.weights));
@ -145,6 +149,30 @@ export class BotLootGenerator
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
this.addLootFromPool(
this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.STIM_ITEMS, botJsonTemplate),
@ -282,19 +310,6 @@ export class BotLootGenerator
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
this.addLootFromPool(
// eslint-disable-next-line @typescript-eslint/naming-convention

View File

@ -74,7 +74,7 @@ export class FenceBaseAssortGenerator
}
}
// Only allow rigs with no slots (carrier rigs)
// Only allow rigs with no slots (carrier rigs)
if (this.itemHelper.isOfBaseclass(rootItemDb._id, BaseClasses.VEST) && rootItemDb._props.Slots.length > 0)
{
continue;
@ -95,6 +95,15 @@ export class FenceBaseAssortGenerator
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))
{
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
{
return this.itemHelper.isOfBaseclass(itemTpl, BaseClasses.AMMO_BOX)

View File

@ -871,7 +871,7 @@ export class LocationGenerator
// Create array with just magazine
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
this.itemHelper.fillMagazineWithRandomCartridge(

View File

@ -99,12 +99,15 @@ export class LootGenerator
&& options.itemTypeWhitelist.includes(x[1]._parent)
);
const randomisedItemCount = this.randomUtil.getInt(options.itemCount.min, options.itemCount.max);
for (let index = 0; index < randomisedItemCount; index++)
if (items.length > 0)
{
if (!this.findAndAddRandomItemToLoot(items, itemTypeCounts, options, result))
const randomisedItemCount = this.randomUtil.getInt(options.itemCount.min, options.itemCount.max);
for (let index = 0; index < randomisedItemCount; index++)
{
index--;
if (!this.findAndAddRandomItemToLoot(items, itemTypeCounts, options, result))
{
index--;
}
}
}
@ -122,13 +125,21 @@ export class LootGenerator
this.itemHelper.isOfBaseclass(preset._encyclopedia, BaseClasses.WEAPON)
);
for (let index = 0; index < randomisedWeaponPresetCount; index++)
if (weaponDefaultPresets.length > 0)
{
if (
!this.findAndAddRandomPresetToLoot(weaponDefaultPresets, itemTypeCounts, itemBlacklistArray, result)
)
for (let index = 0; index < randomisedWeaponPresetCount; index++)
{
index--;
if (
!this.findAndAddRandomPresetToLoot(
weaponDefaultPresets,
itemTypeCounts,
itemBlacklistArray,
result,
)
)
{
index--;
}
}
}
}
@ -146,18 +157,22 @@ export class LootGenerator
const levelFilteredArmorPresets = armorDefaultPresets.filter((armor) =>
this.armorIsDesiredProtectionLevel(armor, options)
);
for (let index = 0; index < randomisedArmorPresetCount; index++)
if (levelFilteredArmorPresets.length > 0)
{
if (
!this.findAndAddRandomPresetToLoot(
levelFilteredArmorPresets,
itemTypeCounts,
itemBlacklistArray,
result,
)
)
for (let index = 0; index < randomisedArmorPresetCount; index++)
{
index--;
if (
!this.findAndAddRandomPresetToLoot(
levelFilteredArmorPresets,
itemTypeCounts,
itemBlacklistArray,
result,
)
)
{
index--;
}
}
}
}
@ -307,7 +322,7 @@ export class LootGenerator
const randomPreset = this.randomUtil.getArrayValue(globalDefaultPresets);
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;
}

View File

@ -192,7 +192,7 @@ export class PMCLootGenerator
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])
{
this.backpackLootPool[itemToAdd._id] = pmcPriceOverrides[itemToAdd._id];

View File

@ -688,19 +688,19 @@ export class BotGeneratorHelper
const itemDetails = this.itemHelper.getItem(itemTpl)[1];
// if item to add is found in exclude filter, not allowed
if (excludedFilter.includes(itemDetails._parent))
if (excludedFilter?.includes(itemDetails._parent))
{
return false;
}
// 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;
}
// 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;
}

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 { 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;
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 { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest";
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";
@injectable()
export class SptCommandoCommands implements ICommandoCommand
export class SptCommandoCommands implements IChatCommand
{
constructor(
@inject("ConfigServer") protected configServer: ConfigServer,
@ -31,7 +31,7 @@ export class SptCommandoCommands implements ICommandoCommand
{
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);
}

View File

@ -1,4 +1,5 @@
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 { PresetHelper } from "@spt-aki/helpers/PresetHelper";
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 { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
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 { HashUtil } from "@spt-aki/utils/HashUtil";
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
import { closestMatch, distance } from "closest-match";
import { inject, injectable } from "tsyringe";
@injectable()
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(
@inject("WinstonLogger") protected logger: ILogger,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@ -21,6 +38,8 @@ export class GiveSptCommand implements ISptCommand
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("PresetHelper") protected presetHelper: PresetHelper,
@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
{
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
{
const giveCommand = request.text.split(" ");
if (giveCommand[1] !== "give")
{
this.logger.error("Invalid action received for give command!");
return request.dialogId;
}
if (!giveCommand[2])
if (!GiveSptCommand.commandRegex.test(request.text))
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
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;
}
const tplId = giveCommand[2];
if (!giveCommand[3])
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
"Invalid use of give command! Quantity is missing. Use \"Help\" for more info",
);
return request.dialogId;
}
const quantity = giveCommand[3];
const result = GiveSptCommand.commandRegex.exec(request.text);
if (Number.isNaN(+quantity))
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)
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
"Invalid use of give command! Quantity is not a valid integer. Use \"Help\" for more info",
);
return request.dialogId;
if (this.savedCommand === undefined)
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
"Invalid use of give command. Use \"help\" for more information.",
);
return request.dialogId;
}
if (+result[6] > this.savedCommand.potentialItemNames.length)
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
"Invalid selection. Outside of bounds! Use \"help\" for more information.",
);
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);
if (!checkedItem[0])
@ -82,21 +187,25 @@ export class GiveSptCommand implements ISptCommand
this.mailSendService.sendUserMessageToPlayer(
sessionId,
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;
}
const itemsToSend: Item[] = [];
const preset = this.presetHelper.getDefaultPreset(checkedItem[1]._id);
if (preset)
if (this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.WEAPON))
{
for (let i = 0; i < +quantity; i++)
const preset = this.presetHelper.getDefaultPreset(checkedItem[1]._id);
if (!preset)
{
// Make sure IDs are unique before adding to array - prevent collisions
const presetToSend = this.itemHelper.replaceIDs(preset._items);
itemsToSend.push(...presetToSend);
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
"That weapon template ID could not be found. Please refine your request and try again.",
);
return request.dialogId;
}
itemsToSend.push(...this.jsonUtil.clone(preset._items));
}
else if (this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.AMMO_BOX))
{
@ -115,13 +224,25 @@ export class GiveSptCommand implements ISptCommand
_tpl: checkedItem[1]._id,
upd: { StackObjectsCount: +quantity, SpawnedInSession: true },
};
itemsToSend.push(...this.itemHelper.splitStack(item));
try
{
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
this.itemHelper.setFoundInRaid(itemsToSend);
this.mailSendService.sendSystemMessageToPlayer(sessionId, "Give command!", itemsToSend);
this.mailSendService.sendSystemMessageToPlayer(sessionId, "SPT GIVE", itemsToSend);
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 { ICommandoCommand } from "@spt-aki/helpers/Dialogue/Commando/ICommandoCommand";
import { IDialogueChatBot } from "@spt-aki/helpers/Dialogue/IDialogueChatBot";
import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest";
import { AbstractDialogueChatBot } from "@spt-aki/helpers/Dialogue/AbstractDialogueChatBot";
import { IChatCommand } from "@spt-aki/helpers/Dialogue/Commando/IChatCommand";
import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile";
import { MemberCategory } from "@spt-aki/models/enums/MemberCategory";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { MailSendService } from "@spt-aki/services/MailSendService";
@injectable()
export class CommandoDialogueChatBot implements IDialogueChatBot
export class CommandoDialogueChatBot extends AbstractDialogueChatBot
{
public constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("MailSendService") protected mailSendService: MailSendService,
@injectAll("CommandoCommand") protected commandoCommands: ICommandoCommand[],
@inject("WinstonLogger") logger: ILogger,
@inject("MailSendService") mailSendService: MailSendService,
@injectAll("CommandoCommand") chatCommands: IChatCommand[],
)
{
}
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);
super(logger, mailSendService, chatCommands);
}
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)
{
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.`,
);
return `I'm sorry soldier, I don't 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
* @returns Items craft time with bonuses subtracted
*/
protected getAdjustedCraftTimeWithSkills(
public getAdjustedCraftTimeWithSkills(
pmcData: IPmcData,
recipeId: string,
applyHideoutManagementBonus = false,
@ -613,13 +613,19 @@ export class HideoutHelper
return undefined;
}
// Seconds to deduct from crafts total time
let timeReductionSeconds = this.getSkillProductionTimeReduction(
pmcData,
recipe.productionTime,
SkillTypes.CRAFTING,
globalSkillsDb.Crafting.ProductionTimeReductionPerLevel,
);
let timeReductionSeconds = 0;
// Bitcoin farm is excluded from crafting skill cooldown reduction
if (recipeId !== HideoutHelper.bitcoinFarm)
{
// Seconds to deduct from crafts total time
timeReductionSeconds += this.getSkillProductionTimeReduction(
pmcData,
recipe.productionTime,
SkillTypes.CRAFTING,
globalSkillsDb.Crafting.ProductionTimeReductionPerLevel,
);
}
// Some crafts take into account hideout management, e.g. fuel, water/air filters
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
*/
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)
{
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;
}
// Armor
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
// Attempt to get the max durability from _props. If not available, use Repairable max durability value instead.
const maxDurability = (itemDetails._props.MaxDurability)
? itemDetails._props.MaxDurability
: repairable.MaxDurability;

View File

@ -85,6 +85,17 @@ export class PresetHelper
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
{
return templateId in this.lookup;

View File

@ -731,16 +731,20 @@ export class QuestHelper
repeatableType.activeQuests
).find((activeQuest) => activeQuest._id === failRequest.qid);
if (matchingRepeatableQuest || quest)
// Quest found and no repeatable found
if (quest && !matchingRepeatableQuest)
{
this.mailSendService.sendLocalisedNpcMessageToPlayer(
sessionID,
this.traderHelper.getTraderById(quest?.traderId ?? matchingRepeatableQuest?.traderId), // Can be null when repeatable quest has been moved to inactiveQuests
MessageType.QUEST_FAIL,
quest.failMessageText,
questRewards,
this.timeUtil.getHoursAsSeconds(this.questConfig.redeemTime),
);
if (quest.failMessageText.trim().length > 0)
{
this.mailSendService.sendLocalisedNpcMessageToPlayer(
sessionID,
this.traderHelper.getTraderById(quest?.traderId ?? matchingRepeatableQuest?.traderId), // Can be null when repeatable quest has been moved to inactiveQuests
MessageType.QUEST_FAIL,
quest.failMessageText,
questRewards,
this.timeUtil.getHoursAsSeconds(this.questConfig.redeemTime),
);
}
}
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
itemPurchased.upd.StackObjectsCount -= buyCount;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,4 +9,8 @@ export interface IHideoutConfig extends IBaseConfig
runIntervalValues: IRunIntervalValues;
hoursForSkillCrafting: 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
{
webSocketPingDelayMs: number;
kind: "aki-http";
/** Address used by webserver */
ip: string;
port: number;
/** Address used by game client to connect to */
backendIp: string;
backendPort: string;
webSocketPingDelayMs: number;
logRequests: boolean;
/** e.g. "Aki_Data/Server/images/traders/579dc571d53a0658a154fbec.png": "Aki_Data/Server/images/traders/NewTraderImage.png" */
serverImagePathOverride: Record<string, string>;

View File

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

View File

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

View File

@ -46,6 +46,8 @@ export interface FenceConfig
presetSlotsToRemoveChancePercent: Record<string, number>;
/** Block seasonal items from appearing when season is inactive */
blacklistSeasonalItems: boolean;
/** Max pen value allowed to be listed on flea - affects ammo + ammo boxes */
ammoMaxPenLimit: number;
blacklist: string[];
coopExtractGift: CoopExtractReward;
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);
},
),
new RouteAction(
"/singleplayer/settings/bot/difficulties/",
(url: string, info: any, sessionID: string, output: string): any =>
{
return this.botCallbacks.getAllBotDifficulties(url, info, sessionID);
},
),
new RouteAction(
"/singleplayer/settings/bot/maxCap",
(url: string, info: any, sessionID: string, output: string): any =>

View File

@ -20,13 +20,6 @@ export class InraidStaticRouter extends StaticRouter
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(
"/singleplayer/settings/raid/menu",
(url: string, info: any, sessionID: string, output: string): any =>

View File

@ -45,9 +45,6 @@ export class HttpServer
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 */
httpServer.listen(this.httpConfig.port, this.httpConfig.ip, () =>
{
@ -82,17 +79,20 @@ export class HttpServer
if (this.httpConfig.logRequests)
{
// TODO: Extend to include 192.168 / 10.10 ranges or check subnet
const isLocalRequest = req.socket.remoteAddress.startsWith("127.0.0");
if (isLocalRequest)
const isLocalRequest = req.socket.remoteAddress?.startsWith("127.0.0");
if (typeof isLocalRequest !== "undefined")
{
this.logger.info(this.localisationService.getText("client_request", req.url));
}
else
{
this.logger.info(this.localisationService.getText("client_request_ip", {
ip: req.socket.remoteAddress,
url: req.url.replaceAll("/", "\\"), // Localisation service escapes `/` into hex code `&#x2f;`
}));
if (isLocalRequest)
{
this.logger.info(this.localisationService.getText("client_request", req.url));
}
else
{
this.logger.info(this.localisationService.getText("client_request_ip", {
ip: req.socket.remoteAddress,
url: req.url.replaceAll("/", "\\"), // Localisation service escapes `/` into hex code `&#x2f;`
}));
}
}
}

View File

@ -89,6 +89,12 @@ export class BotLootCacheService
case LootCacheType.DRUG_ITEMS:
result = this.lootCache[botRole].drugItems;
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:
result = this.lootCache[botRole].stimItems;
break;
@ -219,7 +225,7 @@ export class BotLootCacheService
? 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)
{
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
const stimItems: Record<string, number> =
(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 = {};
for (const itemKey of Object.keys(backpackLootPool))
{
@ -285,6 +329,8 @@ export class BotLootCacheService
|| this.isMagazine(itemTemplate._props)
|| this.isMedicalItem(itemTemplate._props)
|| this.isGrenade(itemTemplate._props)
|| this.isFood(itemTemplate._id)
|| this.isDrink(itemTemplate._id)
)
{
// Is type we dont want as backpack loot, skip
@ -294,7 +340,7 @@ export class BotLootCacheService
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 = {};
for (const itemKey of Object.keys(pocketLootPool))
{
@ -309,6 +355,8 @@ export class BotLootCacheService
|| this.isMagazine(itemTemplate._props)
|| this.isMedicalItem(itemTemplate._props)
|| this.isGrenade(itemTemplate._props)
|| this.isFood(itemTemplate._id)
|| this.isDrink(itemTemplate._id)
|| !("Height" in itemTemplate._props) // lacks height
|| !("Width" in itemTemplate._props) // lacks width
)
@ -319,7 +367,7 @@ export class BotLootCacheService
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 = {};
for (const itemKey of Object.keys(vestLootPool))
{
@ -334,6 +382,8 @@ export class BotLootCacheService
|| this.isMagazine(itemTemplate._props)
|| this.isMedicalItem(itemTemplate._props)
|| this.isGrenade(itemTemplate._props)
|| this.isFood(itemTemplate._id)
|| this.isDrink(itemTemplate._id)
)
{
continue;
@ -344,6 +394,8 @@ export class BotLootCacheService
this.lootCache[botRole].healingItems = healingItems;
this.lootCache[botRole].drugItems = drugItems;
this.lootCache[botRole].foodItems = foodItems;
this.lootCache[botRole].drinkItems = drinkItems;
this.lootCache[botRole].stimItems = stimItems;
this.lootCache[botRole].grenadeItems = grenadeItems;
@ -429,6 +481,16 @@ export class BotLootCacheService
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
* @param botRole role to check for
@ -455,6 +517,8 @@ export class BotLootCacheService
specialItems: {},
grenadeItems: {},
drugItems: {},
foodItems: {},
drinkItems: {},
healingItems: {},
stimItems: {},
};

View File

@ -3,16 +3,16 @@ import { inject, injectable } from "tsyringe";
import { HandbookHelper } from "@spt-aki/helpers/HandbookHelper";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
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 { 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 { IBarterScheme, ITraderAssort } from "@spt-aki/models/eft/common/tables/ITrader";
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { Traders } from "@spt-aki/models/enums/Traders";
import { IItemDurabilityCurrentMax, ITraderConfig } from "@spt-aki/models/spt/config/ITraderConfig";
import { ICreateFenceAssortsResult } from "@spt-aki/models/spt/fence/ICreateFenceAssortsResult";
import {
IFenceAssortGenerationValues,
IGenerationAssortValues,
@ -43,7 +43,7 @@ export class FenceService
/** Assorts shown on a separate tab when you max out fence rep */
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;
constructor(
@ -231,16 +231,23 @@ export class FenceService
this.deleteRandomAssorts(itemCountToReplace, this.fenceAssort);
this.deleteRandomAssorts(discountItemCountToReplace, this.fenceDiscountAssort);
// Get count of what item pools need new items (item/weapon/equipment)
const itemCountsToReplace = this.getCountOfItemsToGenerate();
const normalItemCountsToGenerate = this.getItemCountsToGenerate(
this.fenceAssort.items,
this.desiredAssortCounts.normal,
);
const newItems = this.createAssorts(normalItemCountsToGenerate, 1);
const newItems = this.createFenceAssortSkeleton();
this.createAssorts(itemCountsToReplace.normal, newItems, 1);
this.fenceAssort.items.push(...newItems.items);
// Push newly generated assorts into existing data
this.updateFenceAssorts(newItems, this.fenceAssort);
const newDiscountItems = this.createFenceAssortSkeleton();
this.createAssorts(itemCountsToReplace.discount, newDiscountItems, 2);
this.fenceDiscountAssort.items.push(...newDiscountItems.items);
const discountItemCountsToGenerate = this.getItemCountsToGenerate(
this.fenceDiscountAssort.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
for (const barterItemKey in newItems.barter_scheme)
@ -271,6 +278,46 @@ export class FenceService
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
*/
@ -281,18 +328,18 @@ export class FenceService
}
/**
* Compare the current fence offer count to what the config wants it to be,
* If value is lower add extra count to value to generate more items to fill gap
* @param existingItemCountToReplace count of items to generate
* @returns number of items to generate
* Get values that will hydrate the passed in assorts back to the desired counts
* @param assortItems Current assorts after items have been removed
* @param generationValues Base counts assorts should be adjusted to
* @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 rootPresetItems = this.fenceAssort.items.filter((item) =>
item.slotId === "hideout" && item.upd.sptPresetId
);
const allRootItems = assortItems.filter((item) => item.slotId === "hideout");
const rootPresetItems = allRootItems.filter((item) => item.upd.sptPresetId);
// Get count of weapons
const currentWeaponPresetCount = rootPresetItems.reduce((count, item) =>
@ -306,60 +353,19 @@ export class FenceService
return this.itemHelper.armorItemCanHoldMods(item._tpl) ? count + 1 : count;
}, 0);
const itemCountToGenerate = Math.max(this.desiredAssortCounts.normal.item - currentItemAssortCount, 0);
const weaponCountToGenerate = Math.max(
this.desiredAssortCounts.normal.weaponPreset - currentWeaponPresetCount,
0,
);
const equipmentCountToGenerate = Math.max(
this.desiredAssortCounts.normal.equipmentPreset - currentEquipmentPresetCount,
0,
);
// Normal item count is total count minus weapon + armor count
const nonPresetItemAssortCount = allRootItems.length - (currentWeaponPresetCount + currentEquipmentPresetCount);
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,
weaponPreset: weaponCountToGenerate,
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
{
const rootItemToRemove = this.randomUtil.getArrayValue(rootItems);
// Clean up any mods if item had them
const itemWithChildren = this.itemHelper.findAndReturnChildrenAsItems(assort.items, rootItemToRemove._id);
for (const itemToDelete of itemWithChildren)
{
// Delete item from assort items array
assort.items.splice(assort.items.indexOf(itemToDelete), 1);
const rootItemToAdjust = this.randomUtil.getArrayValue(rootItems);
const itemCountToRemove = this.randomUtil.getInt(1, rootItemToAdjust.upd.StackObjectsCount);
if (itemCountToRemove > 1 && itemCountToRemove < rootItemToAdjust.upd.StackObjectsCount)
{ // 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)
{
// Delete item from assort items array
assort.items.splice(assort.items.indexOf(itemToDelete), 1);
}
delete assort.barter_scheme[rootItemToRemove._id];
delete assort.loyal_level_items[rootItemToRemove._id];
delete assort.barter_scheme[rootItemToAdjust._id];
delete assort.loyal_level_items[rootItemToAdjust._id];
}
}
/**
@ -437,16 +451,35 @@ export class FenceService
this.createInitialFenceAssortGenerationValues();
// Create basic fence assort
const assorts = this.createFenceAssortSkeleton();
this.createAssorts(this.desiredAssortCounts.normal, assorts, 1);
const assorts = this.createAssorts(this.desiredAssortCounts.normal, 1);
// Store in this.fenceAssort
this.setFenceAssort(assorts);
this.setFenceAssort(this.convertIntoFenceAssort(assorts));
// Create level 2 assorts accessible at rep level 6
const discountAssorts = this.createFenceAssortSkeleton();
this.createAssorts(this.desiredAssortCounts.discount, discountAssorts, 2);
const discountAssorts = this.createAssorts(this.desiredAssortCounts.discount, 2);
// 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 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 itemTypeLimitCounts = this.initItemLimitCounter(this.traderConfig.fence.itemTypeLimits);
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)
@ -522,11 +563,13 @@ export class FenceService
this.addPresetsToAssort(
itemCounts.weaponPreset,
itemCounts.equipmentPreset,
assorts,
result,
baseFenceAssortClone,
loyaltyLevel,
);
}
return result;
}
/**
@ -539,15 +582,15 @@ export class FenceService
*/
protected addItemAssorts(
assortCount: number,
assorts: ITraderAssort,
assorts: ICreateFenceAssortsResult,
baseFenceAssortClone: ITraderAssort,
itemTypeLimits: Record<string, { current: number; max: number; }>,
loyaltyLevel: number,
): void
{
const priceLimits = this.traderConfig.fence.itemCategoryRoublePriceLimit;
const assortRootItems = baseFenceAssortClone.items.filter((x) =>
x.parentId === "hideout" && !x.upd?.sptPresetId
const assortRootItems = baseFenceAssortClone.items.filter((item) =>
item.parentId === "hideout" && !item.upd?.sptPresetId
);
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
const existingItemThatMatches = this.getMatchingItem(rootItemBeingAdded, itemDbDetails, assorts.items);
const existingItemThatMatches = this.getMatchingItem(rootItemBeingAdded, itemDbDetails, assorts.sptItems);
const shouldBeStacked = this.itemShouldBeForceStacked(existingItemThatMatches, itemDbDetails);
if (shouldBeStacked && existingItemThatMatches)
{ // Decrement loop counter so another items gets added
@ -630,7 +673,7 @@ export class FenceService
this.randomiseArmorModDurability(desiredAssortItemAndChildrenClone, itemDbDetails);
}
assorts.items.push(...desiredAssortItemAndChildrenClone);
assorts.sptItems.push(desiredAssortItemAndChildrenClone);
assorts.barter_scheme[rootItemBeingAdded._id] = this.jsonUtil.clone(
baseFenceAssortClone.barter_scheme[chosenBaseAssortRoot._id],
@ -651,15 +694,15 @@ export class FenceService
* e.g. salewa hp resource units left
* @param rootItemBeingAdded item to look for a match against
* @param itemDbDetails Db details of matching item
* @param fenceItemAssorts Items to search through
* @param itemsWithChildren Items to search through
* @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
const matchingItems = fenceItemAssorts.filter((item) =>
item._tpl === rootItemBeingAdded._tpl && item.parentId === "hideout"
);
const matchingItems = itemsWithChildren.filter((itemWithChildren) =>
itemWithChildren.find((item) => item._tpl === rootItemBeingAdded._tpl && item.parentId === "hideout")
).flatMap((x) => x);
if (matchingItems.length === 0)
{
// Nothing matches by tpl and is root item, exit early
@ -726,11 +769,13 @@ export class FenceService
return false;
}
return this.itemInPreventDupeCategoryList(itemDbDetails._id);
}
protected itemInPreventDupeCategoryList(tpl: string): boolean
{
// Item type in config list
return this.itemHelper.isOfBaseclasses(
itemDbDetails._id,
this.traderConfig.fence.preventDuplicateOffersOfCategory,
);
return this.itemHelper.isOfBaseclasses(tpl, this.traderConfig.fence.preventDuplicateOffersOfCategory);
}
/**
@ -799,7 +844,7 @@ export class FenceService
protected addPresetsToAssort(
desiredWeaponPresetsCount: number,
desiredEquipmentPresetsCount: number,
assorts: ITraderAssort,
assorts: ICreateFenceAssortsResult,
baseFenceAssort: ITraderAssort,
loyaltyLevel: number,
): void
@ -848,7 +893,7 @@ export class FenceService
// Remapping IDs causes parentid to be altered
presetWithChildrenClone[0].parentId = "hideout";
assorts.items.push(...presetWithChildrenClone);
assorts.sptItems.push(presetWithChildrenClone);
// Set assort price
// 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
presetWithChildrenClone[0].parentId = "hideout";
assorts.items.push(...presetWithChildrenClone);
assorts.sptItems.push(presetWithChildrenClone);
// Set assort price
// 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
{
protected itemBaseClassesCache: Record<string, string[]> = {};
protected items: Record<string, ITemplateItem>;
protected cacheGenerated = false;
constructor(
@ -31,15 +32,15 @@ export class ItemBaseClassService
// Clear existing cache
this.itemBaseClassesCache = {};
const allDbItems = this.databaseServer.getTables().templates.items;
if (!allDbItems)
this.items = this.databaseServer.getTables().templates.items;
if (!this.items)
{
this.logger.warning(this.localisationService.getText("baseclass-missing_db_no_cache"));
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)
{
const itemIdToUpdate = item._id;
@ -48,7 +49,7 @@ export class ItemBaseClassService
this.itemBaseClassesCache[item._id] = [];
}
this.addBaseItems(itemIdToUpdate, item, allDbItems);
this.addBaseItems(itemIdToUpdate, item);
}
this.cacheGenerated = true;
@ -58,16 +59,15 @@ export class ItemBaseClassService
* 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 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);
const parent = allDbItems[item._parent];
const parent = this.items[item._parent];
if (parent._parent !== "")
{
this.addBaseItems(itemIdToUpdate, parent, allDbItems);
this.addBaseItems(itemIdToUpdate, parent);
}
}
@ -91,8 +91,9 @@ export class ItemBaseClassService
return false;
}
// Edge case - this is the 'root' item that all other items inherit from
if (itemTpl === BaseClasses.ITEM)
// The cache is only generated for item templates with `_type === "Item"`, so return false for any other type,
// including item templates that simply don't exist.
if (!this.cachedItemIsOfItemType(itemTpl))
{
return false;
}
@ -114,6 +115,16 @@ export class ItemBaseClassService
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
* @param itemTpl item to get base classes for

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