diff --git a/.gitea/workflows/run-test.yaml b/.gitea/workflows/run-test.yaml index e5f9adad..196672a8 100644 --- a/.gitea/workflows/run-test.yaml +++ b/.gitea/workflows/run-test.yaml @@ -9,50 +9,65 @@ on: jobs: vitest: 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: 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/${GITHUB_REPOSITORY}.git --branch master /workspace/SPT-AKI/Build/server + - name: Clone + run: | + # For pull request events, checkout using GITHUB_SHA + # 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 - git checkout ${GITHUB_SHA} - shell: bash + rm -rf /workspace/SPT-AKI/Server/current + git clone https://dev.sp-tarkov.com/${{ github.repository }}.git /workspace/SPT-AKI/Server/current - - name: Pull LFS Files - run: | - cd /workspace/SPT-AKI/Build/server - git lfs pull - git lfs ls-files - shell: bash + cd /workspace/SPT-AKI/Server/current + git fetch + git checkout $REF + env: + GITHUB_SHA: ${{ github.sha }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_EVENT_NAME: ${{ github.event_name }} + 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: Pull LFS Files + run: | + cd /workspace/SPT-AKI/Server/current && ls -lah + git lfs pull && git lfs ls-files + shell: bash - - 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: Cache NPM Dependencies + id: cache-npm-dependencies + uses: actions/cache@v4 + with: + path: /workspace/SPT-AKI/Server/current/project/node_modules + key: npm-dependencies-${{ hashFiles('/workspace/SPT-AKI/Server/current/project/package.json') }} - - name: Run Tests - id: run-tests - run: | - cd /workspace/SPT-AKI/Build/server/project - npm run test - shell: bash + - name: Install NPM Dependencies + if: steps.cache-npm-dependencies.outputs.cache-hit != 'true' + run: | + cd /workspace/SPT-AKI/Server/current/project + rm -rf /workspace/SPT-AKI/Server/current/project/node_modules + npm install + 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 + - name: Run Tests + id: run-tests + run: | + cd /workspace/SPT-AKI/Server/current/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 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 diff --git a/FEATURES.md b/FEATURES.md new file mode 100644 index 00000000..17ca7702 --- /dev/null +++ b/FEATURES.md @@ -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. diff --git a/README.md b/README.md index 42469df8..de26217d 100644 --- a/README.md +++ b/README.md @@ -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) -[![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) +# Table of Contents -## Privacy -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. -Feel free to use a "fake" username and email on your commits by using the following commands: -```bash -git config --local user.name "USERNAME" -git config --local user.email "USERNAME@SOMETHING.com" -``` +- [Features](#features) +- [Installation](#installation) + - [Requirements](#requirements) + - [Initial Setup](#initial-setup) +- [Development](#development) + - [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) -- Visual Studio Code -- git [LFS](https://git-lfs.github.com/) +For a full list of features, please see [FEATURES.md](FEATURES.md). -## 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 -- 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. +### Requirements -## Pulling -- Run `git lfs fetch` and `git lfs pull` to acquire loot files +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. -## 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` -2. Visual Studio Code > Terminal > Run Task... > npm > npm: Install +- [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) - Editor Settings Synchronization +- [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** --------- | ----------------------------------------------------------------- -release | Visual Studio Code > Terminal > Run Build Task... > build:release -debug | Visual Studio Code > Terminal > Run Build Task... > build:debug +To prepare the project for development you will need to: -## 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 -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 +The following commands are available after the initial setup. Run them with `npm run `. -## 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: - - 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 +### Debugging -## PMCs -- 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 +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. -## Inventory - - 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) +### Mod Debugging -## Traders -- 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 +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. -## Flea market -- 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 +## Contributing -## Quests -- ~~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 +We're really excited that you're interested in contributing! Before submitting your contribution, please consider the following: -## Hideout -- 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 +### Branchs -## Weapon building -- Create weapon presets -- Saving of presets +- __master__ + The default branch used for the latest stable release. This branch is protected and typically is only merges with release branches. +- __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 -- 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 +### Pull Request Guidelines -## Messages -- Receive from traders -- Pin/unpin senders -- Accept all attachments -- Accept individual mail attachment +- __Keep Them Small__ + 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. +- __Perform a Self-Review__ + Before submitting your changes, review your own code. This will help you catch any mistakes you may have made. +- __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 -- Extensive system that allows for the modification of nearly any aspect of SPT -- Example mods covering a good slice of modding capabilities +### Style Guide -## Misc -- 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 +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. -## Code -- TypeScript - - Majority of EFT request/response classes passed from client to server have been mapped -- Unit Tests - - Supports tests via vitest -- Dependency injection -- Config files accessible from `Aki_Data\Server\configs` / `project\assets\configs` +### Tests + +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. + +## License + +This project is licensed under the NCSA Open Source License. See the [LICENSE](LICENSE.md) file for details. diff --git a/project/.vscode/extensions.json b/project/.vscode/extensions.json new file mode 100644 index 00000000..49f98cc2 --- /dev/null +++ b/project/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "EditorConfig.EditorConfig", + "dprint.dprint", + "dbaeumer.vscode-eslint", + "biomejs.biome", + "vitest.explorer", + "refringe.spt-id-highlighter" + ] +} diff --git a/project/Server.code-workspace b/project/Server.code-workspace index 1758f080..5ab13e4e 100644 --- a/project/Server.code-workspace +++ b/project/Server.code-workspace @@ -4,14 +4,6 @@ "path": "." } ], - "extensions": { - "recommendations": [ - "EditorConfig.EditorConfig", - "dprint.dprint", - "dbaeumer.vscode-eslint", - "biomejs.biome" - ] - }, "settings": { "window.title": "SPT-AKI Server", "editor.formatOnSave": true, diff --git a/project/assets/configs/bot.json b/project/assets/configs/bot.json index 1e4efb5f..f9c916dc 100644 --- a/project/assets/configs/bot.json +++ b/project/assets/configs/bot.json @@ -1013,7 +1013,8 @@ "mod_equipment_000": 3, "mod_equipment_001": 3, "mod_equipment_002": 3, - "mod_nvg": 3 + "mod_nvg": 3, + "mod_mount": 1 }, "weaponMods": { "mod_barrel": 5, diff --git a/project/assets/configs/gifts.json b/project/assets/configs/gifts.json index d5518189..d5f3629b 100644 --- a/project/assets/configs/gifts.json +++ b/project/assets/configs/gifts.json @@ -3663,6 +3663,23 @@ "associatedEvent": "Promo", "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." + }, + "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" } } } \ No newline at end of file diff --git a/project/assets/configs/location.json b/project/assets/configs/location.json index e518d8c7..640a7d50 100644 --- a/project/assets/configs/location.json +++ b/project/assets/configs/location.json @@ -1293,13 +1293,12 @@ "minStaticLootPercent": 60, "reducedChancePercent": 95, "reductionPercentWeights": { - "20": 2, + "20": 3, "30": 4, - "40": 4, + "40": 5, "50": 4, "60": 4, - "70": 1, - "80": 1 + "70": 1 }, "adjustWaves": true }, diff --git a/project/assets/database/bots/types/bear.json b/project/assets/database/bots/types/bear.json index d135d38e..8b42f5af 100644 --- a/project/assets/database/bots/types/bear.json +++ b/project/assets/database/bots/types/bear.json @@ -80,6 +80,7 @@ "mod_equipment_001": 25, "mod_equipment_002": 25, "mod_nvg": 40, + "mod_mount": 20, "right_side_plate": 75 }, "weaponMods": { diff --git a/project/assets/database/bots/types/usec.json b/project/assets/database/bots/types/usec.json index 983b3201..f14bd29c 100644 --- a/project/assets/database/bots/types/usec.json +++ b/project/assets/database/bots/types/usec.json @@ -77,6 +77,7 @@ "mod_equipment_001": 25, "mod_equipment_002": 25, "mod_nvg": 40, + "mod_mount": 20, "right_side_plate": 75 }, "weaponMods": { diff --git a/project/assets/database/locales/server/en.json b/project/assets/database/locales/server/en.json index bd4c2091..2346aadf 100644 --- a/project/assets/database/locales/server/en.json +++ b/project/assets/database/locales/server/en.json @@ -431,6 +431,7 @@ "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_101": "What the HECK did you just HECKING say about me, you little Scav? I’ll have you know I graduated top of my class in the USEC corps, and I’ve been involved in numerous secret raids on the {{playerSide}}s, and I have over 300 confirmed kills. I am trained in gorilla warfare and I’m 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. You’re HECKING dead, Scav. I can be anywhere, anytime, and I can kill you in over seven hundred ways, and that’s 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 couldn’t, you didn’t, and now you’re paying the price, you HECKING idiot. I will POOP fury all over you and you will drown in it. You’re 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_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", @@ -529,6 +530,7 @@ "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_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_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", diff --git a/project/src/controllers/MatchController.ts b/project/src/controllers/MatchController.ts index 4869e4b8..60b69751 100644 --- a/project/src/controllers/MatchController.ts +++ b/project/src/controllers/MatchController.ts @@ -173,7 +173,7 @@ export class MatchController if (extractName && this.extractWasViaCoop(extractName) && this.traderConfig.fence.coopExtractGift.sendGift) { - this.handleCoopExtract(pmcData, extractName); + this.handleCoopExtract(sessionId, pmcData, extractName); this.sendCoopTakenFenceMessage(sessionId); } } @@ -225,10 +225,11 @@ export class MatchController /** * Handle when a player extracts using a coop extract - add rep to fence + * @param sessionId Session/player id * @param pmcData Profile * @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) { @@ -256,6 +257,11 @@ export class MatchController // Check if new standing has leveled up trader this.traderHelper.lvlUp(fenceId, pmcData); 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( `Car extract: ${extractName} used, total times taken: ${pmcData.CarExtractCounts[extractName]}`, ); + // 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; diff --git a/project/src/di/Container.ts b/project/src/di/Container.ts index 2fdfde9d..ba2c1223 100644 --- a/project/src/di/Container.ts +++ b/project/src/di/Container.ts @@ -90,7 +90,9 @@ import { BotHelper } from "@spt-aki/helpers/BotHelper"; import { BotWeaponGeneratorHelper } from "@spt-aki/helpers/BotWeaponGeneratorHelper"; import { ContainerHelper } from "@spt-aki/helpers/ContainerHelper"; 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 { SptDialogueChatBot } from "@spt-aki/helpers/Dialogue/SptDialogueChatBot"; import { DialogueHelper } from "@spt-aki/helpers/DialogueHelper"; @@ -375,6 +377,8 @@ export class Container // SptCommando Commands depContainer.registerType("SptCommand", "GiveSptCommand"); + depContainer.registerType("SptCommand", "TraderSptCommand"); + depContainer.registerType("SptCommand", "ProfileSptCommand"); } private static registerUtils(depContainer: DependencyContainer): void @@ -598,6 +602,12 @@ export class Container }); // SptCommands depContainer.register("GiveSptCommand", GiveSptCommand, { lifecycle: Lifecycle.Singleton }); + depContainer.register("TraderSptCommand", TraderSptCommand, { + lifecycle: Lifecycle.Singleton, + }); + depContainer.register("ProfileSptCommand", ProfileSptCommand, { + lifecycle: Lifecycle.Singleton, + }); } private static registerLoaders(depContainer: DependencyContainer): void diff --git a/project/src/helpers/Dialogue/AbstractDialogueChatBot.ts b/project/src/helpers/Dialogue/AbstractDialogueChatBot.ts index ed387b97..c5f9b5c1 100644 --- a/project/src/helpers/Dialogue/AbstractDialogueChatBot.ts +++ b/project/src/helpers/Dialogue/AbstractDialogueChatBot.ts @@ -57,12 +57,34 @@ export abstract class AbstractDialogueChatBot implements IDialogueChatBot 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); + this.mailSendService.sendUserMessageToPlayer( + sessionId, + this.getChatBot(), + "The available commands will be listed below:", + ); + // 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; } diff --git a/project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts b/project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/GiveSptCommand.ts similarity index 96% rename from project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts rename to project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/GiveSptCommand.ts index ccab75e2..9bfb7004 100644 --- a/project/src/helpers/Dialogue/Commando/SptCommands/GiveSptCommand.ts +++ b/project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/GiveSptCommand.ts @@ -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 { 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"; @@ -107,6 +107,15 @@ export class GiveSptCommand implements ISptCommand isItemName = result[5] !== undefined; item = result[5] ? result[5] : result[2]; 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) { diff --git a/project/src/helpers/Dialogue/Commando/SptCommands/SavedCommand.ts b/project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/SavedCommand.ts similarity index 100% rename from project/src/helpers/Dialogue/Commando/SptCommands/SavedCommand.ts rename to project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/SavedCommand.ts diff --git a/project/src/helpers/Dialogue/Commando/SptCommands/ProfileCommand/ProfileSptCommand.ts b/project/src/helpers/Dialogue/Commando/SptCommands/ProfileCommand/ProfileSptCommand.ts new file mode 100644 index 00000000..025a205c --- /dev/null +++ b/project/src/helpers/Dialogue/Commando/SptCommands/ProfileCommand/ProfileSptCommand.ts @@ -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 (?level|skill)((?<=.*skill) (?[\w]+)){0,1} (?(?!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; + } +} diff --git a/project/src/helpers/Dialogue/Commando/SptCommands/TraderCommand/TraderSptCommand.ts b/project/src/helpers/Dialogue/Commando/SptCommands/TraderCommand/TraderSptCommand.ts new file mode 100644 index 00000000..feb3c0b2 --- /dev/null +++ b/project/src/helpers/Dialogue/Commando/SptCommands/TraderCommand/TraderSptCommand.ts @@ -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 (?[\w]+) (?rep|spend) (?(?!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; + } +} diff --git a/project/src/helpers/TraderHelper.ts b/project/src/helpers/TraderHelper.ts index 075e57fc..64d2f17d 100644 --- a/project/src/helpers/TraderHelper.ts +++ b/project/src/helpers/TraderHelper.ts @@ -272,7 +272,7 @@ export class TraderHelper public getTraderUpdateSeconds(traderId: string): number { 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.localisationService.getText("trader-missing_trader_details_using_default_refresh_time", { diff --git a/project/src/models/spt/dialog/ISendMessageDetails.ts b/project/src/models/spt/dialog/ISendMessageDetails.ts index 1f9f0d05..13d8957e 100644 --- a/project/src/models/spt/dialog/ISendMessageDetails.ts +++ b/project/src/models/spt/dialog/ISendMessageDetails.ts @@ -34,7 +34,17 @@ export interface ISendMessageDetails export interface IProfileChangeEvent { _id: string; - Type: "TraderSalesSum" | "TraderStanding" | "ProfileLevel" | "SkillPoints" | "ExamineAllItems" | "UnlockTrader"; + Type: ProfileChangeEventType; value: number; 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", +} diff --git a/project/src/services/MailSendService.ts b/project/src/services/MailSendService.ts index 2660d9c7..4fbc536a 100644 --- a/project/src/services/MailSendService.ts +++ b/project/src/services/MailSendService.ts @@ -10,7 +10,7 @@ import { Dialogue, IUserDialogInfo, Message, MessageItems } from "@spt-aki/model import { BaseClasses } from "@spt-aki/models/enums/BaseClasses"; import { MessageType } from "@spt-aki/models/enums/MessageType"; 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 { DatabaseServer } from "@spt-aki/servers/DatabaseServer"; import { SaveServer } from "@spt-aki/servers/SaveServer"; @@ -169,7 +169,8 @@ export class MailSendService sessionId: string, message: string, items: Item[] = [], - maxStorageTimeSeconds = null, + maxStorageTimeSeconds?: number, + profileChangeEvents?: IProfileChangeEvent[], ): void { const details: ISendMessageDetails = { @@ -185,6 +186,11 @@ export class MailSendService details.itemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800; // 48 hours if no value supplied } + if ((profileChangeEvents?.length ?? 0) > 0) + { + details.profileChangeEvents = profileChangeEvents; + } + this.sendMessageToPlayer(details); } @@ -199,8 +205,8 @@ export class MailSendService sessionId: string, messageLocaleId: string, items: Item[] = [], - profileChangeEvents = [], - maxStorageTimeSeconds = null, + profileChangeEvents?: IProfileChangeEvent[], + maxStorageTimeSeconds?: number, ): void { const details: ISendMessageDetails = { @@ -216,7 +222,7 @@ export class MailSendService details.itemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800; // 48 hours if no value supplied } - if (profileChangeEvents.length > 0) + if ((profileChangeEvents?.length ?? 0) > 0) { details.profileChangeEvents = profileChangeEvents; } diff --git a/project/src/services/PaymentService.ts b/project/src/services/PaymentService.ts index c4658a74..300197a4 100644 --- a/project/src/services/PaymentService.ts +++ b/project/src/services/PaymentService.ts @@ -65,7 +65,13 @@ export class PaymentService if (!this.paymentHelper.isMoneyTpl(item._tpl)) { // 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; } else