Merge branch '3.8.1-DEV' of https://dev.sp-tarkov.com/SPT-AKI/Server into 3.9.0-DEV

This commit is contained in:
Dev 2024-04-27 22:58:05 +01:00
commit 9dff3e0790
22 changed files with 864 additions and 390 deletions

View File

@ -9,50 +9,65 @@ on:
jobs: jobs:
vitest: vitest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > # Conditional to limit runs: checks if it's NOT a push to a branch with an open PR
github.event_name == 'push' ||
github.event.pull_request.head.repo.full_name != github.repository
container: container:
image: refringe/spt-build-node:1.0.7 image: refringe/spt-build-node:1.0.7
steps: steps:
- name: Clone - name: Clone
run: | run: |
rm -rf /workspace/SPT-AKI/Build/server # For pull request events, checkout using GITHUB_SHA
git clone https://dev.sp-tarkov.com/${GITHUB_REPOSITORY}.git --branch master /workspace/SPT-AKI/Build/server # For push events, checkout using GITHUB_REF_NAME
if [[ $GITHUB_EVENT_NAME == "pull_request" ]]; then
REF=${GITHUB_SHA}
else
REF=${GITHUB_REF_NAME}
fi
cd /workspace/SPT-AKI/Build/server rm -rf /workspace/SPT-AKI/Server/current
git checkout ${GITHUB_SHA} git clone https://dev.sp-tarkov.com/${{ github.repository }}.git /workspace/SPT-AKI/Server/current
shell: bash
- name: Pull LFS Files cd /workspace/SPT-AKI/Server/current
run: | git fetch
cd /workspace/SPT-AKI/Build/server git checkout $REF
git lfs pull env:
git lfs ls-files GITHUB_SHA: ${{ github.sha }}
shell: bash GITHUB_REF_NAME: ${{ github.ref_name }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
shell: bash
- name: Cache NPM Dependencies - name: Pull LFS Files
id: cache-npm-dependencies run: |
uses: actions/cache@v4 cd /workspace/SPT-AKI/Server/current && ls -lah
with: git lfs pull && git lfs ls-files
path: /workspace/SPT-AKI/Build/server/project/node_modules shell: bash
key: npm-dependencies-${{ hashFiles('/workspace/SPT-AKI/Build/server/project/package.json') }}
- name: Install NPM Dependencies - name: Cache NPM Dependencies
if: steps.cache-npm-dependencies.outputs.cache-hit != 'true' id: cache-npm-dependencies
run: | uses: actions/cache@v4
cd /workspace/SPT-AKI/Build/server/project with:
rm -rf node_modules path: /workspace/SPT-AKI/Server/current/project/node_modules
npm install key: npm-dependencies-${{ hashFiles('/workspace/SPT-AKI/Server/current/project/package.json') }}
shell: bash
- name: Run Tests - name: Install NPM Dependencies
id: run-tests if: steps.cache-npm-dependencies.outputs.cache-hit != 'true'
run: | run: |
cd /workspace/SPT-AKI/Build/server/project cd /workspace/SPT-AKI/Server/current/project
npm run test rm -rf /workspace/SPT-AKI/Server/current/project/node_modules
shell: bash npm install
shell: bash
- name: Fix Instructions - name: Run Tests
if: failure() && steps.run-tests.outcome == 'failure' id: run-tests
run: | 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" cd /workspace/SPT-AKI/Server/current/project
echo -e "A test written today is a bug prevented tomorrow.™" npm run test
shell: bash 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 committed 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

319
FEATURES.md Normal file
View File

@ -0,0 +1,319 @@
# Features
## Table of Contents
- [Profiles](#Profiles)
- [Progression](#progression)
- [Starting Profile Types](#starting-profile-types)
- [Bots](#bots)
- [AI Types](#ai-types)
- [Generation](#generation)
- [Inventory](#inventory)
- [Traders](#traders)
- [Flea market](#flea-market)
- [Quests](#quests)
- [Hideout](#hideout)
- [Weapon Building](#weapon-building)
- [Raids](#raids)
- [Messages](#messages)
- [Events](#events)
- [Modding](#modding)
## Profiles
### Progression
The player profile is stored as a JSON file, allowing for changes to persist across server restarts. The profile contains the following information for both your PMC and Scav player characters:
- Task Conditions
- Account Bonuses
- Model Selection
- Health
- Energy, Hydration, & Temperature
- Hideout Build & Production Status
- Items (Inventory, Insured, Quest, Wishlist)
- Inventory
- Quest Progress
- Flea Market Rating & Current Offers
- Common and Mastering Skills
- Various Raid Stats
- Trader Status and Loyalty Levels
- Extract Counts
- Achievements
### Starting Profile Types
The following profile types are available to start with when creating an account in the Launcher:
- Standard Profiles:
- Standard
- Left Behind
- Prepare To Escape
- Edge Of Darkness
- Custom profiles
- SPT Easy Start
- Lots of money, quality of life skills to level 20, and player to level 69.
- SPT Zero to Hero
- No money, skills, trader reputation, or items. Start with a knife.
- SPT Developer
- Developer testing profile, player to level 69, max skills, and max trader reputation.
- USEC will have all quests ready to start.
- BEAR will have all quests ready to hand in.
## Bots
### AI Types
Bot data is emulated to mimic live bots as closely as possible. This includes the following bot types:
- Scavs
- Regular Scav (*assault*)
- Sniper Scav (*marksman*)
- Tagged & Cursed (*cursedAssault*)
- Bosses
- Reshalla (*bossBully*)
- Guard (*followerBully*)
- Glukhar (*bossGluhar*)
- Assault Guard (*followerGluharAssault*)
- Scout Guard (*followerGluharScout*)
- Security Guard (*followerGluharSecurity*)
- Sniper Guard (*followerGluharSnipe*)
- Killa (*bossKilla*)
- Shturman (*bossKojainy*)
- Guard (*followerKojaniy*)
- Sanitar (*bossSanitar*)
- Guard (*followerSanitar*)
- Tagilla (*bossTagilla*)
- Knight (*bossKnight*)
- Big Pipe (*followerBigPipe*)
- Bird Eye (*followerBirdEye*)
- Zryachiy (*bossZryachiy*)
- Guard (*followerzryachiy*)
- Kaban (*bossBoar*)
- Sniper Guard (*bossBoarSniper*)
- Guard (*followerBoar*)
- Cultists
- Priest (*sectantPriest*)
- Warrior (*sectantWarrior*)
- Raiders (*pmcBot*)
- Rogues (*exUsec*)
- Santa (*gifter*) - *partially implemented*
*PMCs are generated with a random type from a sub-set of the above list.*
*Some bot types are only available on some maps.*
### Generation
Bots are generated with the following characteristics:
- All Bots:
- Weapons - *Weighted, semi-randomly selected*
- Ammunition - *Weighted, semi-randomly selected*
- Gear - *Weighted, semi-randomly selected*
- Headgear Attachments - *Weighted, semi-randomly selected*
- PMC Bots
- AI Type - *Randomly chosen from sub-set of possible bot types*
- Dogtags - *Random level & name*
- Chance of name being the name of a contributor to the project
- Voices - *Randomly chosen Bear/USEC voices for each faction*
Other bot generation systems/features include:
- Loot item blacklist & whitelist
- Loot items can be configured to be limited to a certain number based on bot type
- Level-relative gear for PMCs from levels 1-15 and 15+
- Level 1-15 bots have lower-tier items
- Level 15+ bots have access to almost anything
- Randomised gear and weapon durability based on bot type and level
## Inventory
The inventory system includes the following features:
- Move, Split, and Delete Item Stacks
- Add, Modify, and Remove Item Tags
- Armor and Weapon Repair Kits
- Auto-sort Inventory
- Out-of-raid Healing, Eating, & Drinking
- Special Player Slots
## Traders
The trader system includes the following features:
- Buy and sell items from each trader
- Listed items are refreshed on a timer based on the trader
- Purchase limits per refresh period
- Tracks currency spent through each trader
- Loyalty levels
- Reputation
- Item repair from Prapor, Skier, and Mechanic
- Unlock and purchase clothing from Ragman
- Insurance from Therapist and Prapor
- Chance for items to be returned, higher chance for more expensive trader
- Chance parts will be stripped from returned weapons based on value
- Post-raid Therapist Healing
- Fence Item Assortment
- Lists random items for sale
- Emulated system of 'churn' for items sold by Fence
## Flea market
The flea market system has been build to simulate the live flea market as closely as possible. It includes the following features:
- Simulated Player Offers
- Generated with random names, ratings, and expiry times
- Variable offer prices based on live item prices (~20% above and below)
- Weapon presets as offers
- Barter offers
- Listed in multiple currencies (Rouble, Euro, and Dollar)
- Dynamically adjust flea prices that drift below trader price
- Buy Items
- Sell Items
- Generates listing fee
- Increase flea rating by selling items
- Decrease flea rating by failing to sell items
- Items purchased by simulated players
- Offer price effects chance that item will be purchased
- Filtering
- By specific item
- By link to item
- Text search by name
- By currency
- By price range
- By condition range
- By Traders, Players, or Both
- To include barter offers (or not)
- Sorting by
- Rating
- Name
- Price
- Expiry
## Quests
The quest system includes the following features:
- Accurate Quest List - *roughly 90% implemented*
- Trader Quests - *Accept, Turn-in Items, and Complete*
- Daily Quests - *Accept, Replace, Turn-in Items, Complete*
- Simulates Daily and Weekly Quests
- Quest Replacement Fee
- Scav Quests
- Trader items unlock through completion of quests
- Receive messages from traders after interacting with a quest
- Item rewards passed through messages
## Hideout
The hideout has the following features implemented:
- Areas
- Air Filter
- Filter Degradation
- Boosts Skill Levelling
- Bitcoin Farm
- Generation Speed Dependent on Number of Graphics Cards
- Booze Generator
- Crafts Moonshine
- Generator
- Fuel Degradation
- Heating
- Energy Regeneration
- Negative Effects Removal
- Illumination
- Intel Centre
- ~~Unlocks Fence's Scav Quests~~ *not implemented - workaround: unlocks at level 5*
- ~~Reduces Insurance Return Time~~ *not implemented*
- Quest Currency Reward Boost
- Lavatory
- Library
- Medstation
- Nutrition Unit
- Rest Space
- Scav Case
- Custom Reward System
- Security
- Shooting Range
- Solar Power
- Stash
- Upgrades grant larger stash sizes
- Vents
- Water Collector
- Workbench
- Unlocks the ability to repair items
- Christmas Tree
- Item Crafting
- Items are marked found-in-raid on completion
- Continues to track crafting progress even when server is not running
## Weapon Building
The weapon building system has been fully implemented:
- Create Weapon Presets
- Saving Presets
- Load Presets
## Raids
The in-raid systems included are as follows:
- Maps
- Customs
- Factory Day
- Factory Night
- Ground Zero
- Interchange
- Laboratory
- Lighthouse
- Reserve
- Shoreline
- Streets
- Woods
- Loot
- Loot spawning has been generated using over 100,000 EFT offline loot runs.
- Static Loot (in containers)
- Each container type can contain items appropriate to that container type found in offline EFT.
- Loose Loot (on map)
- Randomised loose items found on map in offline EFT.
- Airdrops
- Randomised Spawn Chance
- Request with Red Flare
- Crate Types:
- Weapons & Armour
- Food & Medical
- Barter Goods
- Mixed - *mixture of any of the above items*
- Supported Maps:
- Customs
- Interchange
- Lighthouse
- Reserve
- Shoreline
- Streets
- Woods
- Persisted Raid Damage - *extracting with injury will persist injury out of raid*
- Scav Raids - *raid time and items are reduced to simulate entering a raid late*
## Messages
A messaging system has been implemented to allow for the following functionality:
- Receive messages (with item attachments) from traders or "system"
- Pin/unpin senders within the message list
- Receive all (or individual) attachments
- Send messages to "Commando" friend to execute server commands
## Events
The following events have been implemented and have a set time period for when they will be active:
- Snow
- Halloween
- Christmas
## Modding
- The Server project has been built to allow for extensive modifications to nearly any aspect and system used.
- [Example mods](https://dev.sp-tarkov.com/chomp/ModExamples) are provided that cover the most common modding methods.

412
README.md
View File

@ -1,352 +1,122 @@
# Server # Single Player Tarkov - Server Project
Modding framework for Escape From Tarkov This is the Server project for the Single Player Tarkov mod for Escape From Tarkov. It can be run locally to replicate responses to the modified Escape From Tarkov client.
[![Build Status](https://drone.sp-tarkov.com/api/badges/SPT-AKI/Server/status.svg?ref=refs/heads/development)](https://drone.sp-tarkov.com/SPT-AKI/Server) # Table of Contents
[![Quality Gate Status](https://sonar.sp-tarkov.com/api/project_badges/measure?project=AKI&metric=alert_status&token=d3b87ff5fac591c1f49a57d4a2883c92bfe6a77f)](https://sonar.sp-tarkov.com/dashboard?id=AKI)
## Privacy - [Features](#features)
SPT is an open source project. Your commit credentials as author of a commit will be visible by anyone. Please make sure you understand this before submitting a PR. - [Installation](#installation)
Feel free to use a "fake" username and email on your commits by using the following commands: - [Requirements](#requirements)
```bash - [Initial Setup](#initial-setup)
git config --local user.name "USERNAME" - [Development](#development)
git config --local user.email "USERNAME@SOMETHING.com" - [Commands](#commands)
``` - [Debugging](#debugging)
- [Mod Debugging](#mod-debugging)
- [Contributing](#contributing)
- [Branches](#branchs)
- [Pull Request Guidelines](#pull-request-guidelines)
- [Tests](#tests)
- [License](#license)
## Requirements ## Features
- NodeJS (with npm) For a full list of features, please see [FEATURES.md](FEATURES.md).
- Visual Studio Code
- git [LFS](https://git-lfs.github.com/)
## Observations ## Installation
- The server was tested to work with **NodeJS v20.11.1**, if you are using a different version and experiencing difficulties change it before looking for support ### Requirements
- If you are updating a branch you've had for some time, run `npm ci` before running any tasks. This will run the clean and install target from npm.
- You can debug your mods using the server, just copy your mod files into the `user/mods` folder and put breakpoints on the **JS** files. **DO NOT** contact the dev team for support on this.
## Pulling This project has been built in [Visual Studio Code](https://code.visualstudio.com/) (VSC) using [Node.js](https://nodejs.org/). We recommend using [NVM](https://github.com/coreybutler/nvm-windows) to manage installation and switching Node versions. If you do not wish to use NVM, you will need to install the version of Node.js listed within the `.nvmrc` file manually.
- Run `git lfs fetch` and `git lfs pull` to acquire loot files
## Setup There are a number of VSC extensions that we recommended for this project. VSC will prompt you to install these when you open the workspace file. If you do not see the prompt, you can install them manually:
1. Visual Studio Code > File > Open Workspace... > `project\Server.code-workspace` - [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) - Editor Settings Synchronization
2. Visual Studio Code > Terminal > Run Task... > npm > npm: Install - [Dprint Code Formatter](https://marketplace.visualstudio.com/items?itemName=dprint.dprint) - Formatting on Save
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - Linting for Coding Issues & Naming Conventions
- [Biome](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) - Linting for Coding Standards
- [Vitest](https://marketplace.visualstudio.com/items?itemName=vitest.explorer) - Debugging Tests
- [SPT ID Highlighter](https://marketplace.visualstudio.com/items?itemName=refringe.spt-id-highlighter) - Converts IDs to Names
## Build
This is for preparing for a release, not to run locally. ### Initial Setup
**Mode** | **Location** To prepare the project for development you will need to:
-------- | -----------------------------------------------------------------
release | Visual Studio Code > Terminal > Run Build Task... > build:release
debug | Visual Studio Code > Terminal > Run Build Task... > build:debug
## Test / Run locally 1. Run `git clone https://dev.sp-tarkov.com/SPT-AKI/Server.git server` to clone the repository.
2. Run `git lfs pull` to download LFS files locally.
2. Open the `project/mod.code-workspace` file in Visual Studio Code (VSC).
3. Run `nvm use 20.11.1` in the VSC terminal.
4. Run `npm install` in the VSC terminal.
Visual Studio Code > Run > Start Debugging ## Development
# Features ### Commands
## Progression The following commands are available after the initial setup. Run them with `npm run <command>`.
Player profile is stored in SPT folder as a JSON file, allowing for changes to persist
- Scav:
- Stats increase by doing scav raids
- Skills increase by doing scav raids
- Scav reputation system (Karma)
- Scavs hostile below certain level
- Scav run cooldown adjustment
- Scav follow chance adjustment
- Scav case
- ~~Completion time adjustment~~ NOT IMPLEMENTED
- ~~Equipment chance adjustment~~ NOT IMPLEMENTED
- Bosses hostile below certain level
- ~~Exfil price adjustment~~ NOT IMPLEMENTED
- Improved gear with higher rep
- Increase rep by exiting through car extracts
- PMC:
- Stats increase by doing PMC raids
- Skills increase by doing PMC raids
- Hydration/food
- Increase out of raid
- Post-raid levels are persisted to profile
- Raid stat tracking
- Raid count
- Survived count
- KIA count
- MIA count
- AWOL count
- Kills count
## Bots | Command | Description |
|--------------------------|----------------------------------------------------------------------|
| `check:circular` | Check for circular dependencies in the project. |
| `lint` | Lint the project for coding standards. |
| `lint:fix` | Attempt to automatically fix coding standard issues. |
| `style` | Check the project for style/formatting issues. |
| `style:fix` | Attempt to automatically fix style/formatting issues. |
| `test` | Run all tests. |
| `test:watch` | Run tests in watch mode. Tests will re-run when files are changed. |
| `test:coverage` | Run tests and generate a coverage report. |
| `test:ui` | Run tests in UI mode. This will open a browser window to view tests. |
| `build:release` | Build the project for release. |
| `build:debug` | Build the project for debugging. |
| `build:bleeding` | Build the project on the bleeding edge. |
| `build:bleedingmods` | Build the project on the bleeding edge with mods. |
| `run:build` | Run the project in build mode. |
| `run:debug` | Run the project in debug mode. |
| `run:profiler` | Run the project in profiler mode. |
| `gen:types` | Generate types for the project. |
| `gen:docs` | Generate documentation for the project. |
- Emulated bots: ### Debugging
- assault (scav)
- bossBully (Reshalla)
- bossGluhar
- bossKilla
- bossKnight
- bossKojainy (Shturman)
- bossSanitar
- bossTagilla
- bossZryachiy
- bossBoar (Kaban)
- bossBoarSniper
- curedAssault
- exUsec (Rogue)
- followerBigPipe
- Grenade launcher
- followerBirdEye
- followerBoar
- followerBully
- followerGluharAssault
- followerGluharScout
- followerGluharSecurity
- followerGluharSnipe
- followerKojaniy
- followerSanitar
- followerzryachiy
- gifter (Santa)
- Gives gifts (partially implemented)
- marksman
- pmcBot (Raider)
- sectantPriest (Cultist)
- sectantWarrior (Cultist)
- Gear
- Semi-randomised gear chosen with weighting system
- Randomised durability of gear
- Ammo
- Ammo weighting system to mimic live
- Loot
- Semi-randomised loot
- Item type spawn limit system
- Per-map AI types
## PMCs To debug the project in Visual Studio Code, you can select the `Run` tab and then select the `Start Debugging` option (or the `F5` shortcut). This will start the server in debug mode, attaching a debugger to code execution, allowing you to set breakpoints and step through the code as it runs.
- Simulated PMC players
- Custom weapons
- Semi-randomly generated with weighting system
- Semi-randomly chosen ammo with weighting system
- Custom gear
- Semi-randomly generated with weighting system
- Custom headgear
- Randomised attachments with percentage based chance to appear
- Face shields
- Flashlights
- Randomised AI brains
- Chooses random AI behaviour from pool of possible bot types (e.g. raider/rogue/killa)
- Dogtags
- Random level
- Random name
- Voices
- Bear/usec voices for each faction
- Loot item blacklist/whitelist
- Highly configurable in config
- Level-relative gear for PMCs from levels 1-15 and 15+
- 1-15 bots have lower-tier items
- 15+ bots have access to anything
## Inventory ### Mod Debugging
- Move/split/delete stacks
- Tags (add/modify/remove)
- Armor/weapon kit item repair
- Auto-sort
- Out of raid healing
- Out of raid eating
- Special slots (compass etc)
## Traders To debug a server mod in Visual Studio Code, you can copy the mod files into the `user/mods` folder and then start the server in [debug mode](#debugging). You should now be able to set breakpoints in the mod's Typescript files and they will be hit when the server runs the mod files.
- Buy/Sell
- Listed items are refreshed every hour
- purchase limits per refresh period
- Track sold rouble count
- Loyalty levels
- Build reputation
- Item repair
- Calculate randomised durability level based on item type/values
- Alternate clothing from Ragman
- Buy/unlock new clothing
- Insurance
- chance for items to be returned - higher chance for more expensive trader
- Chance parts will be stripped from returned weapons
- Fence
- Lists random items for sale
- Emulated system of 'churn' for items sold by fence
- every 4 minutes 20% of fences' items are replaced
- Configurable through config
## Flea market ## Contributing
- Buy and sell items
- Prices pulled from live data
- Listing tax fee
- Offer filtering
- Offer search
- Filter by item
- Linked search
- Simulated player offers
- Generated with random names/ratings/expiry times
- Variable prices based on live price (20% above/below)
- Weapon presets as offers
- Bartering offers
- Listed currency
- Rouble
- Euro
- Dollar
- Rating
- Increase flea rating by selling items
- Decrease flea rating by failing to sell items
- Will be purchased by simulated players
- Greater chance listed item will be purchased the lower it is listed for
- Adjust flea prices that are massively below trader buy price
- Receive purchased item through mail from seller
- Sorting by
- Rating
- Price
- Name
- Configurable using config
## Quests We're really excited that you're interested in contributing! Before submitting your contribution, please consider the following:
- ~~Accurate quest list~~ INCOMPLETE (85% complete)
- Trader quests
- Accept/Complete
- Daily Quests
- Simulated system of daily quests
- Replace daily quest
- Replace quest with new one
- Charged fee
- Scav daily quests
- Types
- Elimination
- Exit location
- Find
- Trader item unlocks through completion of quests
- Receive mail from traders after accepting/completing/failing a quest
- Item rewards given through mail
## Hideout ### Branchs
- Areas supported
- Air filter
- Air filter degradation speed calculation
- Skill levelling boost + 40%
- Bitcoin farm
- Coin generation speed calculation
- Booze generator
- Create moonshine
- Generator
- Fuel usage calculation
- Heating
- Energy regen rate
- Negative effects removal rate x2
- Illumination
- Intel centre
- ~~Unlocks scav tasks from fence~~ NOT IMPLEMENTED - unlocks at level 5
- ~~Reduces insurance return time by 20%~~ NOT IMPLEMENTED
- Quest money reward boost
- Lavatory
- Library
- Medstation
- Nutrition unit
- Rest space
- Scav case
- Custom reward system
- Configurable in config
- Security
- Shooting range
- Solar power
- Stash
- Gives bonus storage space
- Vents
- Water collector
- Workbench
- Christmas tree
- Item crafting
- Found in raid on completion
- Crafts when server not running
## Weapon building - __master__
- Create weapon presets The default branch used for the latest stable release. This branch is protected and typically is only merges with release branches.
- Saving of presets - __3.9.0-DEV__
Development for the next minor release of SPT. Minor releases target the latest version of EFT. Late in the minor release cycle the EFT version is frozen for stability to prepare for release. Larger changes to the project structure may be included in minor releases.
- __3.8.1-DEV__
Development for the next hotfix release of SPT. Hotfix releases include bug fixes and minor features that do not effect the coding structure of the project. Special care is taken to not break server mod stability. These always target the same version of EFT as the last minor release.
## Raids ### Pull Request Guidelines
- Supported maps
- Customs
- Factory day
- Factory night
- Reserve
- Woods
- Lighthouse
- Laboratory
- Shoreline
- Streets
- Loot
- Generated from over 30,000 loot runs on live, spawn chances calculated from all runs to give fairly accurate depiction of live loot.
- Static loot (containers)
- Each container type can contain items appropriate to that type
- Loose loot
- Randomised loose items found on map
- Airdrops
- Randomised chance of spawning
- Fire red flare to request an airdrop
- Drops 'themed' crates:
- Weapons / armor
- Only weapons and armor
- Food / medical
- Only food and medical items
- Barter goods
- Only barter goods
- Mixed
- A mixture of any of the above items
- Drops lootable crate in:
- Customs
- Reserve
- Woods
- Lighthouse
- Shoreline
- Streets
- Can be adjusted via config file
- Raid damage
- Exiting a raid with injury to player character will be persisted out of raid
- Post-raid therapist healing
- Scav Raids
- Adjusted time when running raids as scav
- Simulated loot being taken by other players the later into the raid player starts
## Messages - __Keep Them Small__
- Receive from traders If you're fixing a bug, try to keep the changes to the bug fix only. If you're adding a feature, try to keep the changes to the feature only. This will make it easier to review and merge your changes.
- Pin/unpin senders - __Perform a Self-Review__
- Accept all attachments Before submitting your changes, review your own code. This will help you catch any mistakes you may have made.
- Accept individual mail attachment - __Remove Noise__
Remove any unnecessary changes to white space, code style formatting, or some text change that has no impact related to the intention of the PR.
- __Create a Meaningful Title__
When creating a PR, make sure the title is meaningful and describes the changes you've made.
- __Write Detailed Commit Messages__
Bring out your table manners, speak the Queen's English and be on your best behaviour.
## Modding ### Style Guide
- Extensive system that allows for the modification of nearly any aspect of SPT
- Example mods covering a good slice of modding capabilities
## Misc We use Dprint to enforce a consistent code style. Please run `npm run style` and/or `npm run style:fix` before submitting your changes. This is made easier by using the recommended VSC extensions to automatically format your code whenever you save a file.
- Profiles
- Standard/Left Behind/Prepare To Escape/Edge Of Darkness
- Custom profiles
- SPT Easy start
- Lots of money / some QoL skills level 20 / level 69
- SPT Zero to hero
- No money, skills, trader rep or items, only a knife
- SPT Developer
- Testing profile, level 69, most skills maxed, max trader rep
- USEC have all quests ready to start
- BEAR have all quests ready to hand in
- Note system
- Add
- Edit
- Delete
- Extensive config system
- Alter how SPT works
- Holiday themes in hideout on appropriate days
- Halloween
- Christmas
## Code ### Tests
- TypeScript
- Majority of EFT request/response classes passed from client to server have been mapped We have a number of tests that are run automatically when you submit a pull request. You can run these tests locally by running `npm run test`. If you're adding a new feature or fixing a bug, please conceder adding tests to cover your changes so that we can ensure they don't break in the future.
- Unit Tests
- Supports tests via vitest ## License
- Dependency injection
- Config files accessible from `Aki_Data\Server\configs` / `project\assets\configs` This project is licensed under the NCSA Open Source License. See the [LICENSE](LICENSE.md) file for details.

10
project/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"recommendations": [
"EditorConfig.EditorConfig",
"dprint.dprint",
"dbaeumer.vscode-eslint",
"biomejs.biome",
"vitest.explorer",
"refringe.spt-id-highlighter"
]
}

View File

@ -4,14 +4,6 @@
"path": "." "path": "."
} }
], ],
"extensions": {
"recommendations": [
"EditorConfig.EditorConfig",
"dprint.dprint",
"dbaeumer.vscode-eslint",
"biomejs.biome"
]
},
"settings": { "settings": {
"window.title": "SPT-AKI Server", "window.title": "SPT-AKI Server",
"editor.formatOnSave": true, "editor.formatOnSave": true,

View File

@ -1013,7 +1013,8 @@
"mod_equipment_000": 3, "mod_equipment_000": 3,
"mod_equipment_001": 3, "mod_equipment_001": 3,
"mod_equipment_002": 3, "mod_equipment_002": 3,
"mod_nvg": 3 "mod_nvg": 3,
"mod_mount": 1
}, },
"weaponMods": { "weaponMods": {
"mod_barrel": 5, "mod_barrel": 5,

View File

@ -3663,6 +3663,23 @@
"associatedEvent": "Promo", "associatedEvent": "Promo",
"collectionTimeHours": 72, "collectionTimeHours": 72,
"messageText": "Thank you for purchasing of the first book in EFT series by A, Kontorovich. We are glad to give you this theme package with ingame items." "messageText": "Thank you for purchasing of the first book in EFT series by A, Kontorovich. We are glad to give you this theme package with ingame items."
},
"UNHEARD": {
"items": [
{
"_id": "a89275c1b18254ef7432a6d9",
"_tpl": "5696686a4bdc2da3298b456a",
"slotId": "main",
"upd": {
"StackObjectsCount": 250
},
"parentId": "64b996c9d0de4697180359b6"
}
],
"sender": "System",
"messageText": "Have a nice 20 minute adventure in the blatant plagiarist game. In and out",
"collectionTimeHours": 72,
"associatedEvent": "Promo"
} }
} }
} }

View File

@ -1293,13 +1293,12 @@
"minStaticLootPercent": 60, "minStaticLootPercent": 60,
"reducedChancePercent": 95, "reducedChancePercent": 95,
"reductionPercentWeights": { "reductionPercentWeights": {
"20": 2, "20": 3,
"30": 4, "30": 4,
"40": 4, "40": 5,
"50": 4, "50": 4,
"60": 4, "60": 4,
"70": 1, "70": 1
"80": 1
}, },
"adjustWaves": true "adjustWaves": true
}, },

View File

@ -80,6 +80,7 @@
"mod_equipment_001": 25, "mod_equipment_001": 25,
"mod_equipment_002": 25, "mod_equipment_002": 25,
"mod_nvg": 40, "mod_nvg": 40,
"mod_mount": 20,
"right_side_plate": 75 "right_side_plate": 75
}, },
"weaponMods": { "weaponMods": {

View File

@ -77,6 +77,7 @@
"mod_equipment_001": 25, "mod_equipment_001": 25,
"mod_equipment_002": 25, "mod_equipment_002": 25,
"mod_nvg": 40, "mod_nvg": 40,
"mod_mount": 20,
"right_side_plate": 75 "right_side_plate": 75
}, },
"weaponMods": { "weaponMods": {

View File

@ -431,6 +431,7 @@
"pmcresponse-victim_negative_99": "Your computer so bad you get 20fps on streets", "pmcresponse-victim_negative_99": "Your computer so bad you get 20fps on streets",
"pmcresponse-victim_negative_100": "I bet you installed SAIN and had to remove it cuz you kept getting killed too much", "pmcresponse-victim_negative_100": "I bet you installed SAIN and had to remove it cuz you kept getting killed too much",
"pmcresponse-victim_negative_101": "What the HECK did you just HECKING say about me, you little Scav? Ill have you know I graduated top of my class in the USEC corps, and Ive been involved in numerous secret raids on the {{playerSide}}s, and I have over 300 confirmed kills. I am trained in gorilla warfare and Im the top sniper in the entire USEC armed forces. You are nothing to me but just another target. I will wipe you the HECK out with precision the likes of which has never been seen before in this raid, mark my HECKING words. You think you can get away with saying that shit to me over the messaging window? Think again, HECKER. As we speak I am contacting my secret network of spies across the Customs location and your stash is being traced right now so you better prepare for the storm, maggot. The storm that wipes out the pathetic little thing you call your life. Youre HECKING dead, Scav. I can be anywhere, anytime, and I can kill you in over seven hundred ways, and thats just with my bare hands. Not only am I extensively trained in unarmed combat, but I have access to the entire arsenal of the USEC Corps and I will use it to its full extent to wipe your miserable butt off the face of the map, you little poop. If only you could have known what unholy retribution your little “clever” kill was about to bring down upon you, maybe you would have held your HECKING tongue. But you couldnt, you didnt, and now youre paying the price, you HECKING idiot. I will POOP fury all over you and you will drown in it. Youre HECKING dead, Scav.", "pmcresponse-victim_negative_101": "What the HECK did you just HECKING say about me, you little Scav? Ill have you know I graduated top of my class in the USEC corps, and Ive been involved in numerous secret raids on the {{playerSide}}s, and I have over 300 confirmed kills. I am trained in gorilla warfare and Im the top sniper in the entire USEC armed forces. You are nothing to me but just another target. I will wipe you the HECK out with precision the likes of which has never been seen before in this raid, mark my HECKING words. You think you can get away with saying that shit to me over the messaging window? Think again, HECKER. As we speak I am contacting my secret network of spies across the Customs location and your stash is being traced right now so you better prepare for the storm, maggot. The storm that wipes out the pathetic little thing you call your life. Youre HECKING dead, Scav. I can be anywhere, anytime, and I can kill you in over seven hundred ways, and thats just with my bare hands. Not only am I extensively trained in unarmed combat, but I have access to the entire arsenal of the USEC Corps and I will use it to its full extent to wipe your miserable butt off the face of the map, you little poop. If only you could have known what unholy retribution your little “clever” kill was about to bring down upon you, maybe you would have held your HECKING tongue. But you couldnt, you didnt, and now youre paying the price, you HECKING idiot. I will POOP fury all over you and you will drown in it. Youre HECKING dead, Scav.",
"pmcresponse-victim_negative_102": "I bet you bought that new edition just for the bigger pockets",
"pmcresponse-victim_plead_1": "I was questing", "pmcresponse-victim_plead_1": "I was questing",
"pmcresponse-victim_plead_2": "I just wanted to finish a quest, whyd you kill me", "pmcresponse-victim_plead_2": "I just wanted to finish a quest, whyd you kill me",
"pmcresponse-victim_plead_3": "Hope ur happy i can't even afford a new kit", "pmcresponse-victim_plead_3": "Hope ur happy i can't even afford a new kit",
@ -529,6 +530,7 @@
"pmcresponse-killer_negative_27": "Easiest loot of today", "pmcresponse-killer_negative_27": "Easiest loot of today",
"pmcresponse-killer_negative_28": "Not to worry, i stashed your gear at your moms house", "pmcresponse-killer_negative_28": "Not to worry, i stashed your gear at your moms house",
"pmcresponse-killer_negative_29": "Were you even trying", "pmcresponse-killer_negative_29": "Were you even trying",
"pmcresponse-killer_negative_30": "I bet you actually paid 250 big ones for that new edition",
"pmcresponse-killer_plead_1": "I was trying to extract a quest item and you were in my path", "pmcresponse-killer_plead_1": "I was trying to extract a quest item and you were in my path",
"pmcresponse-killer_plead_2": "I was looting barrel caches and you were in the way, sorry", "pmcresponse-killer_plead_2": "I was looting barrel caches and you were in the way, sorry",
"pmcresponse-killer_plead_3": "I need PMC kills, Im sure you understand", "pmcresponse-killer_plead_3": "I need PMC kills, Im sure you understand",

View File

@ -173,7 +173,7 @@ export class MatchController
if (extractName && this.extractWasViaCoop(extractName) && this.traderConfig.fence.coopExtractGift.sendGift) if (extractName && this.extractWasViaCoop(extractName) && this.traderConfig.fence.coopExtractGift.sendGift)
{ {
this.handleCoopExtract(pmcData, extractName); this.handleCoopExtract(sessionId, pmcData, extractName);
this.sendCoopTakenFenceMessage(sessionId); this.sendCoopTakenFenceMessage(sessionId);
} }
} }
@ -225,10 +225,11 @@ export class MatchController
/** /**
* Handle when a player extracts using a coop extract - add rep to fence * Handle when a player extracts using a coop extract - add rep to fence
* @param sessionId Session/player id
* @param pmcData Profile * @param pmcData Profile
* @param extractName Name of extract taken * @param extractName Name of extract taken
*/ */
protected handleCoopExtract(pmcData: IPmcData, extractName: string): void protected handleCoopExtract(sessionId: string, pmcData: IPmcData, extractName: string): void
{ {
if (!pmcData.CoopExtractCounts) if (!pmcData.CoopExtractCounts)
{ {
@ -256,6 +257,11 @@ export class MatchController
// Check if new standing has leveled up trader // Check if new standing has leveled up trader
this.traderHelper.lvlUp(fenceId, pmcData); this.traderHelper.lvlUp(fenceId, pmcData);
pmcData.TradersInfo[fenceId].loyaltyLevel = Math.max(pmcData.TradersInfo[fenceId].loyaltyLevel, 1); pmcData.TradersInfo[fenceId].loyaltyLevel = Math.max(pmcData.TradersInfo[fenceId].loyaltyLevel, 1);
// Copy updated fence rep values into scav profile to ensure consistency
const scavData: IPmcData = this.profileHelper.getScavProfile(sessionId);
scavData.TradersInfo[fenceId].standing = pmcData.TradersInfo[fenceId].standing;
scavData.TradersInfo[fenceId].loyaltyLevel = pmcData.TradersInfo[fenceId].loyaltyLevel;
} }
/** /**
@ -313,6 +319,7 @@ export class MatchController
this.logger.debug( this.logger.debug(
`Car extract: ${extractName} used, total times taken: ${pmcData.CarExtractCounts[extractName]}`, `Car extract: ${extractName} used, total times taken: ${pmcData.CarExtractCounts[extractName]}`,
); );
// Copy updated fence rep values into scav profile to ensure consistency // Copy updated fence rep values into scav profile to ensure consistency
const scavData: IPmcData = this.profileHelper.getScavProfile(sessionId); const scavData: IPmcData = this.profileHelper.getScavProfile(sessionId);
scavData.TradersInfo[fenceId].standing = pmcData.TradersInfo[fenceId].standing; scavData.TradersInfo[fenceId].standing = pmcData.TradersInfo[fenceId].standing;

View File

@ -90,7 +90,9 @@ import { BotHelper } from "@spt-aki/helpers/BotHelper";
import { BotWeaponGeneratorHelper } from "@spt-aki/helpers/BotWeaponGeneratorHelper"; import { BotWeaponGeneratorHelper } from "@spt-aki/helpers/BotWeaponGeneratorHelper";
import { ContainerHelper } from "@spt-aki/helpers/ContainerHelper"; import { ContainerHelper } from "@spt-aki/helpers/ContainerHelper";
import { SptCommandoCommands } from "@spt-aki/helpers/Dialogue/Commando/SptCommandoCommands"; import { SptCommandoCommands } from "@spt-aki/helpers/Dialogue/Commando/SptCommandoCommands";
import { GiveSptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/GiveSptCommand"; import { GiveSptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/GiveCommand/GiveSptCommand";
import { ProfileSptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/ProfileCommand/ProfileSptCommand";
import { TraderSptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/TraderCommand/TraderSptCommand";
import { CommandoDialogueChatBot } from "@spt-aki/helpers/Dialogue/CommandoDialogueChatBot"; import { CommandoDialogueChatBot } from "@spt-aki/helpers/Dialogue/CommandoDialogueChatBot";
import { SptDialogueChatBot } from "@spt-aki/helpers/Dialogue/SptDialogueChatBot"; import { SptDialogueChatBot } from "@spt-aki/helpers/Dialogue/SptDialogueChatBot";
import { DialogueHelper } from "@spt-aki/helpers/DialogueHelper"; import { DialogueHelper } from "@spt-aki/helpers/DialogueHelper";
@ -375,6 +377,8 @@ export class Container
// SptCommando Commands // SptCommando Commands
depContainer.registerType("SptCommand", "GiveSptCommand"); depContainer.registerType("SptCommand", "GiveSptCommand");
depContainer.registerType("SptCommand", "TraderSptCommand");
depContainer.registerType("SptCommand", "ProfileSptCommand");
} }
private static registerUtils(depContainer: DependencyContainer): void private static registerUtils(depContainer: DependencyContainer): void
@ -598,6 +602,12 @@ export class Container
}); });
// SptCommands // SptCommands
depContainer.register<GiveSptCommand>("GiveSptCommand", GiveSptCommand, { lifecycle: Lifecycle.Singleton }); depContainer.register<GiveSptCommand>("GiveSptCommand", GiveSptCommand, { lifecycle: Lifecycle.Singleton });
depContainer.register<TraderSptCommand>("TraderSptCommand", TraderSptCommand, {
lifecycle: Lifecycle.Singleton,
});
depContainer.register<ProfileSptCommand>("ProfileSptCommand", ProfileSptCommand, {
lifecycle: Lifecycle.Singleton,
});
} }
private static registerLoaders(depContainer: DependencyContainer): void private static registerLoaders(depContainer: DependencyContainer): void

View File

@ -57,12 +57,34 @@ export abstract class AbstractDialogueChatBot implements IDialogueChatBot
if (splitCommand[0].toLowerCase() === "help") if (splitCommand[0].toLowerCase() === "help")
{ {
const helpMessage = this.chatCommands.map((c) => this.mailSendService.sendUserMessageToPlayer(
`Available commands:\n\n${c.getCommandPrefix()}:\n\n${ sessionId,
Array.from(c.getCommands()).map((command) => c.getCommandHelp(command)).join("\n") this.getChatBot(),
}` "The available commands will be listed below:",
).join("\n"); );
this.mailSendService.sendUserMessageToPlayer(sessionId, this.getChatBot(), helpMessage); // due to BSG being dumb with messages we need a mandatory timeout between messages so they get out on the right order
setTimeout(() =>
{
for (const chatCommand of this.chatCommands)
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
this.getChatBot(),
`Commands available for "${chatCommand.getCommandPrefix()}" prefix:`,
);
setTimeout(() =>
{
for (const subCommand of chatCommand.getCommands())
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
this.getChatBot(),
`Subcommand ${subCommand}:\n${chatCommand.getCommandHelp(subCommand)}`,
);
}
}, 1000);
}
}, 1000);
return request.dialogId; return request.dialogId;
} }

View File

@ -1,5 +1,5 @@
import { SavedCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/GiveCommand/SavedCommand";
import { ISptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/ISptCommand"; import { ISptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/ISptCommand";
import { SavedCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/SavedCommand";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
import { PresetHelper } from "@spt-aki/helpers/PresetHelper"; import { PresetHelper } from "@spt-aki/helpers/PresetHelper";
import { Item } from "@spt-aki/models/eft/common/tables/IItem"; import { Item } from "@spt-aki/models/eft/common/tables/IItem";
@ -107,6 +107,15 @@ export class GiveSptCommand implements ISptCommand
isItemName = result[5] !== undefined; isItemName = result[5] !== undefined;
item = result[5] ? result[5] : result[2]; item = result[5] ? result[5] : result[2];
quantity = +result[6]; quantity = +result[6];
if (quantity <= 0)
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
`Invalid quantity! Must be 1 or higher. Use \"help\" for more information.`,
);
return request.dialogId;
}
if (isItemName) if (isItemName)
{ {

View File

@ -0,0 +1,163 @@
import { SavedCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/GiveCommand/SavedCommand";
import { ISptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/ISptCommand";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
import { PresetHelper } from "@spt-aki/helpers/PresetHelper";
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest";
import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile";
import { SkillTypes } from "@spt-aki/models/enums/SkillTypes";
import { IProfileChangeEvent, ProfileChangeEventType } from "@spt-aki/models/spt/dialog/ISendMessageDetails";
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 { inject, injectable } from "tsyringe";
@injectable()
export class ProfileSptCommand implements ISptCommand
{
/**
* Regex to account for all these cases:
* spt profile level 20
* spt profile skill metabolism 10
*/
private static commandRegex =
/^spt profile (?<command>level|skill)((?<=.*skill) (?<skill>[\w]+)){0,1} (?<quantity>(?!0+)[0-9]+)$/;
protected savedCommand: SavedCommand;
public constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("HashUtil") protected hashUtil: HashUtil,
@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,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
)
{
}
public getCommand(): string
{
return "profile";
}
public getCommandHelp(): string
{
return "spt profile\n========\nSets the profile level or skill to the desired level through the message system.\n\n\tspt profile level [desired level]\n\t\tEx: spt profile level 20\n\n\tspt profile skill [skill name] [quantity]\n\t\tEx: spt profile skill metabolism 51";
}
public performAction(commandHandler: IUserDialogInfo, sessionId: string, request: ISendMessageRequest): string
{
if (!ProfileSptCommand.commandRegex.test(request.text))
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
"Invalid use of trader command. Use \"help\" for more information.",
);
return request.dialogId;
}
const result = ProfileSptCommand.commandRegex.exec(request.text);
const command: string = result.groups.command;
const skill: string = result.groups.skill;
const quantity: number = +result.groups.quantity;
let event: IProfileChangeEvent;
switch (command)
{
case "level":
if (quantity < 1 || quantity > this.profileHelper.getMaxLevel())
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
"Invalid use of profile command, the level was outside bounds: 1 to 70. Use \"help\" for more information.",
);
return request.dialogId;
}
event = this.handleLevelCommand(quantity);
break;
case "skill":
{
const enumSkill = Object.values(SkillTypes).find((t) =>
t.toLocaleLowerCase() === skill.toLocaleLowerCase()
);
if (enumSkill === undefined)
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
"Invalid use of profile command, the skill was not found. Use \"help\" for more information.",
);
return request.dialogId;
}
if (quantity < 0 || quantity > 51)
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
"Invalid use of profile command, the skill level was outside bounds: 1 to 51. Use \"help\" for more information.",
);
return request.dialogId;
}
event = this.handleSkillCommand(enumSkill, quantity);
break;
}
default:
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
`If you are reading this, this is bad. Please report this to SPT staff with a screenshot. Command ${command}.`,
);
return request.dialogId;
}
this.mailSendService.sendSystemMessageToPlayer(
sessionId,
"A single ruble is being attached, required by BSG logic.",
[{
_id: this.hashUtil.generate(),
_tpl: "5449016a4bdc2d6f028b456f",
upd: { StackObjectsCount: 1 },
parentId: this.hashUtil.generate(),
slotId: "main",
}],
undefined,
[event],
);
return request.dialogId;
}
protected handleSkillCommand(skill: string, level: number): IProfileChangeEvent
{
const event: IProfileChangeEvent = {
_id: this.hashUtil.generate(),
Type: ProfileChangeEventType.SKILL_POINTS,
value: level * 100,
entity: skill,
};
return event;
}
protected handleLevelCommand(level: number): IProfileChangeEvent
{
const exp = this.profileHelper.getExperience(level);
const event: IProfileChangeEvent = {
_id: this.hashUtil.generate(),
Type: ProfileChangeEventType.PROFILE_LEVEL,
value: exp,
entity: null,
};
return event;
}
}

View File

@ -0,0 +1,114 @@
import { SavedCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/GiveCommand/SavedCommand";
import { ISptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/ISptCommand";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
import { PresetHelper } from "@spt-aki/helpers/PresetHelper";
import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest";
import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile";
import { IProfileChangeEvent, ProfileChangeEventType } from "@spt-aki/models/spt/dialog/ISendMessageDetails";
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 { inject, injectable } from "tsyringe";
@injectable()
export class TraderSptCommand implements ISptCommand
{
/**
* Regex to account for all these cases:
* spt trader prapor rep 100
* spt trader mechanic spend 1000000
*/
private static commandRegex = /^spt trader (?<trader>[\w]+) (?<command>rep|spend) (?<quantity>(?!0+)[0-9]+)$/;
protected savedCommand: SavedCommand;
public constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("HashUtil") protected hashUtil: HashUtil,
@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,
)
{
}
public getCommand(): string
{
return "trader";
}
public getCommandHelp(): string
{
return "spt trader\n========\nSets the reputation or money spent to the input quantity through the message system.\n\n\tspt trader [trader] rep [quantity]\n\t\tEx: spt trader prapor rep 2\n\n\tspt trader [trader] spend [quantity]\n\t\tEx: spt trader therapist spend 1000000";
}
public performAction(commandHandler: IUserDialogInfo, sessionId: string, request: ISendMessageRequest): string
{
if (!TraderSptCommand.commandRegex.test(request.text))
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
"Invalid use of trader command. Use \"help\" for more information.",
);
return request.dialogId;
}
const result = TraderSptCommand.commandRegex.exec(request.text);
const trader: string = result.groups.trader;
const command: string = result.groups.command;
const quantity: number = +result.groups.quantity;
const dbTrader = Object.values(this.databaseServer.getTables().traders).find((t) =>
t.base.nickname.toLocaleLowerCase() === trader.toLocaleLowerCase()
);
if (dbTrader === undefined)
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
"Invalid use of trader command, the trader was not found. Use \"help\" for more information.",
);
return request.dialogId;
}
let profileChangeEventType: ProfileChangeEventType;
switch (command)
{
case "rep":
profileChangeEventType = ProfileChangeEventType.TRADER_STANDING;
break;
case "spend":
profileChangeEventType = ProfileChangeEventType.TRADER_SALES_SUM;
break;
}
const event: IProfileChangeEvent = {
_id: this.hashUtil.generate(),
Type: profileChangeEventType,
value: quantity,
entity: dbTrader.base._id,
};
this.mailSendService.sendSystemMessageToPlayer(
sessionId,
"A single ruble is being attached, required by BSG logic.",
[{
_id: this.hashUtil.generate(),
_tpl: "5449016a4bdc2d6f028b456f",
upd: { StackObjectsCount: 1 },
parentId: this.hashUtil.generate(),
slotId: "main",
}],
undefined,
[event],
);
return request.dialogId;
}
}

View File

@ -272,7 +272,7 @@ export class TraderHelper
public getTraderUpdateSeconds(traderId: string): number public getTraderUpdateSeconds(traderId: string): number
{ {
const traderDetails = this.traderConfig.updateTime.find((x) => x.traderId === traderId); const traderDetails = this.traderConfig.updateTime.find((x) => x.traderId === traderId);
if (!traderDetails) if (!traderDetails || traderDetails.seconds.min === undefined || traderDetails.seconds.max === undefined)
{ {
this.logger.warning( this.logger.warning(
this.localisationService.getText("trader-missing_trader_details_using_default_refresh_time", { this.localisationService.getText("trader-missing_trader_details_using_default_refresh_time", {

View File

@ -34,7 +34,17 @@ export interface ISendMessageDetails
export interface IProfileChangeEvent export interface IProfileChangeEvent
{ {
_id: string; _id: string;
Type: "TraderSalesSum" | "TraderStanding" | "ProfileLevel" | "SkillPoints" | "ExamineAllItems" | "UnlockTrader"; Type: ProfileChangeEventType;
value: number; value: number;
entity?: string; entity?: string;
} }
export enum ProfileChangeEventType
{
TRADER_SALES_SUM = "TraderSalesSum",
TRADER_STANDING = "TraderStanding",
PROFILE_LEVEL = "ProfileLevel",
SKILL_POINTS = "SkillPoints",
EXAMINE_ALL_ITEMS = "ExamineAllItems",
UNLOCK_TRADER = "UnlockTrader",
}

View File

@ -10,7 +10,7 @@ import { Dialogue, IUserDialogInfo, Message, MessageItems } from "@spt-aki/model
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses"; import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
import { MessageType } from "@spt-aki/models/enums/MessageType"; import { MessageType } from "@spt-aki/models/enums/MessageType";
import { Traders } from "@spt-aki/models/enums/Traders"; import { Traders } from "@spt-aki/models/enums/Traders";
import { ISendMessageDetails } from "@spt-aki/models/spt/dialog/ISendMessageDetails"; import { IProfileChangeEvent, ISendMessageDetails } from "@spt-aki/models/spt/dialog/ISendMessageDetails";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"; import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { SaveServer } from "@spt-aki/servers/SaveServer"; import { SaveServer } from "@spt-aki/servers/SaveServer";
@ -169,7 +169,8 @@ export class MailSendService
sessionId: string, sessionId: string,
message: string, message: string,
items: Item[] = [], items: Item[] = [],
maxStorageTimeSeconds = null, maxStorageTimeSeconds?: number,
profileChangeEvents?: IProfileChangeEvent[],
): void ): void
{ {
const details: ISendMessageDetails = { const details: ISendMessageDetails = {
@ -185,6 +186,11 @@ export class MailSendService
details.itemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800; // 48 hours if no value supplied details.itemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800; // 48 hours if no value supplied
} }
if ((profileChangeEvents?.length ?? 0) > 0)
{
details.profileChangeEvents = profileChangeEvents;
}
this.sendMessageToPlayer(details); this.sendMessageToPlayer(details);
} }
@ -199,8 +205,8 @@ export class MailSendService
sessionId: string, sessionId: string,
messageLocaleId: string, messageLocaleId: string,
items: Item[] = [], items: Item[] = [],
profileChangeEvents = [], profileChangeEvents?: IProfileChangeEvent[],
maxStorageTimeSeconds = null, maxStorageTimeSeconds?: number,
): void ): void
{ {
const details: ISendMessageDetails = { const details: ISendMessageDetails = {
@ -216,7 +222,7 @@ export class MailSendService
details.itemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800; // 48 hours if no value supplied details.itemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800; // 48 hours if no value supplied
} }
if (profileChangeEvents.length > 0) if ((profileChangeEvents?.length ?? 0) > 0)
{ {
details.profileChangeEvents = profileChangeEvents; details.profileChangeEvents = profileChangeEvents;
} }

View File

@ -65,7 +65,13 @@ export class PaymentService
if (!this.paymentHelper.isMoneyTpl(item._tpl)) if (!this.paymentHelper.isMoneyTpl(item._tpl))
{ {
// If the item is not money, remove it from the inventory. // If the item is not money, remove it from the inventory.
this.inventoryHelper.removeItem(pmcData, item._id, sessionID, output); this.inventoryHelper.removeItemByCount(
pmcData,
item._id,
request.scheme_items[index].count,
sessionID,
output,
);
request.scheme_items[index].count = 0; request.scheme_items[index].count = 0;
} }
else else