Compare commits

...

48 Commits

Author SHA1 Message Date
MSWS
9a6af7acab feat: Add M4A1Config support to storage and plugin modules +semver:minor
- Add support for M4A1Config storage in CS2ServiceCollection by including a new `AddModBehavior` for `IStorage<M4A1Config>`
- Implement a new configuration class for the M4A1 shop item in CS2M4A1Config with storage and plugin module interfaces
- Define static configuration variables with validators for price, clear slots, and associated weapons in CS2M4A1Config
- Implement plugin initialization methods and register fake config variables in CS2M4A1Config
- Load configuration in CS2M4A1Config, parsing clear slots and weapons from strings into arrays, with nullable type inference and asynchronous processing
2025-10-02 13:08:31 -07:00
MSWS
8cc241bcca Fix camo applying asynchronously 2025-10-02 13:05:12 -07:00
MSWS
cab156184c Tweak credit penalty 2025-10-02 11:04:09 -07:00
MSWS
9ee69a0b28 feat: Add credits given for killing / identifying +semver:minor
```
- Refactor balance deduction in Shop.cs to use `AddBalance` method for improved consistency and added player notification for successful purchases.
- Enhance BuyCommand.cs by updating Execute method to return Task.FromResult, adding a health check, and modifying item search logic for better accuracy.
- Update PlayerKillListener.cs to extend from BaseListener, integrate ShopAPI, and add methods for handling on-kill and body identification events with balance adjustments.
- Use JetBrains.Annotations in RoleAssignCreditor.cs for potential external or reflective method use, adding UsedImplicitly attribute for OnRoleAssign method.
- Extend ShopMsgs.cs and en.yml with new purchase success messages including item names, improving player feedback.
```
2025-10-02 11:03:03 -07:00
MSWS
e529229200 Update licenses 2025-10-01 23:46:52 -07:00
MSWS
e7dc5c02fe feat: Add camouflage item +semver:minor (resolves #73)
- Add `CamouflageItem` class for managing camouflage items in the game
- Implement `AddCamoServices` in service collection extension for mod behavior
- Introduce configuration loading for camouflage settings with `IStorage<CamoConfig>`
- Provide localization for item name and description
- Implement purchasing logic: visibility settings and ownership check in `CamouflageItem`
- Add "Camouflage" item and description to the language file
- Expand shop services to include camouflage features via `AddCamoServices`
- Establish `CamoConfig` in `ShopAPI` with default pricing and visibility properties
- Create `CamoMsgs` class for managing camouflage item messages
2025-10-01 23:34:05 -07:00
MSWS
522e42a5ff Reformat & Cleanup 2025-10-01 23:26:30 -07:00
MSWS
871500fbdc Cleanup unused lines 2025-10-01 23:21:39 -07:00
MSWS
f023d36aa9 Update CS2 impl 2025-10-01 23:10:58 -07:00
MSWS
a185b217e0 feat: Add traitor gloves item and reorganize configs. (resolves #81)
- Add new "Gloves" item to traitor shop with localization and descriptions
- Allow modification of `Killer` property in `IBody.cs` for enhanced gameplay flexibility
- Reorganize configuration files under relevant directories for better clarity (e.g., HealthStationConfig, DnaScannerConfig)
- Introduce `GlovesListener` class to handle event-driven interactions for the "Gloves" item
- Implement traitor-specific configurations and services for items like C4 and gloves, including default pricing and usage limits
2025-10-01 23:09:43 -07:00
MSWS
dbfd360c6c feat: Add M4A1 shop item +semver:minor (resolves #71)
```
Add M4A1 Item to Shop with Configuration and Localization

- Add TTT/Shop/Items/M4A1/M4A1Msgs.cs to manage localized messages for the M4A1 item.
- Update TTT/Shop/lang/en.yml with a new shop item "M4A1 Rifle and USP-S" and its description.
- Modify TTT/Shop/ShopServiceCollection.cs to include M4A1 services and adjust service ordering for better organization.
- Introduce TTT/ShopAPI/Configs/M4A1Config.cs for setting up the M4A1 item configuration, including price and weapon slots.
- Create TTT/Shop/Items/M4A1/M4A1ShopItem.cs to define the M4A1 shop item with purchase and inventory management logic.
```
2025-10-01 22:47:05 -07:00
MSWS
4a64741a8e Add CS2-specific configuration of C4 item 2025-10-01 22:20:14 -07:00
MSWS
7372ffda45 feat: Add C4 item for TraitorRole in shop +semver:minor (resolves #79)
- Add C4Msgs class for localization of C4 shop item in the Traitor category
- Introduce C4ShopItem class with methods for purchase, limitations, and role restriction handling
- Update ShopServiceCollection to include C4 service for Traitor role
- Introduce C4Config class for configuration of C4 shop items with various properties
- Add English localization entry for "C4 Explosive" shop item with description
2025-10-01 22:16:03 -07:00
MSWS
9ea9c78208 Remove extra spacing in DNA locale 2025-10-01 22:02:59 -07:00
MSWS
85601f1fc0 feat: Implement configurable sound and localization support
- Add configuration for `UseSound` property in `DamageStationConfig` and `HealthStationConfig`
- Implement localization support in `HealthStation.cs` by replacing hardcoded strings with localized messages
- Introduce `UseSound` abstract property in `StationConfig`
- Update localization files with new shop items and descriptions in `lang/en.yml`
- Create `StationMsgs.cs` to manage station-related item messages via a factory pattern
- Enhance `DamageStation.cs` for internationalization and configurable sound settings
- Annotate unused or implicitly used methods in `StickerListener.cs` with `[UsedImplicitly]`
2025-10-01 22:02:12 -07:00
MSWS
ddf52f057d Update from main 2025-10-01 21:50:19 -07:00
Isaac
c2ecba1847 Merge branch 'main' into dev 2025-10-01 20:39:01 -07:00
Isaac
354ccf2fbe Update TTT/Shop/Items/Detective/Stickers/Stickers.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-10-01 20:38:03 -07:00
Isaac
9d1a7f5618 feat: Integrate ShopAPI and add station item features +semver:minor (resolves #84) (#92)
```
- Integrate ShopAPI into various components including PlayerPurchaseItemEvent, DnaScanner, and multiple stations for improved shop functionalities.
- Introduce new HealthStationConfig and DamageStationConfig for configuring health and damage-related station items.
- Update CS2 project to target .NET 8.0 and enable advanced C# features, while removing outdated folder references for streamlined project structure.
- Correct namespace declarations across multiple files, enhancing consistency and organizational clarity within the codebase.
- Add new or updated commands and extensions, such as SetHealthCommand, to improve player health management and interaction.
- Enhance localization features by importing TTT.Game.lang across various roles, commands, and logging implementations.
- Incorporate new logic in DNA scanning and station interactions, including damage dealing and healing over time based on player proximity.
```
2025-10-01 19:13:36 -07:00
MSWS
ea62b312be Update unit tests 2025-10-01 19:12:10 -07:00
MSWS
6778531312 feat: Integrate ShopAPI and add station item features +semver:minor
```
- Integrate ShopAPI into various components including PlayerPurchaseItemEvent, DnaScanner, and multiple stations for improved shop functionalities.
- Introduce new HealthStationConfig and DamageStationConfig for configuring health and damage-related station items.
- Update CS2 project to target .NET 8.0 and enable advanced C# features, while removing outdated folder references for streamlined project structure.
- Correct namespace declarations across multiple files, enhancing consistency and organizational clarity within the codebase.
- Add new or updated commands and extensions, such as SetHealthCommand, to improve player health management and interaction.
- Enhance localization features by importing TTT.Game.lang across various roles, commands, and logging implementations.
- Incorporate new logic in DNA scanning and station interactions, including damage dealing and healing over time based on player proximity.
```
2025-10-01 19:08:30 -07:00
MSWS
8ab4328d9b Fix YML 2025-10-01 15:01:35 -07:00
MSWS
c5a91f334d refactor: Improve item not found handling and code readability
- Add a new message for item not found in `en.yml` to enhance user error feedback
- Clean up `GiveItemCommand.cs` by importing `TTT.Locale` and removing debug messages
- Improve `CS2AliveSpoofer.cs` readability with better conditional logic and early return handling
- Enhance `BuyCommand.cs` by removing redundant searches and utilizing localization for consistency
- Change `DecayTime` in `DnaScannerConfig.cs` from 3 minutes to 10 seconds for faster testing
2025-10-01 14:55:28 -07:00
MSWS
b2f4474e8f Reformat 2025-10-01 14:39:15 -07:00
Isaac
8aee59a87e Feat/dna (resolves #87) (#91) 2025-10-01 14:21:04 -07:00
MSWS
e27bddf8e2 feat: Implement enhanced DNA scanner messaging +semver:minor
- Clean up `ShopAPI.csproj` by removing redundant project references.
- Remove the DNA Scanner item and its description from the English language file in `Shop`.
- Streamline `DnaScanner.cs` by eliminating an unnecessary using directive.
- Enhance `DnaListener.cs` to improve messaging logic with role information and implement new message templating for DNA scans.
- Refactor `DnaMsgs.cs` by adjusting namespaces and adding message properties for enhanced DNA scan functionalities.
- Update `CS2.csproj` by removing a duplicate project reference.
- Expand `en.yml` in `CS2` with translations for DNA scanner items and messages for DNA scanning, including specific scenarios involving suicide.
2025-10-01 14:17:27 -07:00
MSWS
4514e9baa0 feat: Introduce DNA scanner functionality
- Extend functionality in `DnaListener.cs` by adding dependency injection and enhancing the `OnPropPickup` event handler with player and body checks.
- Add `TimeOfDeath` property to `CS2Body.cs` for improved tracking of body creation time.
- Introduce `DnaScannerServiceCollection` in `DnaScanner.cs` for better service management and mod behavior registration.
- Extend `IBody` interface with `TimeOfDeath` to track deceased player identification time.
- Update `ShopServiceCollection.cs` to integrate `AddDnaScannerServices`, enhancing shop capabilities with DNA scanner features.
2025-10-01 11:23:31 -07:00
MSWS
67755c36c6 Tweak AI prompt and input 2025-09-30 18:25:18 -07:00
MSWS
eaf1ab627e Begin work on adding CS2-specific items
```
- Rename and reorganize directories for DNA-related items to new path under "CS2/Items/DNA"
- Relocate RoleRestrictedItem.cs while maintaining its original functionality
- Move and update DnaListener.cs with a low-priority event handler and add an execution order comment
- Rename BaseItem.cs file path as part of project structure reorganization
- Update CS2.csproj by adding "Items\" folder and correcting duplicate project reference
```
2025-09-30 18:22:13 -07:00
MSWS
57bef00055 Refactor Shop api into its own project, separate from impl 2025-09-30 18:18:07 -07:00
MSWS
7dd6d4dd38 Finalize stickers (resolves #89) 2025-09-30 17:46:52 -07:00
MSWS
f1cce6c230 Fix windows-specific issues 2025-09-30 17:32:42 -07:00
MSWS
922f121009 refactor: Add IIconManager and Sticker features +semver:minor
```
- Rename property `Name` to `Id` across various commands and classes for consistency and clarity, affecting files like `Test/SetRoleCommand.cs`, `GameHandlers/KarmaSyncer.cs`, and `Command/Test/TestCommand.cs`.
- Add new interface `IIconManager` to manage player visibility with methods for handling up to 64 players using a bitmask in `API/Player/IIconManager.cs`.
- Introduce `ShowIconsCommand` and `IndexCommand` classes to enhance game command functionality, leveraging the new `IIconManager` for icon management.
- Implement a new shop item "Stickers" with associated classes `Stickers.cs`, `StickerListener.cs`, and `StickerMsgs.cs`, providing role-revealing capabilities for detective players.
- Refactor shop item and command structures to use a new `BaseItem` abstract class, enhancing code organization and inheritance patterns.
- Update logging in `Plugin/TTT.cs` to use `Id` instead of `Name` for module identification, standardizing log outputs.
- Adjust visibility and color duration settings in `Listeners/ScreenColorApplier.cs` for improved gameplay feedback.
- Refactor service registration and command handling to remove redundancies and improve icon manager integrations in files like `CS2ServiceCollection.cs` and `Command/Test/ScreenColorCommand.cs`.
```
2025-09-30 17:00:57 -07:00
MSWS
2a0924138f Tweak 1-shot weapon defaults 2025-09-30 13:03:30 -07:00
Isaac
a4dc781ee4 Feat/one shot revolver (resolves #66) (#90) 2025-09-30 11:15:24 -07:00
MSWS
935b430769 refactor: Enhance purchase messaging system with localization
- Add new localized messages for shop interactions in `TTT/Shop/lang/en.yml`
- Implement `PurchaseResultExtensions` in `TTT/Shop/Shop/PurchaseResult.cs` to translate purchase outcomes to user-friendly messages
- Streamline and optimize purchase process in `TTT/Shop/Commands/BuyCommand.cs`
- Localize error messages and improve test setup in `TTT/Test/Shop/Commands/BuyTest.cs`
- Update `TTT/Shop/Shop.cs` to use localized messages and enhance error handling logic
2025-09-30 11:13:25 -07:00
MSWS
9dd4414733 Tweak restrictions on deagle config 2025-09-30 10:59:04 -07:00
MSWS
dce4edd6a4 feat: Make friendly fire configurable
```
- Introduce a new configuration variable in `CS2OneShotDeagleConfig.cs` to determine if the shooter should be killed upon friendly fire, and update the `Load` method accordingly.
- Add a static message `SHOP_ITEM_DEAGLE_HIT_FF` in `DeagleMsgs.cs` for handling new Deagle functionality messages.
- Rename "One-Hit Deagle" to "One-Hit Revolver" in `en.yml` and update description and messages for consistency.
- Refactor friendly fire logic in `DeagleDamageListener.cs` by integrating nested conditions and simplifying weapon verification logic for damage events.
- Add `KillShooterOnFF` configuration option in `OneShotDeagle.cs` to manage shooter consequences on friendly fire.
```
2025-09-30 10:57:22 -07:00
MSWS
324711acb9 Working deagle impl +semver:minor 2025-09-30 10:34:47 -07:00
MSWS
85ae2c4210 Add reverse cache to CCPlayerConverter 2025-09-30 09:52:12 -07:00
Isaac
2e6743c25d Miscelleaneous Tweaks
<p dir="auto">This pull request appears to be a development branch merge
 that implements several enhancements and fixes to the TTT (Trouble in 
Terrorist Town) game system. The changes focus on improving weapon 
handling, adding new test coverage, and enhancing player visual
effects.</p>
<ul dir="auto">
<li>Refactors weapon API by renaming <code class="notranslate">Id</code>
property to <code class="notranslate">WeaponId</code> for better
clarity</li>
<li>Implements comprehensive shop and weapon testing infrastructure</li>
<li>Adds screen color effects and player visual enhancements</li>
</ul>
<h3 dir="auto">Reviewed Changes</h3>
<p dir="auto">Copilot reviewed 32 out of 32 changed files in this pull
request and generated 2 comments.</p>
<details open="">
<summary>Show a summary per file</summary>
<markdown-accessiblity-table data-catalyst="">
File | Description
-- | --
TTT/API/IWeapon.cs | Renames weapon identifier property from Id to
WeaponId
TTT/Test/Shop/ShopTests.cs | Adds comprehensive test coverage for shop
functionality
TTT/Test/Shop/Items/DeagleTests.cs | Implements tests for one-shot
deagle weapon behavior
TTT/Test/TestPlayer.cs | Enhances test player with computed IsAlive
property
TTT/CS2/Player/CS2InventoryManager.cs | Adds weapon slot management and
refactors weapon handling
TTT/CS2/Extensions/PlayerExtensions.cs | Implements screen color fade
effects for players
TTT/CS2/Listeners/ScreenColorApplier.cs | Adds role-based screen color
feedback
TTT/Game/Roles/BaseWeapon.cs | Updates weapon class to use WeaponId
property
TTT/Shop/Items/OneShotDeagle/OneShotDeagle.cs | Updates deagle
implementation for new weapon API

</markdown-accessiblity-table></details>
2025-09-28 01:32:07 -07:00
Isaac
5ff27b37e5 Merge branch 'main' into dev 2025-09-28 01:29:38 -07:00
MSWS
1a4e5e3e77 +semver:minor 2025-09-28 01:24:45 -07:00
MSWS
721504f612 Debug out workflow +semver:patch 2025-09-28 01:19:57 -07:00
MSWS
86c24533b5 Test bumping +semver:patch 2025-09-28 01:15:39 -07:00
Isaac
eba49139c2 ci: Implement AI-driven changelog rewriting workflow +ratio (#59)
- Add environment variables and steps for OpenAI API usage in
`.github/workflows/release.yml`
- Retain raw changelog on AI rewrite failure and differentiate naming
- Introduce conditional logic for selective changelog rewriting
- Update GitHub release creation to utilize AI-rewritten changelog when
available
2025-09-28 01:05:24 -07:00
MSWS
d33550a5a4 ci: Enhance release workflow and changelog generation
- Add `fetch-tags: true` to actions/checkout in release workflow to ensure all tags are fetched during checkout.
- Improve tag determination process with lineage-aware strategy and refined pattern matching.
- Change changelog generation to use local git log for better control over commit messages.
- Enhance logic for finding commits for changelog, specifically handling first and subsequent releases.
- Improve error handling and retry mechanism for OpenAI API calls, and refine changelog rewrite logic with fallback strategies.
- Update comment styles for better clarity and organization.
2025-09-28 01:03:57 -07:00
MSWS
453ce77711 ci: Implement AI-driven changelog rewriting workflow +ratio
- Add environment variables and steps for OpenAI API usage in `.github/workflows/release.yml`
- Retain raw changelog on AI rewrite failure and differentiate naming
- Introduce conditional logic for selective changelog rewriting
- Update GitHub release creation to utilize AI-rewritten changelog when available
2025-09-28 00:58:21 -07:00
132 changed files with 2633 additions and 464 deletions

View File

@@ -12,10 +12,18 @@ permissions:
jobs:
auto-release:
runs-on: ubuntu-latest
env:
# Tweak these if you want a different model or style
OPENAI_MODEL: gpt-4o-mini
OPENAI_TEMPERATURE: "0.2"
# Safety: cap how many characters we feed to the model
MAX_CHANGELOG_CHARS: "50000"
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
fetch-tags: true
# 1. Calculate version using GitVersion
- name: Install GitVersion
@@ -48,7 +56,7 @@ jobs:
run: |
cd build/TTT
zip -r TTT-${{ steps.gitversion.outputs.fullSemVer }}.zip *
# 2. Get latest tag
- name: Get latest tag
id: latest_tag
@@ -68,27 +76,131 @@ jobs:
git tag ${{ steps.gitversion.outputs.fullSemVer }}
git push origin ${{ steps.gitversion.outputs.fullSemVer }}
# 4. Determine previous tag for changelog
# 4. Determine previous relevant tag (lineage-aware)
- name: Determine previous relevant tag
id: prev_tag
run: |
set -euo pipefail
branch="${GITHUB_REF_NAME}"
if [[ "$branch" == "main" ]]; then
prev=$(git tag --sort=-creatordate | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sed -n 2p)
else
prev=$(git tag --sort=-creatordate | grep -E '^[0-9]+\.[0-9]+\.[0-9]+-' | sed -n 2p)
fi
echo "tag=${prev:-0.0.0}" >> $GITHUB_OUTPUT
# 5. Generate changelog
# Use HEAD^ to skip the tag we just created. If no parent, fall back to HEAD.
if git rev-parse --verify -q HEAD^ >/dev/null; then
base_rev="HEAD^"
else
base_rev="HEAD"
fi
# Match stable tags on main and prerelease tags on non-main
if [[ "$branch" == "main" ]]; then
pattern='[0-9]*.[0-9]*.[0-9]*'
else
pattern='[0-9]*.[0-9]*.[0-9]*-*'
fi
# Nearest tag reachable on this lineage, not just "second most recent by date"
prev=$(git describe --tags --abbrev=0 --match "$pattern" --tags "$base_rev" 2>/dev/null || true)
echo "tag=${prev:-0.0.0}" >> "$GITHUB_OUTPUT"
# 5. Generate changelog using local git (no compare API)
- name: Generate changelog
run: |
gh api repos/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.tag }}...${{ steps.gitversion.outputs.fullSemVer }} \
--jq '.commits[].commit.message' > CHANGELOG.md
env:
GH_TOKEN: ${{ github.token }}
set -euo pipefail
# 6. Create release
prev="${{ steps.prev_tag.outputs.tag }}"
curr="${{ steps.gitversion.outputs.fullSemVer }}"
# Choose what you want in the raw feed: %s = subject only, %B = full message
GIT_LOG_FORMAT='%B'
if [[ "$prev" == "0.0.0" ]]; then
# First release: whole history to this tag, first-parent to reflect mains narrative
git log --no-merges --format="${GIT_LOG_FORMAT}" --reverse "$curr" > CHANGELOG.md
else
# Strict range between the previous reachable tag and the new tag on this lineage
git log --no-merges --format="${GIT_LOG_FORMAT}" --reverse "$prev..$curr" > CHANGELOG.md
fi
# Fallback in case nothing was captured
if [[ ! -s CHANGELOG.md ]]; then
echo "No commits found between $prev and $curr on first-parent. Using full messages without first-parent filter." >&2
if [[ "$prev" == "0.0.0" ]]; then
git log --no-merges --format="${GIT_LOG_FORMAT}" --reverse "$curr" > CHANGELOG.md
else
git log --no-merges --format="${GIT_LOG_FORMAT}" --reverse "$prev..$curr" > CHANGELOG.md
fi
fi
cat CHANGELOG.md
# 5b. Rewrite changelog with OpenAI
- name: Rewrite changelog with OpenAI
id: ai_changelog
if: success()
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_MODEL: ${{ env.OPENAI_MODEL }}
OPENAI_TEMPERATURE: ${{ env.OPENAI_TEMPERATURE }}
MAX_CHANGELOG_CHARS: ${{ env.MAX_CHANGELOG_CHARS }}
run: |
set -euo pipefail
# Ensure we have a changelog to work with
if [[ ! -s CHANGELOG.md ]]; then
echo "CHANGELOG.md is empty. Skipping AI rewrite."
echo "skipped=true" >> $GITHUB_OUTPUT
exit 0
fi
# Trim the input to a safe size for token limits
head -c "${MAX_CHANGELOG_CHARS}" CHANGELOG.md > CHANGELOG_RAW.md
# Build the JSON body. We feed system guidance and the raw changelog
# See OpenAI Responses API docs for the schema and output_text helper. :contentReference[oaicite:0]{index=0}
jq -Rs --arg sys "You are an expert release-notes writer. Given a list of changes in various formats (e.g: commits, merges, etc.), write Release notes, grouping by features, features, and other pertinent groups where appropriate. Do not include a group if it is not necessary / populated. Remove internal ticket IDs and commit hashes unless essential. Merge duplicates. Use imperative, past tense voice voice. Output valid Markdown only." \
--arg temp "${OPENAI_TEMPERATURE}" \
--arg model "${OPENAI_MODEL}" \
'{model:$model, temperature: ($temp|tonumber), input:[{role:"system", content:$sys},{role:"user", content:.}]}' CHANGELOG_RAW.md > request.json
# Call the API
# Basic retry on transient failures
for i in 1 2 3; do
HTTP_CODE=$(curl -sS -w "%{http_code}" -o ai_response.json \
https://api.openai.com/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
--data-binary @request.json) && break || true
echo "Call attempt $i failed with HTTP $HTTP_CODE"
sleep $((i*i))
done
if [[ "${HTTP_CODE:-000}" -lt 200 || "${HTTP_CODE:-000}" -ge 300 ]]; then
echo "OpenAI API call failed with HTTP $HTTP_CODE. Keeping raw changelog."
echo "skipped=true" >> $GITHUB_OUTPUT
exit 0
fi
# Prefer output_text if present. Fallback to first text item. :contentReference[oaicite:1]{index=1}
if jq -e '.output_text' ai_response.json >/dev/null; then
jq -r '.output_text' ai_response.json > CHANGELOG.md
else
jq -r '.output[0].content[] | select(.type=="output_text") | .text' ai_response.json | sed '/^[[:space:]]*$/d' > CHANGELOG.md
fi
# If the rewrite somehow produced an empty file, keep the raw one
if [[ ! -s CHANGELOG.md ]]; then
echo "AI returned empty content. Restoring raw changelog."
mv CHANGELOG_RAW.md CHANGELOG.md
echo "skipped=true" >> $GITHUB_OUTPUT
exit 0
fi
echo "skipped=false" >> $GITHUB_OUTPUT
echo "Rewritten changelog:"
cat CHANGELOG.md
# 6. Create release using the (possibly rewritten) changelog
- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:

View File

@@ -1,15 +1,18 @@
| Package | Version | License Information Origin | License Expression | License Url | Copyright | Authors | Package Project Url |
| ----------------------------------------------------- | -------- | -------------------------- | ------------------ | --------------------------------------- | ----------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------ |
| CounterStrikeSharp.API | 1.0.332 | Expression | GPL-3.0-only | https://licenses.nuget.org/GPL-3.0-only | | Roflmuffin | http://docs.cssharp.dev/ |
| CounterStrikeSharp.API | 1.0.340 | Expression | GPL-3.0-only | https://licenses.nuget.org/GPL-3.0-only | | Roflmuffin | http://docs.cssharp.dev/ |
| JetBrains.Annotations | 2025.2.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) 2016-2025 JetBrains s.r.o. | JetBrains | https://www.jetbrains.com/help/resharper/Code_Analysis__Code_Annotations.html |
| Microsoft.Extensions.DependencyInjection.Abstractions | 9.0.7 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://dot.net/ |
| Microsoft.Extensions.Localization.Abstractions | 8.0.3 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://asp.net/ |
| Microsoft.NET.Test.Sdk | 17.14.1 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://github.com/microsoft/vstest |
| Microsoft.Reactive.Testing | 6.0.1 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) .NET Foundation and Contributors. | .NET Foundation and Contributors | https://github.com/dotnet/reactive |
| Microsoft.Testing.Extensions.CodeCoverage | 17.14.2 | Unknown | | https://aka.ms/deprecateLicenseUrl | © Microsoft Corporation. All rights reserved. | Microsoft | https://github.com/microsoft/codecoverage |
| System.Reactive | 6.0.1 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) .NET Foundation and Contributors. | .NET Foundation and Contributors | https://github.com/dotnet/reactive |
| Xunit.DependencyInjection | 10.6.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright © 2019 | Wei Peng | https://github.com/pengweiqhca/Xunit.DependencyInjection/tree/main/src/Xunit.DependencyInjection |
| xunit.runner.visualstudio | 3.1.3 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | Copyright (C) .NET Foundation | jnewkirk,bradwilson | |
| xunit.v3 | 3.0.0 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | Copyright (C) .NET Foundation | jnewkirk,bradwilson | |
| YamlDotNet | 16.3.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) Antoine Aubry and contributors | Antoine Aubry | https://github.com/aaubry/YamlDotNet/wiki |
| Package | Version | License Information Origin | License Expression | License Url | Copyright | Authors | Package Project Url |
| ----------------------------------------------------- | -------- | -------------------------- | ------------------ | --------------------------------------- | ----------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------ |
| CounterStrikeSharp.API | 1.0.332 | Expression | GPL-3.0-only | https://licenses.nuget.org/GPL-3.0-only | | Roflmuffin | http://docs.cssharp.dev/ |
| CounterStrikeSharp.API | 1.0.340 | Expression | GPL-3.0-only | https://licenses.nuget.org/GPL-3.0-only | | Roflmuffin | http://docs.cssharp.dev/ |
| Dapper | 2.1.66 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | 2019 Stack Exchange, Inc. | Sam Saffron,Marc Gravell,Nick Craver | https://github.com/DapperLib/Dapper |
| JetBrains.Annotations | 2025.2.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) 2016-2025 JetBrains s.r.o. | JetBrains | https://www.jetbrains.com/help/resharper/Code_Analysis__Code_Annotations.html |
| Microsoft.Extensions.DependencyInjection.Abstractions | 9.0.7 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://dot.net/ |
| Microsoft.Extensions.Localization.Abstractions | 8.0.3 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://asp.net/ |
| Microsoft.NET.Test.Sdk | 17.14.1 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://github.com/microsoft/vstest |
| Microsoft.Reactive.Testing | 6.0.1 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) .NET Foundation and Contributors. | .NET Foundation and Contributors | https://github.com/dotnet/reactive |
| Microsoft.Testing.Extensions.CodeCoverage | 17.14.2 | Unknown | | https://aka.ms/deprecateLicenseUrl | © Microsoft Corporation. All rights reserved. | Microsoft | https://github.com/microsoft/codecoverage |
| MySqlConnector | 2.4.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright 20162024 Bradley Grainger | Bradley Grainger | https://mysqlconnector.net/ |
| System.Reactive | 6.0.1 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) .NET Foundation and Contributors. | .NET Foundation and Contributors | https://github.com/dotnet/reactive |
| System.Text.Json | 8.0.5 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://dot.net/ |
| Xunit.DependencyInjection | 10.6.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright © 2019 | Wei Peng | https://github.com/pengweiqhca/Xunit.DependencyInjection/tree/main/src/Xunit.DependencyInjection |
| xunit.runner.visualstudio | 3.1.3 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | Copyright (C) .NET Foundation | jnewkirk,bradwilson | |
| xunit.v3 | 3.0.0 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | Copyright (C) .NET Foundation | jnewkirk,bradwilson | |
| YamlDotNet | 16.3.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) Antoine Aubry and contributors | Antoine Aubry | https://github.com/aaubry/YamlDotNet/wiki |

View File

@@ -11,7 +11,7 @@
<ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.332"/>
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="8.0.3"/>
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="System.Text.Json" Version="8.0.5"/>
<PackageReference Include="YamlDotNet" Version="16.3.0"/>
</ItemGroup>

View File

@@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shop", "TTT\Shop\Shop.cspro
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Karma", "TTT\Karma\Karma.csproj", "{AFC791EC-750C-423F-9F35-87636657E990}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShopAPI", "TTT\ShopAPI\ShopAPI.csproj", "{16F720B5-9D45-47BF-8C80-4F91005E36D1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -66,6 +68,10 @@ Global
{AFC791EC-750C-423F-9F35-87636657E990}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AFC791EC-750C-423F-9F35-87636657E990}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AFC791EC-750C-423F-9F35-87636657E990}.Release|Any CPU.Build.0 = Release|Any CPU
{16F720B5-9D45-47BF-8C80-4F91005E36D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{16F720B5-9D45-47BF-8C80-4F91005E36D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{16F720B5-9D45-47BF-8C80-4F91005E36D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{16F720B5-9D45-47BF-8C80-4F91005E36D1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection

View File

@@ -6,6 +6,6 @@
<RootNamespace>TTT.API</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="System.Text.Json" Version="8.0.5"/>
</ItemGroup>
</Project>

View File

@@ -7,7 +7,7 @@ public interface ICommand : ITerrorModule {
string[] Usage => [];
string[] RequiredFlags => [];
string[] RequiredGroups => [];
string[] Aliases => [Name];
string[] Aliases => [Id];
Task<CommandResult> Execute(IOnlinePlayer? executor, ICommandInfo info);
}

View File

@@ -12,20 +12,17 @@ public static class ServiceCollectionExtensions {
public static void AddModBehavior<TExtension>(
this IServiceCollection collection)
where TExtension : class, ITerrorModule {
if (typeof(TExtension).IsAssignableTo(typeof(IPluginModule))) {
if (typeof(TExtension).IsAssignableTo(typeof(IPluginModule)))
collection.AddTransient<IPluginModule>(provider
=> (provider.GetRequiredService<TExtension>() as IPluginModule)!);
}
if (typeof(TExtension).IsAssignableTo(typeof(IListener))) {
if (typeof(TExtension).IsAssignableTo(typeof(IListener)))
collection.AddTransient<IListener>(provider
=> (provider.GetRequiredService<TExtension>() as IListener)!);
}
if (typeof(TExtension).IsAssignableTo(typeof(ICommand))) {
if (typeof(TExtension).IsAssignableTo(typeof(ICommand)))
collection.AddTransient<ICommand>(provider
=> (provider.GetRequiredService<TExtension>() as ICommand)!);
}
collection.AddScoped<TExtension>();

View File

@@ -1,7 +1,7 @@
namespace TTT.API;
public interface ITerrorModule : IDisposable {
string Name => GetType().Name;
string Id => GetType().Name;
string Version => GitVersionInformation.FullSemVer;
void Start();

View File

@@ -0,0 +1,16 @@
namespace TTT.API.Player;
/// <summary>
/// Assumes a maximum of 64 players.
/// Each bit in the bitmask represents whether a player is visible to the client.
/// Bit 0 is unused, bit 1 represents player 1, bit 2 represents player 2, and so on.
/// </summary>
public interface IIconManager {
ulong GetVisiblePlayers(int client);
void SetVisiblePlayers(int client, ulong playersBitmask);
void RevealToAll(int client);
void AddVisiblePlayer(int client, int player);
void RemoveVisiblePlayer(int client, int player);
void ClearAllVisibility();
}

View File

@@ -6,24 +6,24 @@ public interface IInventoryManager {
/// </summary>
/// <param name="player">The player to give the weapon to.</param>
/// <param name="weapon"></param>
void GiveWeapon(IOnlinePlayer player, IWeapon weapon);
Task GiveWeapon(IOnlinePlayer player, IWeapon weapon);
/// <summary>
/// Removes a weapon from the player.
/// </summary>
/// <param name="player">The player to remove the weapon from.</param>
/// <param name="weaponId">The ID of the weapon to remove.</param>
void RemoveWeapon(IOnlinePlayer player, string weaponId);
Task RemoveWeapon(IOnlinePlayer player, string weaponId);
void RemoveWeapon(IOnlinePlayer player, IWeapon weapon) {
RemoveWeapon(player, weapon.WeaponId);
Task RemoveWeapon(IOnlinePlayer player, IWeapon weapon) {
return RemoveWeapon(player, weapon.WeaponId);
}
void RemoveWeaponInSlot(IOnlinePlayer player, int slot);
Task RemoveWeaponInSlot(IOnlinePlayer player, int slot);
/// <summary>
/// Removes all weapons from the player.
/// </summary>
/// <param name="player">The player to remove all weapons from.</param>
void RemoveAllWeapons(IOnlinePlayer player);
Task RemoveAllWeapons(IOnlinePlayer player);
}

View File

@@ -12,7 +12,7 @@
<ProjectReference Include="..\API\API.csproj"/>
<ProjectReference Include="..\Game\Game.csproj"/>
<ProjectReference Include="..\Karma\Karma.csproj"/>
<ProjectReference Include="..\Shop\Shop.csproj"/>
<ProjectReference Include="..\ShopAPI\ShopAPI.csproj"/>
</ItemGroup>
<ItemGroup>

View File

@@ -1,36 +1,19 @@
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game;
using TTT.Game.Roles;
using TTT.Locale;
namespace TTT.CS2;
public class CS2Body(IServiceProvider provider, CRagdollProp ragdoll,
IPlayer player) : IBody {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IMsgLocalizer locale =
provider.GetRequiredService<IMsgLocalizer>();
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
public class CS2Body(CRagdollProp ragdoll, IPlayer player) : IBody {
public CRagdollProp Ragdoll { get; } = ragdoll;
public IPlayer OfPlayer { get; } = player;
public bool IsIdentified { get; set; }
public IWeapon? MurderWeapon { get; private set; }
public IPlayer? Killer { get; private set; }
public IPlayer? Killer { get; set; }
public string Id { get; } = ragdoll.Index.ToString();
public DateTime TimeOfDeath { get; } = DateTime.Now;
public CS2Body WithWeapon(IWeapon weapon) {
MurderWeapon = weapon;

View File

@@ -1,5 +1,7 @@
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
using TTT.API.Command;
using TTT.API.Extensions;
using TTT.API.Game;
@@ -20,8 +22,6 @@ using TTT.CS2.Listeners;
using TTT.CS2.Player;
using TTT.Game;
using TTT.Locale;
using TTT.Shop;
using TTT.Shop.Items;
namespace TTT.CS2;
@@ -33,12 +33,15 @@ public static class CS2ServiceCollection {
CCPlayerConverter>();
collection.AddModBehavior<ICommandManager, CS2CommandManager>();
collection.AddModBehavior<IAliveSpoofer, CS2AliveSpoofer>();
collection.AddModBehavior<IIconManager, RoleIconsHandler>();
// Configs
collection.AddModBehavior<IStorage<TTTConfig>, CS2GameConfig>();
collection.AddModBehavior<IStorage<ShopConfig>, CS2ShopConfig>();
collection
.AddModBehavior<IStorage<OneShotDeagleConfig>, CS2OneShotDeagleConfig>();
collection.AddModBehavior<IStorage<C4Config>, CS2C4Config>();
collection.AddModBehavior<IStorage<M4A1Config>, CS2M4A1Config>();
// TTT - CS2 Specific optionals
collection.AddScoped<ITextSpawner, TextSpawner>();
@@ -49,7 +52,6 @@ public static class CS2ServiceCollection {
collection.AddModBehavior<DamageCanceler>();
collection.AddModBehavior<PlayerConnectionsHandler>();
collection.AddModBehavior<PropMover>();
collection.AddModBehavior<RoleIconsHandler>();
collection.AddModBehavior<RoundEnd_GameEndHandler>();
collection.AddModBehavior<RoundStart_GameStartHandler>();

View File

@@ -5,8 +5,8 @@ using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Command;
using TTT.API.Player;
using TTT.Game;
using TTT.Game.Commands;
using TTT.Game.lang;
namespace TTT.CS2.Command;

View File

@@ -12,7 +12,7 @@ public class ForceAliveCommand(IServiceProvider provider) : ICommand {
public void Dispose() { }
public string Name => "forcealive";
public string Id => "forcealive";
public void Start() { }

View File

@@ -0,0 +1,50 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Command;
using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class GiveItemCommand(IServiceProvider provider) : ICommand {
private readonly IShop shop = provider.GetRequiredService<IShop>();
public void Dispose() { }
public void Start() { }
public string Id => "giveitem";
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
if (info.ArgCount == 1) return Task.FromResult(CommandResult.PRINT_USAGE);
var query = string.Join(" ", info.Args.Skip(1));
var item = searchItem(query);
if (item == null) {
info.ReplySync($"Item '{query}' not found.");
return Task.FromResult(CommandResult.ERROR);
}
shop.GiveItem(executor, item);
info.ReplySync($"Gave item '{item.Name}' to {executor.Name}.");
return Task.FromResult(CommandResult.SUCCESS);
}
private IShopItem? searchItem(string query) {
var item = shop.Items.FirstOrDefault(it
=> it.Name.Equals(query, StringComparison.OrdinalIgnoreCase));
if (item != null) return item;
item = shop.Items.FirstOrDefault(it
=> it.Name.Equals(query, StringComparison.OrdinalIgnoreCase));
if (item != null) return item;
item = shop.Items.FirstOrDefault(it
=> it.Name.Contains(query, StringComparison.OrdinalIgnoreCase));
return item;
}
}

View File

@@ -14,7 +14,7 @@ public class IdentifyAllCommand(IServiceProvider provider) : ICommand {
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
public string Name => "identifyall";
public string Id => "identifyall";
public void Dispose() { }
public void Start() { }

View File

@@ -0,0 +1,29 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class IndexCommand(IServiceProvider provider) : ICommand {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public string Id => "index";
public void Dispose() { }
public void Start() { }
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
Server.NextWorldUpdate(() => {
foreach (var player in Utilities.GetPlayers())
info.ReplySync($"{player.PlayerName} - {player.Slot}");
});
return Task.FromResult(CommandResult.SUCCESS);
}
}

View File

@@ -12,7 +12,7 @@ public class ScreenColorCommand(IServiceProvider provider) : ICommand {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public string Name => "screencolor";
public string Id => "screencolor";
public void Dispose() { }
public void Start() { }

View File

@@ -0,0 +1,27 @@
using TTT.API.Command;
using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class SetHealthCommand : ICommand {
public string Id => "sethealth";
public void Dispose() { }
public void Start() { }
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
if (info.ArgCount != 2) return Task.FromResult(CommandResult.PRINT_USAGE);
if (!int.TryParse(info.Args[1], out var health)) {
info.ReplySync("Invalid health value.");
return Task.FromResult(CommandResult.ERROR);
}
executor.Health = health;
info.ReplySync($"Set health of {executor.Name} to {health}.");
return Task.FromResult(CommandResult.SUCCESS);
}
}

View File

@@ -17,7 +17,7 @@ public class SetRoleCommand(IServiceProvider provider) : ICommand {
public void Dispose() { }
public string Name => "setrole";
public string Id => "setrole";
public void Start() { }
public Task<CommandResult>

View File

@@ -0,0 +1,27 @@
using CounterStrikeSharp.API;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class ShowIconsCommand(IServiceProvider provider) : ICommand {
private readonly IIconManager icons =
provider.GetRequiredService<IIconManager>();
public string Id => "showicons";
public void Dispose() { }
public void Start() { }
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
Server.NextWorldUpdate(() => {
for (var i = 0; i < Server.MaxPlayers; i++)
icons.SetVisiblePlayers(i, ulong.MaxValue);
});
info.ReplySync("Set all icons visible");
return Task.FromResult(CommandResult.SUCCESS);
}
}

View File

@@ -9,7 +9,7 @@ public class StateCommand(IServiceProvider provider) : ICommand {
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
public string Name => "state";
public string Id => "state";
public void Dispose() { }
public void Start() { }

View File

@@ -11,7 +11,7 @@ public class StopCommand(IServiceProvider provider) : ICommand {
provider.GetRequiredService<IGameManager>();
public void Dispose() { }
public string Name => "stop";
public string Id => "stop";
public void Start() { }

View File

@@ -11,7 +11,7 @@ public class TestCommand(IServiceProvider provider) : ICommand, IPluginModule {
public void Dispose() { }
public string Name => "test";
public string Id => "test";
public void Start() {
subCommands.Add("setrole", new SetRoleCommand(provider));
@@ -20,6 +20,10 @@ public class TestCommand(IServiceProvider provider) : ICommand, IPluginModule {
subCommands.Add("identifyall", new IdentifyAllCommand(provider));
subCommands.Add("state", new StateCommand(provider));
subCommands.Add("screencolor", new ScreenColorCommand(provider));
subCommands.Add("giveitem", new GiveItemCommand(provider));
subCommands.Add("index", new IndexCommand(provider));
subCommands.Add("showicons", new ShowIconsCommand(provider));
subCommands.Add("sethealth", new SetHealthCommand());
}
public Task<CommandResult>
@@ -29,7 +33,7 @@ public class TestCommand(IServiceProvider provider) : ICommand, IPluginModule {
if (info.ArgCount == 1) {
foreach (var c in subCommands.Values)
info.ReplySync(
$"- {c.Name} {c.Usage.FirstOrDefault()}: {c.Description ?? "No description provided."}");
$"- {c.Id} {c.Usage.FirstOrDefault()}: {c.Description ?? "No description provided."}");
return Task.FromResult(CommandResult.INVALID_ARGS);
}

View File

@@ -7,7 +7,7 @@ using TTT.API.Storage;
using TTT.CS2.Validators;
using TTT.Game;
namespace TTT.CS2.Game;
namespace TTT.CS2.Configs;
public class CS2GameConfig : IStorage<TTTConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_ROUND_COUNTDOWN = new(

View File

@@ -2,9 +2,9 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs;
using TTT.API;
using TTT.API.Storage;
using TTT.Shop;
namespace TTT.CS2.Configs;

View File

@@ -0,0 +1,61 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs.Traitor;
using TTT.API;
using TTT.API.Storage;
using TTT.CS2.Validators;
namespace TTT.CS2.Configs.ShopItems;
public class CS2C4Config : IStorage<C4Config>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new("css_ttt_shop_c4_price",
"Price of the C4 item", 140, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<string> CV_WEAPON = new(
"css_ttt_shop_c4_weapon", "Weapon entity name used for the C4", "weapon_c4",
ConVarFlags.FCVAR_NONE, new ItemValidator(allowMultiple: false));
public static readonly FakeConVar<int> CV_MAX_PER_ROUND = new(
"css_ttt_shop_c4_max_per_round",
"Maximum number of C4 that can be purchased per round", 0,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 64));
public static readonly FakeConVar<int> CV_MAX_AT_ONCE = new(
"css_ttt_shop_c4_max_at_once",
"Maximum number of C4 that can be active at once", 1,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 64));
public static readonly FakeConVar<float> CV_POWER = new(
"css_ttt_shop_c4_power", "Explosion power (damage multiplier) of the C4",
100f, ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0f, 10000f));
public static readonly FakeConVar<int> CV_FUSE_TIME = new(
"css_ttt_shop_c4_fuse_time", "Fuse time of the C4 in seconds", 30,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 300));
public static readonly FakeConVar<bool> CV_FRIENDLY_FIRE = new(
"css_ttt_shop_c4_ff", "Whether the C4 damages teammates");
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) { plugin?.RegisterFakeConVars(this); }
public Task<C4Config?> Load() {
var cfg = new C4Config {
Price = CV_PRICE.Value,
Weapon = CV_WEAPON.Value,
MaxC4PerRound = CV_MAX_PER_ROUND.Value,
MaxC4AtOnce = CV_MAX_AT_ONCE.Value,
Power = CV_POWER.Value,
FuseTime = TimeSpan.FromSeconds(CV_FUSE_TIME.Value),
FriendlyFire = CV_FRIENDLY_FIRE.Value
};
return Task.FromResult<C4Config?>(cfg);
}
}

View File

@@ -0,0 +1,54 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs;
using TTT.API;
using TTT.API.Storage;
using TTT.CS2.Validators;
namespace TTT.CS2.Configs.ShopItems;
public class CS2M4A1Config : IStorage<M4A1Config>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_m4a1_price", "Price of the M4A1 item", 90,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<string> CV_CLEAR_SLOTS = new(
"css_ttt_shop_m4a1_clear_slots",
"Slots to clear when granting M4A1 (comma-separated ints)", "0,1");
public static readonly FakeConVar<string> CV_WEAPONS = new(
"css_ttt_shop_m4a1_weapons",
"Weapons granted with this item (comma-separated names)",
"weapon_m4a1,weapon_usp_silencer", ConVarFlags.FCVAR_NONE,
new ItemValidator(allowMultiple: true));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<M4A1Config?> Load() {
var slots = CV_CLEAR_SLOTS.Value.Split(',')
.Select(s => s.Trim())
.Where(s => int.TryParse(s, out _))
.Select(int.Parse)
.ToArray();
var weapons = CV_WEAPONS.Value.Split(',')
.Select(s => s.Trim())
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
var cfg = new M4A1Config {
Price = CV_PRICE.Value, ClearSlots = slots, Weapons = weapons
};
return Task.FromResult<M4A1Config?>(cfg);
}
}

View File

@@ -2,42 +2,44 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs;
using TTT.API;
using TTT.API.Storage;
using TTT.CS2.Validators;
using TTT.Shop.Items;
namespace TTT.CS2.Configs.ShopItems;
public class CS2OneShotDeagleConfig : IStorage<OneShotDeagleConfig>,
IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_onedeagle_price", "Price of the One-Shot Deagle item", 100,
"css_ttt_shop_onedeagle_price", "Price of the One-Shot Deagle item", 120,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<bool> CV_FRIENDLY_FIRE = new(
"css_ttt_shop_onedeagle_ff",
"Whether the One-Shot Deagle damages teammates", true);
"Whether the One-Shot Deagle damages teammates");
public static readonly FakeConVar<bool> CV_KILL_SHOOTER_ON_FF = new(
"css_ttt_shop_onedeagle_kill_shooter_on_ff",
"Whether the shooter is killed if they shoot a teammate", true);
public static readonly FakeConVar<string> CV_WEAPON = new(
"css_ttt_shop_onedeagle_weapon",
"Weapon entity name used for the One-Shot Deagle", "weapon_revolver",
ConVarFlags.FCVAR_NONE, new ItemValidator(allowMultiple: false));
"Weapon entity name used for the One-Shot Weapon", "weapon_revolver",
ConVarFlags.FCVAR_NONE, new ItemValidator(allowEmpty: false));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public void Start(BasePlugin? plugin) { plugin?.RegisterFakeConVars(this); }
public Task<OneShotDeagleConfig?> Load() {
var cfg = new OneShotDeagleConfig {
Price = CV_PRICE.Value,
DoesFriendlyFire = CV_FRIENDLY_FIRE.Value,
Weapon = CV_WEAPON.Value
Weapon = CV_WEAPON.Value,
KillShooterOnFF = CV_KILL_SHOOTER_ON_FF.Value
};
return Task.FromResult<OneShotDeagleConfig?>(cfg);

View File

@@ -6,6 +6,10 @@ using CounterStrikeSharp.API.Modules.UserMessages;
namespace TTT.CS2.Extensions;
public static class PlayerExtensions {
public enum FadeFlags {
FADE_IN, FADE_OUT, FADE_STAYOUT
}
public static CBasePlayerWeapon? GetWeaponBase(
this CCSPlayerController player, string designerName) {
if (!player.IsValid) return null;
@@ -26,6 +30,26 @@ public static class PlayerExtensions {
ev.FireEvent(false);
}
public static void SetHealth(this CCSPlayerController player, int health) {
if (player.Pawn.Value == null) return;
if (health <= 0) {
player.CommitSuicide(false, true);
return;
}
player.Pawn.Value.Health = health;
Utilities.SetStateChanged(player.Pawn.Value, "CBaseEntity", "m_iHealth");
}
public static int GetHealth(this CCSPlayerController player) {
return player.Pawn.Value?.Health ?? 0;
}
public static void AddHealth(this CCSPlayerController player, int health) {
if (player.Pawn.Value == null) return;
player.SetHealth(player.Pawn.Value.Health + health);
}
public static void SetColor(this CCSPlayerController player, Color color) {
if (!player.IsValid) return;
var pawn = player.Pawn.Value;
@@ -37,10 +61,6 @@ public static class PlayerExtensions {
pawn.SetColor(color);
}
public enum FadeFlags {
FADE_IN, FADE_OUT, FADE_STAYOUT
}
public static void ColorScreen(this CCSPlayerController player, Color color,
float hold = 0.1f, float fade = 0.2f, FadeFlags flags = FadeFlags.FADE_IN,
bool withPurge = true) {

View File

@@ -6,6 +6,7 @@ using TTT.API.Role;
using TTT.CS2.Roles;
using TTT.CS2.Utils;
using TTT.Game;
using TTT.Game.lang;
using TTT.Game.Roles;
namespace TTT.CS2.Game;

View File

@@ -11,7 +11,7 @@ public class CS2GameManager(IServiceProvider provider) : GameManager(provider) {
provider.GetRequiredService<IMessenger>();
public override IGame CreateGame() {
messenger.Debug($"Attempting to create a new CS2 game...");
messenger.Debug("Attempting to create a new CS2 game...");
switch (ActiveGame) {
case { State: State.IN_PROGRESS or State.COUNTDOWN }:
throw new InvalidOperationException(

View File

@@ -4,6 +4,7 @@ using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Events;
@@ -27,6 +28,7 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
public void Dispose() { }
public void Start() { }
[UsedImplicitly]
[GameEventHandler]
public HookResult OnDeath(EventPlayerDeath ev, GameEventInfo _) {
if (games.ActiveGame is not { State: State.IN_PROGRESS })
@@ -36,7 +38,7 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
player.SetColor(Color.FromArgb(0, 255, 255, 255));
var ragdollBody = makeGameRagdoll(player);
var body = new CS2Body(provider, ragdollBody, converter.GetPlayer(player));
var body = new CS2Body(ragdollBody, converter.GetPlayer(player));
if (ev.Attacker != null && ev.Attacker.IsValid)
body.WithKiller(converter.GetPlayer(ev.Attacker));
@@ -50,6 +52,7 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
return HookResult.Continue;
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnStart(EventRoundStart ev, GameEventInfo _) {
Server.NextWorldUpdate(() => {

View File

@@ -18,7 +18,7 @@ public class KarmaSyncer(IServiceProvider provider) : IPluginModule {
provider.GetRequiredService<IPlayerFinder>();
public void Dispose() { }
public string Name => nameof(KarmaSyncer);
public string Id => nameof(KarmaSyncer);
public string Version => GitVersionInformation.FullSemVer;
public void Start() { }

View File

@@ -3,8 +3,6 @@ using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.Game.Events.Player;
@@ -17,12 +15,6 @@ public class PlayerConnectionsHandler(IServiceProvider provider)
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
public void Start() { }
public void Start(BasePlugin? plugin, bool hotReload) {
@@ -34,12 +26,12 @@ public class PlayerConnectionsHandler(IServiceProvider provider)
CounterStrikeSharp.API.Core.Listeners.OnClientDisconnect>(
disconnectFromServer);
Server.NextWorldUpdate(() => {
foreach (var ev in Utilities.GetPlayers()
.Select(player => converter.GetPlayer(player))
.Select(gamePlayer => new PlayerJoinEvent(gamePlayer)))
bus.Dispatch(ev);
});
if (!hotReload) return;
foreach (var ev in Utilities.GetPlayers()
.Select(player => converter.GetPlayer(player))
.Select(gamePlayer => new PlayerJoinEvent(gamePlayer)))
bus.Dispatch(ev);
}
public void Dispose() { }

View File

@@ -1,4 +1,6 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
@@ -18,15 +20,17 @@ using TTT.Game.Roles;
namespace TTT.CS2.GameHandlers;
public class RoleIconsHandler(IServiceProvider provider)
: BaseListener(provider), IPluginModule {
: BaseListener(provider), IPluginModule, IIconManager {
private static readonly string CT_MODEL =
"characters/models/ctm_fbi/ctm_fbi_varianth.vmdl";
private static readonly string T_MODEL =
"characters/models/tm_phoenix/tm_phoenix.vmdl";
private readonly IDictionary<int, IEnumerable<CPointWorldText>>
detectiveIcons = new Dictionary<int, IEnumerable<CPointWorldText>>();
// private readonly IDictionary<int, IEnumerable<CPointWorldText>> icons =
// new Dictionary<int, IEnumerable<CPointWorldText>>();
private readonly IEnumerable<CPointWorldText>?[] icons =
new IEnumerable<CPointWorldText>[64];
private readonly IPlayerConverter<CCSPlayerController> players =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
@@ -34,24 +38,65 @@ public class RoleIconsHandler(IServiceProvider provider)
private readonly ITextSpawner? textSpawner =
provider.GetService<ITextSpawner>();
private readonly IDictionary<int, IEnumerable<CPointWorldText>> traitorIcons =
new Dictionary<int, IEnumerable<CPointWorldText>>();
private readonly HashSet<int> traitorsThisRound = new();
private readonly ISet<int> traitors = new HashSet<int>();
private readonly ulong[] visibilities = new ulong[64];
public void Start(BasePlugin? plugin) {
public ulong GetVisiblePlayers(int client) {
if (client < 1 || client >= visibilities.Length)
throw new ArgumentOutOfRangeException(nameof(client));
return visibilities[client];
}
public void SetVisiblePlayers(int client, ulong playersBitmask) {
guardRange(client, nameof(client));
visibilities[client] = playersBitmask;
}
public void RevealToAll(int client) {
guardRange(client, nameof(client));
for (var i = 0; i < visibilities.Length; i++)
visibilities[i] |= 1UL << client;
}
public void AddVisiblePlayer(int client, int player) {
guardRange(client, nameof(client));
guardRange(player, nameof(player));
visibilities[client] |= 1UL << player;
}
public void RemoveVisiblePlayer(int client, int player) {
guardRange(client, nameof(client));
guardRange(player, nameof(player));
visibilities[client] &= ~(1UL << player);
}
public void ClearAllVisibility() {
Array.Clear(visibilities, 0, visibilities.Length);
}
public void Start(BasePlugin? plugin, bool hotReload) {
plugin
?.RegisterListener<CounterStrikeSharp.API.Core.Listeners.CheckTransmit>(
onTransmit);
}
[GameEventHandler]
public HookResult OnRoundEnd(EventRoundStart _, GameEventInfo _1) {
foreach (var text in Utilities
.FindAllEntitiesByDesignerName<CPointWorldText>("point_worldtext"))
text.AcceptInput("Kill");
return HookResult.Continue;
}
[UsedImplicitly]
[EventHandler(IgnoreCanceled = true)]
public void OnRoundStart(GameStateUpdateEvent ev) {
if (ev.NewState != State.IN_PROGRESS) return;
traitors.Clear();
traitorIcons.Clear();
detectiveIcons.Clear();
for (var i = 0; i < icons.Length; i++) removeIcon(i);
ClearAllVisibility();
traitorsThisRound.Clear();
}
[UsedImplicitly]
@@ -66,7 +111,7 @@ public class RoleIconsHandler(IServiceProvider provider)
}
// Remove in case we're re-assigning for some reason
removeAllIcons(player);
removeIcon(player.Slot);
player.SwitchTeam(ev.Role is DetectiveRole ?
CsTeam.CounterTerrorist :
@@ -77,10 +122,36 @@ public class RoleIconsHandler(IServiceProvider provider)
if (pawn == null || !pawn.IsValid) return;
pawn.SetModel(ev.Role is DetectiveRole ? CT_MODEL : T_MODEL);
if (ev.Role is InnocentRole) return;
assignIcon(player, ev.Role);
switch (ev.Role) {
case DetectiveRole: {
for (var i = 0; i < Server.MaxPlayers; i++)
AddVisiblePlayer(i, player.Slot);
break;
}
case TraitorRole: {
traitorsThisRound.Add(player.Slot);
foreach (var traitor in traitorsThisRound) {
AddVisiblePlayer(traitor, player.Slot);
AddVisiblePlayer(player.Slot, traitor);
}
break;
}
}
}
private void removeIcon(int slot) {
var existing = icons[slot];
if (existing == null) return;
foreach (var ent in existing) {
if (!ent.IsValid) continue;
ent.AcceptInput("Kill");
}
icons[slot] = null;
}
private void assignIcon(CCSPlayerController player, IRole role) {
@@ -88,39 +159,8 @@ public class RoleIconsHandler(IServiceProvider provider)
msg = role.Name.First(char.IsAsciiLetter).ToString(), color = role.Color
};
var roleIcon = textSpawner?.CreateTextHat(textSettings, player);
if (roleIcon == null) return;
if (role is DetectiveRole) {
detectiveIcons[player.Slot] = roleIcon;
return;
}
traitors.Add(player.Slot);
traitorIcons[player.Slot] = roleIcon;
}
private void removeAllIcons(CCSPlayerController player) {
removeTraitorIcon(player);
removeDetectiveIcon(player);
}
private void removeTraitorIcon(CCSPlayerController player) {
removeIcons(player.Slot, traitorIcons);
}
private void removeDetectiveIcon(CCSPlayerController player) {
removeIcons(player.Slot, detectiveIcons);
}
private void removeIcons(int slot,
IDictionary<int, IEnumerable<CPointWorldText>> cache) {
cache.Remove(slot, out var icons);
if (icons == null) return;
foreach (var icon in icons) {
if (!icon.IsValid) continue;
icon.Remove();
}
icons[player.Slot] = roleIcon;
}
[EventHandler(Priority = Priority.MONITOR)]
@@ -128,16 +168,29 @@ public class RoleIconsHandler(IServiceProvider provider)
var gamePlayer = players.GetPlayer(ev.Victim);
if (gamePlayer == null || !gamePlayer.IsValid) return;
removeAllIcons(gamePlayer);
removeIcon(gamePlayer.Slot);
}
// ReSharper disable once PossiblyImpureMethodCallOnReadonlyVariable
private void onTransmit(CCheckTransmitInfoList infoList) {
foreach (var (info, player) in infoList) {
if (player == null || !player.IsValid) continue;
if (traitors.Contains(player.Slot)) continue;
foreach (var icon in traitorIcons.Values.SelectMany(s => s))
info.TransmitEntities.Remove(icon);
hideIcons(info, player.Slot);
}
}
private void hideIcons(CCheckTransmitInfo info, int source) {
var visible = visibilities[source];
if (visible == ulong.MaxValue) return;
for (var i = 0; i < icons.Length; i++) {
if ((visible & 1UL << i) != 0) continue;
var iconList = icons[i];
if (iconList == null) continue;
foreach (var icon in iconList) info.TransmitEntities.Remove(icon);
}
}
private void guardRange(int index, string name) {
if (index < 0 || index >= visibilities.Length)
throw new ArgumentOutOfRangeException(name);
}
}

View File

@@ -19,7 +19,7 @@ public interface ITextSpawner {
IEnumerable<CPointWorldText> CreateTextHat(TextSetting setting,
CCSPlayerController player);
IEnumerable<CPointWorldText> CreateTextScreen(TextSetting setting,
CCSPlayerController player);
}

View File

@@ -0,0 +1,11 @@
using TTT.Locale;
namespace TTT.CS2.Items.Camouflage;
public class CamoMsgs {
public static IMsg SHOP_ITEM_CAMO
=> MsgFactory.Create(nameof(SHOP_ITEM_CAMO));
public static IMsg SHOP_ITEM_CAMO_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_CAMO_DESC));
}

View File

@@ -0,0 +1,45 @@
using System.Drawing;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.CS2.Extensions;
namespace TTT.CS2.Items.Camouflage;
public static class CamoServiceCollection {
public static void AddCamoServices(this IServiceCollection collection) {
collection.AddModBehavior<CamouflageItem>();
}
}
public class CamouflageItem(IServiceProvider provider) : BaseItem(provider) {
private readonly CamoConfig config =
provider.GetService<IStorage<CamoConfig>>()?.Load().GetAwaiter().GetResult()
?? new CamoConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public override string Name => Locale[CamoMsgs.SHOP_ITEM_CAMO];
public override string Description => Locale[CamoMsgs.SHOP_ITEM_CAMO_DESC];
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) {
Server.NextWorldUpdate(() => {
var gamePlayer = converter.GetPlayer(player);
var alpha = (int)Math.Round(config.CamoVisibility * 255);
gamePlayer?.SetColor(Color.FromArgb(alpha, Color.White));
});
}
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
return Shop.HasItem<CamouflageItem>(player) ?
PurchaseResult.ALREADY_OWNED :
PurchaseResult.SUCCESS;
}
}

View File

@@ -0,0 +1,94 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.CS2.API;
using TTT.CS2.Events;
using TTT.Game.Events.Game;
using TTT.Game.Listeners;
namespace TTT.CS2.Items.DNA;
public class DnaListener(IServiceProvider provider) : BaseListener(provider) {
private static readonly TimeSpan cooldown = TimeSpan.FromSeconds(15);
private static readonly string[] missingDnaExplanations = {
"the killer used gloves... for their bullets",
"the killer was very careful", "the killer wiped the weapon clean",
"the killer retrieved the bullets", "the bullets disintegrated on impact",
"the killer was GOATed", "but no DNA was found",
"but legal litigation caused the DNA to be lost",
"and confirmed they were dead", "and they will remember that", "good job"
};
private readonly IBodyTracker bodies =
provider.GetRequiredService<IBodyTracker>();
private readonly DnaScannerConfig config = provider
.GetService<IStorage<DnaScannerConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new DnaScannerConfig();
private readonly Dictionary<string, DateTime> lastMessages = new();
private readonly IShop shop = provider.GetRequiredService<IShop>();
// Low priority to allow body identification to happen first
[UsedImplicitly]
[EventHandler(Priority = Priority.LOW)]
public void OnPropPickup(PropPickupEvent ev) {
if (ev.Player is not IOnlinePlayer player) return;
if (!shop.HasItem<DnaScanner>(player)) return;
if (!bodies.TryLookup(ev.Prop.Index.ToString(), out var body)) return;
if (body == null) return;
var victimRole = Roles.GetRoles(body.OfPlayer).FirstOrDefault();
if (victimRole == null) return;
if (lastMessages.TryGetValue(player.Id + "." + body.Id,
out var lastMessageTime))
if (DateTime.Now - lastMessageTime < cooldown)
return;
lastMessages[player.Id + "." + body.Id] = DateTime.Now;
if (DateTime.Now - body.TimeOfDeath > config.DecayTime) {
Messenger.Message(player,
Locale[DnaMsgs.SHOP_ITEM_DNA_EXPIRED(victimRole, body.OfPlayer)]);
return;
}
if (body.Killer == null) {
var explanation =
missingDnaExplanations[
Random.Shared.Next(missingDnaExplanations.Length)];
Messenger.Message(player,
Locale[
DnaMsgs.SHOP_ITEM_DNA_SCANNED_OTHER(victimRole, body.OfPlayer,
explanation)]);
return;
}
if (body.Killer == body.OfPlayer) {
Messenger.Message(player,
Locale[
DnaMsgs.SHOP_ITEM_DNA_SCANNED_SUICIDE(victimRole, body.OfPlayer)]);
return;
}
Messenger.Message(player,
Locale[
DnaMsgs.SHOP_ITEM_DNA_SCANNED(victimRole, body.OfPlayer, body.Killer)]);
}
[EventHandler]
public void OnRoundEnd(GameStateUpdateEvent ev) {
if (ev.NewState != State.FINISHED) return;
lastMessages.Clear();
}
}

View File

@@ -0,0 +1,36 @@
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.CS2.Items.DNA;
public class DnaMsgs {
public static IMsg SHOP_ITEM_DNA => MsgFactory.Create(nameof(SHOP_ITEM_DNA));
public static IMsg SHOP_ITEM_DNA_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_DNA_DESC));
public static IMsg SHOP_ITEM_DNA_SCANNED(IRole victimRole, IPlayer player,
IPlayer killer) {
return MsgFactory.Create(nameof(SHOP_ITEM_DNA_SCANNED),
GameMsgs.GetRolePrefix(victimRole), player.Name, killer.Name);
}
public static IMsg SHOP_ITEM_DNA_SCANNED_OTHER(IRole victimRole,
IPlayer player, string explanation) {
return MsgFactory.Create(nameof(SHOP_ITEM_DNA_SCANNED_OTHER),
GameMsgs.GetRolePrefix(victimRole), player.Name);
}
public static IMsg
SHOP_ITEM_DNA_SCANNED_SUICIDE(IRole victimRole, IPlayer player) {
return SHOP_ITEM_DNA_SCANNED_OTHER(victimRole, player,
"they killed themselves");
}
public static IMsg SHOP_ITEM_DNA_EXPIRED(IRole victimRole, IPlayer player) {
return MsgFactory.Create(nameof(SHOP_ITEM_DNA_EXPIRED),
GameMsgs.GetRolePrefix(victimRole), player.Name);
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game.Roles;
namespace TTT.CS2.Items.DNA;
public static class DnaScannerServiceCollection {
public static void AddDnaScannerServices(this IServiceCollection collection) {
collection.AddModBehavior<DnaScanner>();
collection.AddModBehavior<DnaListener>();
}
}
public class DnaScanner(IServiceProvider provider)
: RoleRestrictedItem<DetectiveRole>(provider) {
private readonly DnaScannerConfig config = provider
.GetService<IStorage<DnaScannerConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new DnaScannerConfig();
public override string Name => Locale[DnaMsgs.SHOP_ITEM_DNA];
public override string Description => Locale[DnaMsgs.SHOP_ITEM_DNA_DESC];
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) { }
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
if (Shop.HasItem(player, this)) return PurchaseResult.ALREADY_OWNED;
return base.CanPurchase(player);
}
}

View File

@@ -0,0 +1,74 @@
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
using TTT.CS2.Extensions;
using TTT.Game.Roles;
namespace TTT.CS2.Items.Station;
public static class DamageStationCollection {
public static void AddDamageStation(this IServiceCollection collection) {
collection.AddModBehavior<DamageStation>();
}
}
public class DamageStation(IServiceProvider provider) : StationItem(provider,
provider.GetService<IStorage<DamageStationConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new DamageStationConfig()) {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
public override string Name => Locale[StationMsgs.SHOP_ITEM_STATION_HURT];
public override string Description
=> Locale[StationMsgs.SHOP_ITEM_STATION_HURT_DESC];
override protected void onInterval() {
var players = finder.GetOnline();
foreach (var (prop, info) in props) {
if (Math.Abs(info.HealthGiven) > Math.Abs(_Config.TotalHealthGiven)) {
props.Remove(prop);
continue;
}
var propPos = prop.AbsOrigin;
if (propPos == null) continue;
var playerMapping = players.Select(p
=> (ApiPlayer: p, GamePlayer: converter.GetPlayer(p)))
.Where(m => m.GamePlayer != null);
var playerDists = playerMapping
.Where(t => !roles.GetRoles(t.ApiPlayer).OfType<TraitorRole>().Any())
.Select(t => (t.ApiPlayer, Origin: t.GamePlayer!.Pawn.Value?.AbsOrigin,
t.GamePlayer))
.Where(t => t is { Origin: not null, ApiPlayer.IsAlive: true })
.Select(t
=> (t.ApiPlayer, Dist: t.Origin!.Distance(propPos), t.GamePlayer))
.Where(t => t.Dist <= _Config.MaxRange)
.ToList();
foreach (var (player, dist, gamePlayer) in playerDists) {
var healthScale = 1.0 - dist / _Config.MaxRange;
var damageAmount =
(int)Math.Floor(_Config.HealthIncrements * healthScale);
player.Health += damageAmount;
info.HealthGiven += damageAmount;
gamePlayer.ExecuteClientCommand("play " + _Config.UseSound);
}
}
}
}

View File

@@ -0,0 +1,57 @@
using CounterStrikeSharp.API;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs;
using TTT.API.Extensions;
using TTT.API.Storage;
using TTT.CS2.Extensions;
namespace TTT.CS2.Items.Station;
public static class HealthStationCollection {
public static void AddHealthStation(this IServiceCollection collection) {
collection.AddModBehavior<HealthStation>();
}
}
public class HealthStation(IServiceProvider provider) : StationItem(provider,
provider.GetService<IStorage<HealthStationConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new HealthStationConfig()) {
public override string Name => Locale[StationMsgs.SHOP_ITEM_STATION_HEALTH];
public override string Description
=> Locale[StationMsgs.SHOP_ITEM_STATION_HEALTH_DESC];
override protected void onInterval() {
var players = Utilities.GetPlayers();
foreach (var (prop, info) in props) {
if (Math.Abs(info.HealthGiven) > _Config.TotalHealthGiven) {
props.Remove(prop);
continue;
}
var propPos = prop.AbsOrigin;
if (propPos == null) continue;
var playerDists = players
.Select(p => (Player: p, Pos: p.Pawn.Value?.AbsOrigin))
.Where(t => t is { Pos: not null, Player.Pawn.Value.Health: > 0 })
.Select(t => (t.Player, Dist: t.Pos!.Distance(propPos)))
.Where(t => t.Dist <= _Config.MaxRange)
.ToList();
foreach (var (player, dist) in playerDists) {
var maxHp = player.Pawn.Value?.MaxHealth ?? 100;
var healthScale = 1.0 - dist / _Config.MaxRange;
var healAmount =
(int)Math.Ceiling(_Config.HealthIncrements * healthScale);
var newHealth = Math.Min(player.GetHealth() + healAmount, maxHp);
player.SetHealth(newHealth);
info.HealthGiven += healAmount;
player.ExecuteClientCommand("play " + _Config.UseSound);
}
}
}
}

View File

@@ -0,0 +1,9 @@
using CounterStrikeSharp.API.Core;
namespace TTT.CS2.Items.Station;
public class StationInfo(CPhysicsPropMultiplayer prop, int health) {
public readonly CPhysicsPropMultiplayer Prop = prop;
public int Health = health;
public int HealthGiven = 0;
}

View File

@@ -0,0 +1,146 @@
using System.Reactive.Concurrency;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using TTT.API;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.CS2.Extensions;
using TTT.Game.Roles;
namespace TTT.CS2.Items.Station;
public abstract class StationItem(IServiceProvider provider,
StationConfig config)
: RoleRestrictedItem<DetectiveRole>(provider), IPluginModule {
private static readonly long PROP_SIZE_SQUARED = 500;
protected readonly StationConfig _Config = config;
protected readonly IPlayerConverter<CCSPlayerController> Converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
protected readonly Dictionary<CPhysicsPropMultiplayer, StationInfo> props =
new();
private readonly IScheduler scheduler =
provider.GetRequiredService<IScheduler>();
private IDisposable? intervalHandle;
public override ShopItemConfig Config => _Config;
public override void Start() {
base.Start();
intervalHandle = scheduler.SchedulePeriodic(_Config.HealthInterval,
() => Server.NextWorldUpdate(onInterval));
}
public void Start(BasePlugin? plugin) {
Start();
messenger.DebugAnnounce("Starting StationItem2 ");
plugin
?.RegisterListener<
CounterStrikeSharp.API.Core.Listeners.OnServerPrecacheResources>(m => {
m.AddResource("models/props/cs_office/microwave.vmdl");
});
}
public override void Dispose() {
base.Dispose();
intervalHandle?.Dispose();
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnBulletImpact(EventBulletImpact ev, GameEventInfo info) {
var hitVec = new Vector(ev.X, ev.Y, ev.Z);
var nearest = props
.Select(kv => (kv.Key, kv.Value,
Distance: kv.Key.AbsOrigin!.DistanceSquared(hitVec)))
.Where(t => t.Key is { IsValid: true, AbsOrigin: not null })
.OrderBy(t => t.Distance)
.FirstOrDefault();
if (nearest.Key == null || nearest.Value == null
|| nearest.Distance > PROP_SIZE_SQUARED)
return HookResult.Continue;
var dmg = getBulletDamage(ev);
nearest.Value.Health -= dmg;
if (nearest.Value.Health <= 0) {
nearest.Key.AcceptInput("Kill");
props.Remove(nearest.Key);
return HookResult.Continue;
}
nearest.Key.SetColor(
_Config.GetColor(nearest.Value.Health / (float)_Config.StationHealth));
return HookResult.Continue;
}
private int getBulletDamage(EventBulletImpact ev) {
var user = ev.Userid;
var weapon = user?.Pawn.Value?.WeaponServices?.ActiveWeapon.Value;
var dist =
(user?.Pawn.Value?.AbsOrigin?.Distance(new Vector(ev.X, ev.Y, ev.Z))
?? null) ?? 1;
var distScale = Math.Clamp(256 / dist, 0.1, 1);
var baseDamage = getBaseDamage(weapon?.DesignerName ?? "");
var total = (int)(baseDamage * distScale);
return Math.Max(total, 1);
}
private int getBaseDamage(string designerWeapon) {
return designerWeapon switch {
"weapon_awp" => 115,
"weapon_glock" => 8,
"weapon_usp_silencer" => 20,
"weapon_deagle" => 40,
_ when Tag.PISTOLS.Contains(designerWeapon) => 10,
_ when Tag.SMGS.Contains(designerWeapon) => 15,
_ when Tag.SHOTGUNS.Contains(designerWeapon) => 25,
_ when Tag.RIFLES.Contains(designerWeapon) => 45,
_ => 5
};
}
public override void OnPurchase(IOnlinePlayer player) {
Server.NextWorldUpdate(() => {
var prop =
Utilities.CreateEntityByName<CPhysicsPropMultiplayer>(
"prop_physics_multiplayer");
if (prop == null) return;
props[prop] = new StationInfo(prop, _Config.StationHealth);
prop.SetModel("models/props/cs_office/microwave.vmdl");
prop.DispatchSpawn();
var gamePlayer = Converter.GetPlayer(player);
if (gamePlayer == null || !gamePlayer.Pawn.IsValid
|| gamePlayer.Pawn.Value == null)
return;
var spawnPos = gamePlayer.Pawn.Value.AbsOrigin.Clone();
if (spawnPos != null && gamePlayer.PlayerPawn.Value != null) {
var forward = gamePlayer.PlayerPawn.Value.EyeAngles.ToForward();
forward.Z = 0;
spawnPos += forward.Normalized() * 8;
}
prop.Teleport(spawnPos);
});
}
abstract protected void onInterval();
}

View File

@@ -0,0 +1,17 @@
using TTT.Locale;
namespace TTT.CS2.Items.Station;
public class StationMsgs {
public static IMsg SHOP_ITEM_STATION_HEALTH
=> MsgFactory.Create(nameof(SHOP_ITEM_STATION_HEALTH));
public static IMsg SHOP_ITEM_STATION_HEALTH_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_STATION_HEALTH_DESC));
public static IMsg SHOP_ITEM_STATION_HURT
=> MsgFactory.Create(nameof(SHOP_ITEM_STATION_HURT));
public static IMsg SHOP_ITEM_STATION_HURT_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_STATION_HURT_DESC));
}

View File

@@ -7,8 +7,8 @@ using TTT.API.Player;
using TTT.CS2.API;
using TTT.CS2.Events;
using TTT.CS2.Extensions;
using TTT.Game;
using TTT.Game.Events.Body;
using TTT.Game.lang;
using TTT.Game.Listeners;
using TTT.Game.Roles;

View File

@@ -20,11 +20,8 @@ public class ScreenColorApplier(IServiceProvider provider)
if (ev.Role is SpectatorRole) return;
var player = converter.GetPlayer(ev.Player);
var alphaColor = Color.FromArgb(16, ev.Role.Color);
var alphaColor = Color.FromArgb(64, ev.Role.Color);
if (player != null)
player.ColorScreen(alphaColor, 5f, 5f,
flags: PlayerExtensions.FadeFlags.FADE_OUT);
player?.PrintToCenterHtml("You are a " + ev.Role.Name, 20);
player.ColorScreen(alphaColor, 3, 1, PlayerExtensions.FadeFlags.FADE_OUT);
}
}

View File

@@ -8,6 +8,7 @@ namespace TTT.CS2.Player;
public class CCPlayerConverter : IPluginModule,
IPlayerConverter<CCSPlayerController> {
private readonly Dictionary<string, CS2Player> playerCache = new();
private readonly Dictionary<string, CCSPlayerController> reverseCache = new();
public IPlayer GetPlayer(CCSPlayerController player) {
if (playerCache.TryGetValue(player.SteamID.ToString(),
@@ -28,12 +29,19 @@ public class CCPlayerConverter : IPluginModule,
public CCSPlayerController? GetPlayer(IPlayer player) {
if (!ulong.TryParse(player.Id, out var steamId)) return null;
if (reverseCache.TryGetValue(player.Id, out var cachedPlayer)) {
if (cachedPlayer.IsValid) return cachedPlayer;
reverseCache.Remove(player.Id);
}
CCSPlayerController? result = null;
var gamePlayer = Utilities.GetPlayerFromSteamId(steamId);
if (gamePlayer is { IsValid: true }) result = gamePlayer;
var bot = Utilities.GetPlayerFromIndex((int)steamId);
if (bot is { IsValid: true }) result = bot;
if (result != null) reverseCache[player.Id] = result;
return result;
}

View File

@@ -11,27 +11,25 @@ public class CS2AliveSpoofer : IAliveSpoofer, IPluginModule {
public void SpoofAlive(CCSPlayerController player) {
if (player.IsBot) {
player.PawnIsAlive = true;
Utilities.SetStateChanged(player, "CCSPlayerController",
"m_bPawnIsAlive");
Server.NextWorldUpdate(() => {
var pawn = player.Pawn.Value;
if (pawn == null || !pawn.IsValid) return;
pawn.DeathTime = 0;
Utilities.SetStateChanged(pawn, "CBasePlayerPawn", "m_flDeathTime");
Utilities.SetStateChanged(pawn, "CBasePlayerController",
"m_flDeathTime");
Server.NextWorldUpdate(() => {
player.PawnIsAlive = true;
Utilities.SetStateChanged(player, "CCSPlayerController",
"m_bPawnIsAlive");
});
});
return;
}
FakeAlivePlayers.Add(player);
Server.NextWorldUpdate(() => {
var pawn = player.Pawn.Value;
if (pawn == null || !pawn.IsValid) return;
pawn.DeathTime = 0;
Utilities.SetStateChanged(pawn, "CBasePlayerPawn", "m_flDeathTime");
Utilities.SetStateChanged(pawn, "CBasePlayerController", "m_flDeathTime");
Server.NextWorldUpdate(() => {
player.PawnIsAlive = true;
Utilities.SetStateChanged(player, "CCSPlayerController",
"m_bPawnIsAlive");
});
});
}
public void UnspoofAlive(CCSPlayerController player) {

View File

@@ -9,8 +9,8 @@ namespace TTT.CS2.Player;
public class CS2InventoryManager(
IPlayerConverter<CCSPlayerController> converter) : IInventoryManager {
public void GiveWeapon(IOnlinePlayer player, IWeapon weapon) {
Server.NextWorldUpdate(() => {
public Task GiveWeapon(IOnlinePlayer player, IWeapon weapon) {
return Server.NextWorldUpdateAsync(() => {
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
@@ -18,65 +18,8 @@ public class CS2InventoryManager(
});
}
private void giveWeapon(CCSPlayerController player, IWeapon weapon) {
if (player.Team is CsTeam.None or CsTeam.Spectator) return;
// Give the weapon
player.GiveNamedItem(weapon.WeaponId);
// Set ammo if applicable
var weaponBase = player.GetWeaponBase(weapon.WeaponId);
if (weaponBase == null) return;
if (weapon.CurrentAmmo != null) weaponBase.Clip1 = weapon.CurrentAmmo.Value;
if (weapon.ReserveAmmo != null) weaponBase.Clip2 = weapon.ReserveAmmo.Value;
}
public static gear_slot_t IntToSlot(int slot)
=> slot switch {
0 => gear_slot_t.GEAR_SLOT_RIFLE,
1 => gear_slot_t.GEAR_SLOT_PISTOL,
2 => gear_slot_t.GEAR_SLOT_KNIFE,
3 => gear_slot_t.GEAR_SLOT_UTILITY,
4 => gear_slot_t.GEAR_SLOT_C4,
_ => gear_slot_t.GEAR_SLOT_FIRST
};
public static int SlotToInt(gear_slot_t slot)
=> slot switch {
gear_slot_t.GEAR_SLOT_RIFLE => 0,
gear_slot_t.GEAR_SLOT_PISTOL => 1,
gear_slot_t.GEAR_SLOT_KNIFE => 2,
gear_slot_t.GEAR_SLOT_UTILITY => 3,
gear_slot_t.GEAR_SLOT_C4 => 4,
_ => -1
};
private void clearSlot(CCSPlayerController player,
params gear_slot_t[] slots) {
if (player.Team is CsTeam.None or CsTeam.Spectator) return;
var weapons = player.Pawn.Value?.WeaponServices?.MyWeapons;
if (weapons == null || weapons.Count == 0) return;
foreach (var weapon in weapons) {
if (!weapon.IsValid || weapon.Value == null) continue;
if (!weapon.Value.IsValid
|| !weapon.Value.DesignerName.StartsWith("weapon_"))
continue;
if (weapon.Value.Entity == null) continue;
if (!weapon.Value.OwnerEntity.IsValid) continue;
var weaponBase = weapon.Value.As<CBaseEntity>();
if (!weaponBase.IsValid || (weaponBase.Entity == null)) continue;
var weaponData = (weaponBase as CCSWeaponBase)?.VData;
if (weaponData == null) continue;
if (!slots.Contains(weaponData.GearSlot)) continue;
weapon.Value.AddEntityIOEvent("Kill", weapon.Value);
}
}
public void RemoveWeapon(IOnlinePlayer player, string weaponId) {
Server.NextWorldUpdate(() => {
public Task RemoveWeapon(IOnlinePlayer player, string weaponId) {
return Server.NextWorldUpdateAsync(() => {
if (!player.IsAlive) return;
var gamePlayer = converter.GetPlayer(player);
@@ -102,8 +45,8 @@ public class CS2InventoryManager(
});
}
public void RemoveWeaponInSlot(IOnlinePlayer player, int slot) {
Server.NextWorldUpdate(() => {
public Task RemoveWeaponInSlot(IOnlinePlayer player, int slot) {
return Server.NextWorldUpdateAsync(() => {
if (!player.IsAlive) return;
var gamePlayer = converter.GetPlayer(player);
@@ -113,8 +56,8 @@ public class CS2InventoryManager(
});
}
public void RemoveAllWeapons(IOnlinePlayer player) {
Server.NextWorldUpdate(() => {
public Task RemoveAllWeapons(IOnlinePlayer player) {
return Server.NextWorldUpdateAsync(() => {
if (!player.IsAlive) return;
var gamePlayer = converter.GetPlayer(player);
@@ -123,4 +66,69 @@ public class CS2InventoryManager(
gamePlayer.RemoveWeapons();
});
}
private void giveWeapon(CCSPlayerController player, IWeapon weapon) {
if (player.Team is CsTeam.None or CsTeam.Spectator) return;
// Give the weapon
player.GiveNamedItem(weapon.WeaponId);
// Set ammo if applicable
var weaponBase = player.GetWeaponBase(weapon.WeaponId);
if (weaponBase == null)
if (weapon.WeaponId.Equals("weapon_revolver"))
weaponBase = player.GetWeaponBase("weapon_deagle");
if (weaponBase == null) return;
if (weapon.CurrentAmmo != null) weaponBase.Clip1 = weapon.CurrentAmmo.Value;
if (weapon.ReserveAmmo != null)
weaponBase.ReserveAmmo[0] = weapon.ReserveAmmo.Value;
Utilities.SetStateChanged(weaponBase, "CBasePlayerWeapon", "m_iClip1");
Utilities.SetStateChanged(weaponBase, "CBasePlayerWeapon",
"m_pReserveAmmo");
}
public static gear_slot_t IntToSlot(int slot) {
return slot switch {
0 => gear_slot_t.GEAR_SLOT_RIFLE,
1 => gear_slot_t.GEAR_SLOT_PISTOL,
2 => gear_slot_t.GEAR_SLOT_KNIFE,
3 => gear_slot_t.GEAR_SLOT_UTILITY,
4 => gear_slot_t.GEAR_SLOT_C4,
_ => gear_slot_t.GEAR_SLOT_FIRST
};
}
public static int SlotToInt(gear_slot_t slot) {
return slot switch {
gear_slot_t.GEAR_SLOT_RIFLE => 0,
gear_slot_t.GEAR_SLOT_PISTOL => 1,
gear_slot_t.GEAR_SLOT_KNIFE => 2,
gear_slot_t.GEAR_SLOT_UTILITY => 3,
gear_slot_t.GEAR_SLOT_C4 => 4,
_ => -1
};
}
private void clearSlot(CCSPlayerController player,
params gear_slot_t[] slots) {
if (player.Team is CsTeam.None or CsTeam.Spectator) return;
var weapons = player.Pawn.Value?.WeaponServices?.MyWeapons;
if (weapons == null || weapons.Count == 0) return;
foreach (var weapon in weapons) {
if (!weapon.IsValid || weapon.Value == null) continue;
if (!weapon.Value.IsValid
|| !weapon.Value.DesignerName.StartsWith("weapon_"))
continue;
if (weapon.Value.Entity == null) continue;
var weaponBase = weapon.Value.As<CCSWeaponBase>();
if (!weaponBase.IsValid || weaponBase.Entity == null) continue;
var vdata = weaponBase.VData;
if (vdata == null) continue;
if (!slots.Contains(vdata.GearSlot)) continue;
weapon.Value.AddEntityIOEvent("Kill", weapon.Value);
}
}
}

View File

@@ -58,15 +58,18 @@ public class CS2Player : IOnlinePlayer {
get => Player?.Pawn.Value != null ? Player.Pawn.Value.Health : 0;
set {
if (Player?.Pawn.Value == null) return;
Server.NextWorldUpdate(() => {
if (Player?.Pawn.Value == null) return;
if (value <= 0) {
Player.CommitSuicide(false, true);
return;
}
if (value <= 0) {
Player.CommitSuicide(false, true);
return;
}
Player.Pawn.Value.Health = value;
Utilities.SetStateChanged(Player.Pawn.Value, "CBaseEntity", "m_iHealth");
Player.Pawn.Value.Health = value;
Utilities.SetStateChanged(Player.Pawn.Value, "CBaseEntity",
"m_iHealth");
});
}
}

View File

@@ -1,6 +1,6 @@
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.CS2.lang;

View File

@@ -1,2 +1,17 @@
ROLE_SPECTATOR: "Spectator"
TASER_SCANNED: "%PREFIX%You scanned {0}{grey}, they are %an% {1}{grey}!"
TASER_SCANNED: "%PREFIX%You scanned {0}{grey}, they are %an% {1}{grey}!"
DNA_PREFIX: "{darkblue}D{blue}N{lightblue}A{grey} | {grey}"
SHOP_ITEM_DNA: "DNA Scanner"
SHOP_ITEM_DNA_DESC: "Scan bodies to reveal the person who killed them."
SHOP_ITEM_DNA_SCANNED: "%DNA_PREFIX%You scanned {0}{1}'%s% {grey}body, their killer was {red}{2}{grey}."
SHOP_ITEM_DNA_SCANNED_OTHER: "%DNA_PREFIX%You scanned {0}{1}'%s% {grey}body, {2}."
SHOP_ITEM_DNA_EXPIRED: "%DNA_PREFIX%You scanned {0}{1}'%s% {grey}body, but the DNA has expired."
SHOP_ITEM_STATION_HEALTH: "Health Station"
SHOP_ITEM_STATION_HEALTH_DESC: "A health station that heals players around it."
SHOP_ITEM_STATION_HURT: "Hurt Station"
SHOP_ITEM_STATION_HURT_DESC: "A station that hurts non-Traitors around it."
SHOP_ITEM_CAMO: "Camouflage"
SHOP_ITEM_CAMO_DESC: "Disguise yourself and make yourself harder to see."

View File

@@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Command;
using TTT.API.Player;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.Game.Commands;

View File

@@ -11,7 +11,7 @@ public class LogsCommand(IServiceProvider provider) : ICommand {
public void Dispose() { }
public string Name => "logs";
public string Id => "logs";
public void Start() { }
// TODO: Restrict and verbalize usage

View File

@@ -4,6 +4,7 @@ using TTT.API;
using TTT.API.Command;
using TTT.API.Events;
using TTT.API.Player;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.Game.Commands;
@@ -13,7 +14,7 @@ public class TTTCommand(IServiceProvider provider) : ICommand {
provider.GetRequiredService<IMsgLocalizer>();
public void Dispose() { }
public string Name => "ttt";
public string Id => "ttt";
public string[] Usage => ["<modules/commands/listeners>"];
public void Start() { }
@@ -70,7 +71,7 @@ public class TTTCommand(IServiceProvider provider) : ICommand {
IEnumerable<ITerrorModule> listeners) {
foreach (var listener in listeners)
printVersionedEntry(info, listener.Version,
listener.Name + " - " + listener.GetType().Name);
listener.Id + " - " + listener.GetType().Name);
}
private void printVersionedEntry(ICommandInfo info, string version,

View File

@@ -7,6 +7,7 @@ public interface IBody {
IPlayer OfPlayer { get; }
bool IsIdentified { get; set; }
IWeapon? MurderWeapon { get; }
IPlayer? Killer { get; }
IPlayer? Killer { get; set; }
string Id { get; }
DateTime TimeOfDeath { get; }
}

View File

@@ -1,9 +1,9 @@
using System.Reactive.Concurrency;
using CounterStrikeSharp.API;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Game;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.Game.Loggers;

View File

@@ -4,6 +4,7 @@ using TTT.API.Messages;
using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.Game.Roles;

View File

@@ -2,8 +2,8 @@
namespace TTT.Game.Roles;
public class BaseWeapon(string id, int? reserve = null,
int? current = null) : IWeapon {
public class BaseWeapon(string id, int? reserve = null, int? current = null)
: IWeapon {
public string WeaponId { get; } = id;
public int? ReserveAmmo { get; } = reserve;
public int? CurrentAmmo { get; } = current;

View File

@@ -1,5 +1,6 @@
using System.Drawing;
using TTT.API.Player;
using TTT.Game.lang;
namespace TTT.Game.Roles;

View File

@@ -1,6 +1,7 @@
using System.Drawing;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Player;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.Game.Roles;

View File

@@ -1,5 +1,6 @@
using System.Drawing;
using TTT.API.Player;
using TTT.Game.lang;
namespace TTT.Game.Roles;

View File

@@ -8,6 +8,7 @@ using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
using TTT.Game.Events.Game;
using TTT.Game.lang;
using TTT.Game.Loggers;
using TTT.Game.Roles;
using TTT.Locale;

View File

@@ -4,7 +4,7 @@ using TTT.API.Role;
using TTT.Game.Roles;
using TTT.Locale;
namespace TTT.Game;
namespace TTT.Game.lang;
public static class GameMsgs {
public static IMsg PREFIX => MsgFactory.Create(nameof(PREFIX));

View File

@@ -42,7 +42,7 @@ public class KarmaStorage(IServiceProvider provider) : IKarmaService {
}
public void Dispose() { }
public string Name => nameof(KarmaStorage);
public string Id => nameof(KarmaStorage);
public string Version => GitVersionInformation.FullSemVer;
public async Task Write(IPlayer key, int newData) {

View File

@@ -25,7 +25,7 @@ public class TTT(IServiceProvider provider) : BasePlugin {
module.Start();
loadedModules.Add(module);
Logger.LogInformation(
$"Loaded {module.Version} {module.Name} {module.GetType().Namespace}");
$"Loaded {module.Version} {module.Id} {module.GetType().Namespace}");
}
var pluginModules = modules.Where(m => m is IPluginModule)
@@ -40,7 +40,7 @@ public class TTT(IServiceProvider provider) : BasePlugin {
RegisterAllAttributes(module);
loadedModules.Add(module);
Logger.LogInformation(
$"Registered {module.Version} {module.Name} {module.GetType().Namespace}");
$"Registered {module.Version} {module.Id} {module.GetType().Namespace}");
}
Logger.LogInformation("All modules loaded successfully.");
@@ -54,10 +54,10 @@ public class TTT(IServiceProvider provider) : BasePlugin {
foreach (var module in loadedModules)
try {
Logger.LogInformation($"Unloading {module.Name} ({module.Version})");
Logger.LogInformation($"Unloading {module.Id} ({module.Version})");
module.Dispose();
} catch (Exception e) {
Logger.LogError(e, $"Error unloading module {module.Name}");
Logger.LogError(e, $"Error unloading module {module.Id}");
}
base.Dispose(disposing);

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Command;
using TTT.API.Player;
@@ -6,8 +7,8 @@ namespace TTT.Shop.Commands;
public class BalanceCommand(IServiceProvider provider) : ICommand {
private readonly IShop shop = provider.GetRequiredService<IShop>();
public string Name => "balance";
public string[] Aliases => [Name, "bal", "credits", "money"];
public string Id => "balance";
public string[] Aliases => [Id, "bal", "credits", "money"];
public void Dispose() { }
public void Start() { }

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Command;
using TTT.API.Game;
using TTT.API.Player;
@@ -16,55 +17,43 @@ public class BuyCommand(IServiceProvider provider) : ICommand {
private readonly IShop shop = provider.GetRequiredService<IShop>();
public void Dispose() { }
public string Name => "buy";
public string Id => "buy";
public void Start() { }
public string[] Aliases => [Name, "purchase", "b"];
public string[] Aliases => [Id, "purchase", "b"];
public async Task<CommandResult> Execute(IOnlinePlayer? executor,
public Task<CommandResult> Execute(IOnlinePlayer? executor,
ICommandInfo info) {
if (executor == null) {
info.ReplySync("You must be a player to buy items.");
return CommandResult.PLAYER_ONLY;
}
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
if (games.ActiveGame is not { State: State.IN_PROGRESS }) {
info.ReplySync(locale[ShopMsgs.SHOP_INACTIVE]);
return CommandResult.SUCCESS;
return Task.FromResult(CommandResult.SUCCESS);
}
if (info.ArgCount == 1) return CommandResult.PRINT_USAGE;
if (info.ArgCount == 1) return Task.FromResult(CommandResult.PRINT_USAGE);
if (executor.Health <= 0) {
info.ReplySync(locale[ShopMsgs.SHOP_INACTIVE]);
return Task.FromResult(CommandResult.SUCCESS);
}
var query = string.Join(" ", info.Args.Skip(1));
info.ReplySync($"Searching for item: {query}");
var item = searchItem(query);
var item = searchItem(query);
if (item == null) {
info.ReplySync($"Item '{query}' not found.");
return CommandResult.ERROR;
info.ReplySync(locale[ShopMsgs.SHOP_ITEM_NOT_FOUND(query)]);
return Task.FromResult(CommandResult.ERROR);
}
var bal = await shop.Load(executor);
if (item.Config.Price > bal) {
info.ReplySync(
$"You cannot afford '{item.Name}'. It costs {item.Config.Price}, but you have {bal}.");
return CommandResult.ERROR;
}
if (item.CanPurchase(executor) != PurchaseResult.SUCCESS) {
info.ReplySync($"You cannot purchase '{item.Name}'.");
return CommandResult.ERROR;
}
await shop.Write(executor, bal - item.Config.Price);
item.OnPurchase(executor);
shop.GiveItem(executor, item);
return CommandResult.SUCCESS;
var result = shop.TryPurchase(executor, item);
return Task.FromResult(result == PurchaseResult.SUCCESS ?
CommandResult.SUCCESS :
CommandResult.ERROR);
}
private IShopItem? searchItem(string query) {
var item = shop.Items.FirstOrDefault(it
=> it.Id.Equals(query, StringComparison.OrdinalIgnoreCase));
=> it.Name.Equals(query, StringComparison.OrdinalIgnoreCase));
if (item != null) return item;
@@ -74,7 +63,7 @@ public class BuyCommand(IServiceProvider provider) : ICommand {
if (item != null) return item;
item = shop.Items.FirstOrDefault(it
=> it.Name.Contains(query, StringComparison.OrdinalIgnoreCase));
=> it.Description.Contains(query, StringComparison.OrdinalIgnoreCase));
return item;
}

View File

@@ -1,4 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Command;
using TTT.API.Messages;
using TTT.API.Player;
@@ -13,14 +15,15 @@ public class ListCommand(IServiceProvider provider) : ICommand {
public void Dispose() { }
public string Name => "list";
public string Id => "list";
public void Start() { }
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
foreach (var item in shop.Items)
messenger.Message(executor, $"{item.Name} - {item.Description}");
messenger.Message(executor,
$"{ChatColors.Grey}- {ChatColors.White}{item.Name} {ChatColors.Grey}- {item.Description}");
return Task.FromResult(CommandResult.SUCCESS);
}

View File

@@ -2,7 +2,7 @@ using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Player;
using TTT.Game;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.Shop.Commands;
@@ -18,7 +18,7 @@ public class ShopCommand(IServiceProvider provider) : ICommand {
};
public void Dispose() { }
public string Name => "shop";
public string Id => "shop";
public void Start() { }
@@ -27,17 +27,17 @@ public class ShopCommand(IServiceProvider provider) : ICommand {
HashSet<string> sent = [];
if (info.ArgCount == 1) {
foreach (var (_, cmd) in subcommands) {
if (!sent.Add(cmd.Name)) continue;
if (!sent.Add(cmd.Id)) continue;
var uses = cmd.Usage.Where(use => !string.IsNullOrWhiteSpace(use))
.ToList();
var useString =
uses.Count > 0 ? "(" + string.Join(", ", uses) + ")" : "";
if (cmd.Description != null)
info.ReplySync(
$"{locale[GameMsgs.PREFIX]}{ChatColors.White}{cmd.Name} {ChatColors.Grey}- {ChatColors.BlueGrey}{cmd.Description}");
$"{locale[GameMsgs.PREFIX]}{ChatColors.White}{cmd.Id} {ChatColors.Grey}- {ChatColors.BlueGrey}{cmd.Description}");
else
info.ReplySync(
$"{locale[GameMsgs.PREFIX]}{ChatColors.White}{cmd.Name} {ChatColors.Grey}{useString}");
$"{locale[GameMsgs.PREFIX]}{ChatColors.White}{cmd.Id} {ChatColors.Grey}{useString}");
}
return Task.FromResult(CommandResult.SUCCESS);

View File

@@ -1,4 +1,5 @@
using TTT.API.Player;
using ShopAPI;
using TTT.API.Player;
using TTT.Game.Events.Player;
namespace TTT.Shop.Events;

View File

@@ -1,4 +1,5 @@
using TTT.API.Events;
using ShopAPI;
using TTT.API.Events;
using TTT.API.Player;
namespace TTT.Shop.Events;

View File

@@ -0,0 +1,43 @@
using CounterStrikeSharp.API.Core;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
namespace TTT.Shop.Items.Detective.Stickers;
public class StickerListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IIconManager? icons = provider.GetService<IIconManager>();
private readonly IShop shop = provider.GetRequiredService<IShop>();
[UsedImplicitly]
[EventHandler(Priority = Priority.MONITOR)]
public void OnHurt(PlayerDamagedEvent ev) {
if (icons == null || ev.Attacker == null
|| !shop.HasItem<Stickers>(ev.Attacker))
return;
if (Games.ActiveGame is not { State: State.IN_PROGRESS }) return;
if (ev.Weapon == null) return;
if (!ev.Weapon.Contains("taser", StringComparison.OrdinalIgnoreCase))
return;
if (!ev.IsCanceled) return;
var victim = ev.Player;
var attacker = ev.Attacker;
if (attacker == null) return;
var player = converter.GetPlayer(victim);
if (player == null || !player.IsValid) return;
icons.RevealToAll(player.Slot);
Messenger.Message(victim, Locale[StickerMsgs.SHOP_ITEM_STICKERS_HIT]);
}
}

View File

@@ -0,0 +1,14 @@
using TTT.Locale;
namespace TTT.Shop.Items.Detective.Stickers;
public class StickerMsgs {
public static IMsg SHOP_ITEM_STICKERS
=> MsgFactory.Create(nameof(SHOP_ITEM_STICKERS));
public static IMsg SHOP_ITEM_STICKERS_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_STICKERS_DESC));
public static IMsg SHOP_ITEM_STICKERS_HIT
=> MsgFactory.Create(nameof(SHOP_ITEM_STICKERS_HIT));
}

View File

@@ -0,0 +1,41 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game.Roles;
namespace TTT.Shop.Items.Detective.Stickers;
public static class StickerExtensions {
public static void AddStickerServices(this IServiceCollection services) {
services.AddModBehavior<Stickers>();
services.AddModBehavior<StickerListener>();
}
}
public class Stickers(IServiceProvider provider)
: RoleRestrictedItem<DetectiveRole>(provider) {
private readonly StickerConfig config = provider
.GetService<IStorage<StickerConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new StickerConfig();
private readonly IIconManager? icons = provider.GetService<IIconManager>();
public override string Name => Locale[StickerMsgs.SHOP_ITEM_STICKERS];
public override string Description
=> Locale[StickerMsgs.SHOP_ITEM_STICKERS_DESC];
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) { }
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
if (Shop.HasItem(player, this)) return PurchaseResult.ALREADY_OWNED;
return base.CanPurchase(player);
}
}

View File

@@ -0,0 +1,11 @@
using TTT.Locale;
namespace TTT.Shop.Items.M4A1;
public class M4A1Msgs {
public static IMsg SHOP_ITEM_M4A1
=> MsgFactory.Create(nameof(SHOP_ITEM_M4A1));
public static IMsg SHOP_ITEM_M4A1_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_M4A1_DESC));
}

View File

@@ -0,0 +1,40 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game.Roles;
namespace TTT.Shop.Items.M4A1;
public static class M4A1ServiceCollection {
public static void AddM4A1Services(this IServiceCollection collection) {
collection.AddModBehavior<M4A1ShopItem>();
}
}
public class M4A1ShopItem(IServiceProvider provider) : BaseItem(provider) {
private readonly M4A1Config config =
provider.GetService<IStorage<M4A1Config>>()?.Load().GetAwaiter().GetResult()
?? new M4A1Config();
public override string Name => Locale[M4A1Msgs.SHOP_ITEM_M4A1];
public override string Description => Locale[M4A1Msgs.SHOP_ITEM_M4A1_DESC];
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) {
Task.Run(async () => {
foreach (var slot in config.ClearSlots)
await Inventory.RemoveWeaponInSlot(player, slot);
foreach (var weapon in config.Weapons)
await Inventory.GiveWeapon(player, new BaseWeapon(weapon));
});
}
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
return PurchaseResult.SUCCESS;
}
}

View File

@@ -1,5 +1,7 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
@@ -22,38 +24,37 @@ public class DeagleDamageListener(IServiceProvider provider)
[UsedImplicitly]
[EventHandler]
public void OnDamage(PlayerDamagedEvent ev) {
Messenger.Debug("DeagleDamageListener: OnDamage");
if (Games.ActiveGame is not { State: State.IN_PROGRESS }) return;
var attacker = ev.Attacker;
var victim = ev.Player;
if (attacker == null) return;
Messenger.Debug("DeagleDamageListener: Attacker is not null");
var deagleItem = shop.GetOwnedItems(attacker)
.FirstOrDefault(s => s.Id == OneShotDeagle.ID);
.FirstOrDefault(s => s is OneShotDeagle);
if (deagleItem == null) return;
Messenger.DebugAnnounce(
$"DeagleDamageListener: Attacker has deagle item, weapon: {ev.Weapon}");
if (ev.Weapon != config.Weapon) return;
if (ev.Weapon != config.Weapon)
// CS2 specifically causes the weapon to be "weapon_deagle" even if
// the player is holding a revolver, so we need to check for that as well
if (ev.Weapon is not "weapon_deagle"
|| !config.Weapon.Equals("weapon_revolver"))
return;
var attackerRole = Roles.GetRoles(attacker);
var victimRole = Roles.GetRoles(victim);
shop.RemoveItem(attacker, deagleItem);
if (!config.DoesFriendlyFire && attackerRole.Intersect(victimRole).Any()) {
Messenger.DebugAnnounce(
"DeagleDamageListener: Friendly fire is off, roles intersect");
return;
if (attackerRole.Intersect(victimRole).Any()) {
if (config.KillShooterOnFF) attacker.Health = 0;
Messenger.Message(attacker, Locale[DeagleMsgs.SHOP_ITEM_DEAGLE_HIT_FF]);
if (!config.DoesFriendlyFire) {
ev.IsCanceled = true;
return;
}
}
Messenger.DebugAnnounce(
"DeagleDamageListener: One-shot kill conditions met");
if (victim is not IOnlinePlayer onlineVictim) return;
Messenger.DebugAnnounce("DeagleDamageListener: One-shot kill applied");
onlineVictim.Health = 0;
}
}

View File

@@ -8,4 +8,7 @@ public class DeagleMsgs {
public static IMsg SHOP_ITEM_DEAGLE_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_DEAGLE_DESC));
public static IMsg SHOP_ITEM_DEAGLE_HIT_FF
=> MsgFactory.Create(nameof(SHOP_ITEM_DEAGLE_HIT_FF));
}

View File

@@ -1,60 +1,49 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using TTT.API;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Locale;
namespace TTT.Shop.Items;
public static class DeagleServiceCollection {
public static void AddDeagleServices(this IServiceCollection collection) {
collection.AddModBehavior<OneShotDeagle>();
collection.AddModBehavior<DeagleDamageListener>();
}
}
public class OneShotDeagle(IServiceProvider provider) : IWeapon, IShopItem {
public const string ID = "ttt.shop.item.oneshotdeagle";
public class OneShotDeagle(IServiceProvider provider)
: BaseItem(provider), IWeapon {
private readonly OneShotDeagleConfig deagleConfigStorage = provider
.GetService<IStorage<OneShotDeagleConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new OneShotDeagleConfig();
private readonly IInventoryManager inventoryManager =
provider.GetRequiredService<IInventoryManager>();
public override string Name => Locale[DeagleMsgs.SHOP_ITEM_DEAGLE];
private readonly IMsgLocalizer locale =
provider.GetRequiredService<IMsgLocalizer>();
public override string Description
=> Locale[DeagleMsgs.SHOP_ITEM_DEAGLE_DESC];
public string Name => locale[DeagleMsgs.SHOP_ITEM_DEAGLE];
public void Start() { }
public void Dispose() { }
public string Description => locale[DeagleMsgs.SHOP_ITEM_DEAGLE_DESC];
public ShopItemConfig Config => deagleConfigStorage;
public void OnPurchase(IOnlinePlayer player) {
inventoryManager.GiveWeapon(player, this);
}
public PurchaseResult CanPurchase(IOnlinePlayer player) {
return PurchaseResult.SUCCESS;
}
string IShopItem.Id => ID;
public override ShopItemConfig Config => deagleConfigStorage;
public string WeaponId => deagleConfigStorage.Weapon;
public int? ReserveAmmo { get; init; } = 0;
public int? CurrentAmmo { get; init; } = 1;
}
public record OneShotDeagleConfig : ShopItemConfig {
public override int Price { get; init; } = 100;
public bool DoesFriendlyFire { get; init; } = true;
public string Weapon { get; init; } = "revolver";
public override void OnPurchase(IOnlinePlayer player) {
Task.Run(async () => {
await Inventory.RemoveWeaponInSlot(player,
deagleConfigStorage.WeaponSlot);
await Inventory.GiveWeapon(player, this);
});
}
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
return PurchaseResult.SUCCESS;
}
}

View File

@@ -0,0 +1,10 @@
using TTT.Locale;
namespace TTT.Shop.Items.Traitor.C4;
public class C4Msgs {
public static IMsg SHOP_ITEM_C4_DESC =
MsgFactory.Create(nameof(SHOP_ITEM_C4_DESC));
public static IMsg SHOP_ITEM_C4 => MsgFactory.Create(nameof(SHOP_ITEM_C4));
}

View File

@@ -0,0 +1,64 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
using TTT.API.Events;
using TTT.API.Extensions;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game.Events.Game;
using TTT.Game.Roles;
namespace TTT.Shop.Items.Traitor.C4;
public static class C4ServiceCollection {
public static void AddC4Services(this IServiceCollection collection) {
collection.AddModBehavior<C4ShopItem>();
}
}
public class C4ShopItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider), IListener {
private readonly C4Config config = provider.GetService<IStorage<C4Config>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new C4Config();
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
private int c4sBought;
public override string Name => Locale[C4Msgs.SHOP_ITEM_C4];
public override string Description => Locale[C4Msgs.SHOP_ITEM_C4_DESC];
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) {
Inventory.GiveWeapon(player, new BaseWeapon(config.Weapon));
}
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
if (config.MaxC4PerRound > 0)
if (c4sBought > config.MaxC4PerRound)
return PurchaseResult.ITEM_NOT_PURCHASABLE;
if (config.MaxC4AtOnce > 0) {
var count = 0;
if (finder.GetOnline()
.Where(p => Shop.HasItem<C4ShopItem>(p))
.Any(_ => count++ >= config.MaxC4AtOnce))
return PurchaseResult.ITEM_NOT_PURCHASABLE;
}
return base.CanPurchase(player);
}
[UsedImplicitly]
[EventHandler]
public void OnRoundStart(GameStateUpdateEvent ev) {
if (ev.NewState != State.IN_PROGRESS) return;
c4sBought = 0;
}
}

View File

@@ -0,0 +1,35 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game.Roles;
namespace TTT.Shop.Items.Traitor.Gloves;
public static class GlovesServiceCollection {
public static void AddGlovesServices(this IServiceCollection collection) {
collection.AddModBehavior<GlovesItem>();
collection.AddModBehavior<GlovesListener>();
}
}
public class GlovesItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider) {
private readonly GlovesConfig config =
provider.GetService<IStorage<GlovesConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new GlovesConfig();
public override string Name => Locale[GlovesMsgs.SHOP_ITEM_GLOVES];
public override string Description
=> Locale[GlovesMsgs.SHOP_ITEM_GLOVES_DESC];
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) { }
}

View File

@@ -0,0 +1,75 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs.Traitor;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game.Events.Body;
using TTT.Game.Events.Game;
using TTT.Game.Listeners;
using TTT.Shop.Events;
namespace TTT.Shop.Items.Traitor.Gloves;
public class GlovesListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly GlovesConfig item =
provider.GetService<IStorage<GlovesConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new GlovesConfig();
private readonly IShop shop = provider.GetRequiredService<IShop>();
private readonly Dictionary<IPlayer, int> uses = new();
[UsedImplicitly]
[EventHandler]
public void OnPurchase(PlayerPurchaseItemEvent ev) {
if (ev.Item is not GlovesItem) return;
uses[ev.Player] = item.MaxUses;
}
[UsedImplicitly]
[EventHandler]
public void BodyCreate(BodyCreateEvent ev) {
if (ev.Body.Killer == null || !useGloves(ev.Body.Killer)) return;
if (ev.Body.Killer is not IOnlinePlayer online) return;
ev.Body.Killer = null;
Messenger.Message(online,
Locale[
GlovesMsgs.SHOP_ITEM_GLOVES_USED_KILL(uses[online], item.MaxUses)]);
}
[UsedImplicitly]
[EventHandler]
public void BodyIdentify(BodyIdentifyEvent ev) {
if (ev.Identifier == null || !useGloves(ev.Identifier)) return;
ev.IsCanceled = true;
Messenger.Message(ev.Identifier,
Locale[
GlovesMsgs.SHOP_ITEM_GLOVES_USED_BODY(uses[ev.Identifier],
item.MaxUses)]);
}
[UsedImplicitly]
[EventHandler]
public void OnGameState(GameStateUpdateEvent ev) {
if (ev.NewState != State.IN_PROGRESS) return;
uses.Clear();
}
private bool useGloves(IPlayer player) {
uses.TryGetValue(player, out var useCount);
if (useCount <= 0) return false;
uses[player] = useCount - 1;
if (useCount - 1 > 0) return true;
if (player is not IOnlinePlayer online) return true;
shop.RemoveItem<GlovesItem>(online);
Messenger.Message(online, Locale[GlovesMsgs.SHOP_ITEM_GLOVES_WORN_OUT]);
return true;
}
}

View File

@@ -0,0 +1,24 @@
using TTT.Locale;
namespace TTT.Shop.Items.Traitor.Gloves;
public class GlovesMsgs {
public static IMsg SHOP_ITEM_GLOVES
=> MsgFactory.Create(nameof(SHOP_ITEM_GLOVES));
public static IMsg SHOP_ITEM_GLOVES_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_GLOVES_DESC));
public static IMsg SHOP_ITEM_GLOVES_WORN_OUT
=> MsgFactory.Create(nameof(SHOP_ITEM_GLOVES_WORN_OUT));
public static IMsg SHOP_ITEM_GLOVES_USED_BODY(int usesLeft, int maxUses) {
return MsgFactory.Create(nameof(SHOP_ITEM_GLOVES_USED_BODY), usesLeft,
maxUses);
}
public static IMsg SHOP_ITEM_GLOVES_USED_KILL(int usesLeft, int maxUses) {
return MsgFactory.Create(nameof(SHOP_ITEM_GLOVES_USED_KILL), usesLeft,
maxUses);
}
}

View File

@@ -1,17 +1,51 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.Game.Events.Body;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
namespace TTT.Shop.Listeners;
public class PlayerKillListener(IServiceProvider provider) : IListener {
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
public void Dispose() { bus.UnregisterListener(this); }
public class PlayerKillListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly IShop shop = provider.GetRequiredService<IShop>();
[UsedImplicitly]
[EventHandler]
public void OnKill(PlayerDeathEvent ev) { }
public async Task OnKill(PlayerDeathEvent ev) {
if (Games.ActiveGame is { State: State.IN_PROGRESS }) return;
if (ev.Killer == null) return;
var victimBal = await shop.Load(ev.Victim);
shop.AddBalance(ev.Killer, victimBal / 6, "Killed " + ev.Victim.Name);
}
[UsedImplicitly]
[EventHandler]
public async Task OnIdentify(BodyIdentifyEvent ev) {
if (ev.Identifier == null) return;
var victimBal = await shop.Load(ev.Body.OfPlayer);
shop.AddBalance(ev.Identifier, victimBal / 4,
"Identified " + ev.Body.OfPlayer.Name);
if (ev.Body.Killer is not IOnlinePlayer killer) return;
if (!isGoodKill(ev.Body.Killer, ev.Body.OfPlayer)) {
var killerBal = await shop.Load(killer);
shop.AddBalance(killer, -killerBal / 4,
ev.Body.OfPlayer.Name + " kill invalidated");
return;
}
shop.AddBalance(killer, victimBal / 4,
ev.Body.OfPlayer.Name + " kill validated");
}
private bool isGoodKill(IPlayer attacker, IPlayer victim) {
return !Roles.GetRoles(attacker).Intersect(Roles.GetRoles(victim)).Any();
}
}

View File

@@ -1,4 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using TTT.API.Events;
using TTT.API.Player;
using TTT.API.Storage;
@@ -15,6 +18,7 @@ public class RoleAssignCreditor(IServiceProvider provider)
private readonly IShop shop = provider.GetRequiredService<IShop>();
[UsedImplicitly]
[EventHandler]
public void OnRoleAssign(PlayerRoleAssignEvent ev) {
var toGive = config.StartingCreditsForRole(ev.Role);

View File

@@ -1,5 +1,6 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Events;
using TTT.API.Game;
using TTT.Game.Events.Game;

View File

@@ -1,11 +1,11 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API;
using TTT.API.Events;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.Locale;
using TTT.Shop.Events;
using TTT.Shop.Items;
namespace TTT.Shop;
@@ -26,14 +26,40 @@ public class Shop(IServiceProvider provider) : ITerrorModule, IShop {
public ISet<IShopItem> Items { get; } = new HashSet<IShopItem>();
public bool RegisterItem(IShopItem item) {
item.Start();
return Items.Add(item);
}
public bool RegisterItem(IShopItem item) { return Items.Add(item); }
public PurchaseResult TryPurchase(IOnlinePlayer player, IShopItem item,
bool printReason = true) {
return PurchaseResult.UNKNOWN_ERROR;
var cost = item.Config.Price;
var bal = balances.GetValueOrDefault(player.Id, 0);
if (cost > bal) {
if (printReason)
messenger?.Message(player,
localizer[ShopMsgs.SHOP_INSUFFICIENT_BALANCE(item, bal)]);
return PurchaseResult.INSUFFICIENT_FUNDS;
}
var canPurchase = item.CanPurchase(player);
if (canPurchase != PurchaseResult.SUCCESS) {
if (!printReason) return canPurchase;
if (canPurchase == PurchaseResult.UNKNOWN_ERROR)
messenger?.Message(player, localizer[ShopMsgs.SHOP_CANNOT_PURCHASE]);
else
messenger?.Message(player,
localizer[
ShopMsgs.SHOP_CANNOT_PURCHASE_WITH_REASON(
canPurchase.ToMessage())]);
return canPurchase;
}
AddBalance(player, -cost, item.Name);
GiveItem(player, item);
if (printReason)
messenger?.Message(player, localizer[ShopMsgs.SHOP_PURCHASED(item)]);
return PurchaseResult.SUCCESS;
}
public void AddBalance(IOnlinePlayer player, int amount, string reason = "",
@@ -66,6 +92,7 @@ public class Shop(IServiceProvider provider) : ITerrorModule, IShop {
public void GiveItem(IOnlinePlayer player, IShopItem item) {
if (!items.ContainsKey(player.Id)) items[player.Id] = [];
items[player.Id].Add(item);
item.OnPurchase(player);
}
public IList<IShopItem> GetOwnedItems(IOnlinePlayer player) {
@@ -88,5 +115,5 @@ public class Shop(IServiceProvider provider) : ITerrorModule, IShop {
Items.Clear();
}
public void Start() { RegisterItem(new OneShotDeagle(provider)); }
public void Start() { }
}

View File

@@ -16,6 +16,8 @@
<ItemGroup>
<ProjectReference Include="..\API\API.csproj"/>
<ProjectReference Include="..\Game\Game.csproj"/>
<ProjectReference Include="..\CS2\CS2.csproj"/>
<ProjectReference Include="..\ShopAPI\ShopAPI.csproj"/>
</ItemGroup>
</Project>

View File

@@ -1,39 +0,0 @@
namespace TTT.Shop;
public enum PurchaseResult {
/// <summary>
/// The purchase was successful.
/// </summary>
SUCCESS,
/// <summary>
/// The player does not have enough funds to purchase the item.
/// </summary>
INSUFFICIENT_FUNDS,
/// <summary>
/// The item was not found in the shop or does not exist.
/// </summary>
ITEM_NOT_FOUND,
/// <summary>
/// The player cannot purchase this item, either due to per-player restrictions
/// or the item being unavailable for purchase at the moment.
/// </summary>
ITEM_NOT_PURCHASABLE,
/// <summary>
/// An event canceled the purchase.
/// </summary>
PURCHASE_CANCELED,
/// <summary>
/// An unknown error occurred during the purchase process.
/// </summary>
UNKNOWN_ERROR,
/// <summary>
/// The item cannot be purchased multiple times, and the player already owns it.
/// </summary>
ALREADY_OWNED
}

View File

@@ -1,7 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Extensions;
using TTT.CS2.Items.Camouflage;
using TTT.CS2.Items.DNA;
using TTT.CS2.Items.Station;
using TTT.Shop.Commands;
using TTT.Shop.Items;
using TTT.Shop.Items.Detective.Stickers;
using TTT.Shop.Items.M4A1;
using TTT.Shop.Items.Traitor.C4;
using TTT.Shop.Items.Traitor.Gloves;
using TTT.Shop.Listeners;
namespace TTT.Shop;
@@ -17,6 +25,14 @@ public static class ShopServiceCollection {
collection.AddModBehavior<BuyCommand>();
collection.AddModBehavior<BalanceCommand>();
collection.AddC4Services();
collection.AddCamoServices();
collection.AddDamageStation();
collection.AddDeagleServices();
collection.AddDnaScannerServices();
collection.AddGlovesServices();
collection.AddHealthStation();
collection.AddM4A1Services();
collection.AddStickerServices();
}
}

View File

@@ -1,3 +1,4 @@
using ShopAPI;
using TTT.Locale;
namespace TTT.Shop;
@@ -7,6 +8,16 @@ public static class ShopMsgs {
public static IMsg CREDITS_NAME => MsgFactory.Create(nameof(CREDITS_NAME));
public static IMsg SHOP_CANNOT_PURCHASE
=> MsgFactory.Create(nameof(SHOP_CANNOT_PURCHASE));
public static IMsg SHOP_PURCHASED(IShopItem item)
=> MsgFactory.Create(nameof(SHOP_PURCHASED), item.Name);
public static IMsg SHOP_ITEM_NOT_FOUND(string query) {
return MsgFactory.Create(nameof(SHOP_ITEM_NOT_FOUND), query);
}
public static IMsg CREDITS_GIVEN(int amo) {
return MsgFactory.Create(nameof(CREDITS_GIVEN), amo > 0 ? "+" : "-",
Math.Abs(amo));
@@ -16,4 +27,13 @@ public static class ShopMsgs {
return MsgFactory.Create(nameof(CREDITS_GIVEN_REASON), amo > 0 ? "+" : "-",
Math.Abs(amo), reason);
}
public static IMsg SHOP_INSUFFICIENT_BALANCE(IShopItem item, int bal) {
return MsgFactory.Create(nameof(SHOP_INSUFFICIENT_BALANCE), item.Name,
item.Config.Price, bal);
}
public static IMsg SHOP_CANNOT_PURCHASE_WITH_REASON(string reason) {
return MsgFactory.Create(nameof(SHOP_CANNOT_PURCHASE_WITH_REASON), reason);
}
}

View File

@@ -1,6 +1,31 @@
SHOP_INACTIVE: "%PREFIX%The shop is currently closed."
SHOP_ITEM_DEAGLE: "One-Hit Deagle"
SHOP_ITEM_DEAGLE_DESC: "A one-hit kill deagle with a single bullet. Aim carefully!"
SHOP_ITEM_NOT_FOUND: "%PREFIX%Could not find an item named \"{default}{0}{grey}\"."
SHOP_ITEM_DEAGLE: "One-Hit Revolver"
SHOP_ITEM_DEAGLE_DESC: "A one-hit kill revolver with a single bullet. Aim carefully!"
SHOP_ITEM_DEAGLE_HIT_FF: "You hit a teammate!"
SHOP_ITEM_STICKERS: "Stickers"
SHOP_ITEM_STICKERS_DESC: "Reveal the roles of all players you taser to others."
SHOP_ITEM_STICKERS_HIT: "%PREFIX%You got stickered, your role is now visible to everyone."
SHOP_ITEM_C4: "C4 Explosive"
SHOP_ITEM_C4_DESC: "A powerful explosive that blows up after a delay."
SHOP_ITEM_M4A1: "M4A1 Rifle and USP-S"
SHOP_ITEM_M4A1_DESC: "A fully automatic rifle with a silencer accompanied by a silenced pistol."
SHOP_ITEM_GLOVES: "Gloves"
SHOP_ITEM_GLOVES_DESC: "Lets you kill without DNA being left behind, or move bodies without identifying the body."
SHOP_ITEM_GLOVES_USED_BODY: "%PREFIX%You used your gloves to move a body without leaving DNA. ({yellow}{0}{grey}/{yellow}{1}{grey} use%s% left)."
SHOP_ITEM_GLOVES_USED_KILL: "%PREFIX%You used your gloves to kill without leaving DNA evidence. ({yellow}{0}{grey}/{yellow}{1}{grey} use%s% left)."
SHOP_ITEM_GLOVES_WORN_OUT: "%PREFIX%Your gloves worn out."
SHOP_INSUFFICIENT_BALANCE: "%PREFIX%You cannot afford {white}{0}{grey}, it costs {yellow}{1}{grey} credit%s%, and you have {yellow}{2}{grey}."
SHOP_CANNOT_PURCHASE: "%PREFIX%You cannot purchase this item."
SHOP_CANNOT_PURCHASE_WITH_REASON: "%PREFIX%You cannot purchase this item: {red}{0}{grey}."
SHOP_PURCHASED: "%PREFIX%You purchased {white}{0}{grey}."
CREDITS_NAME: "credit"
CREDITS_GIVEN: "%PREFIX%{0}{1} %CREDITS_NAME%%s%"
CREDITS_GIVEN_REASON: "%PREFIX%{0}{1} %CREDITS_NAME%%s% {grey}({white}{2}{grey})"

31
TTT/ShopAPI/BaseItem.cs Normal file
View File

@@ -0,0 +1,31 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs;
using TTT.API.Player;
using TTT.API.Role;
using TTT.Locale;
namespace ShopAPI;
public abstract class BaseItem(IServiceProvider provider) : IShopItem {
protected readonly IInventoryManager Inventory =
provider.GetRequiredService<IInventoryManager>();
protected readonly IMsgLocalizer Locale =
provider.GetRequiredService<IMsgLocalizer>();
protected readonly IServiceProvider Provider = provider;
protected readonly IRoleAssigner Roles =
provider.GetRequiredService<IRoleAssigner>();
protected readonly IShop Shop = provider.GetRequiredService<IShop>();
public virtual void Dispose() { }
public abstract string Name { get; }
public abstract string Description { get; }
public abstract ShopItemConfig Config { get; }
public abstract void OnPurchase(IOnlinePlayer player);
public abstract PurchaseResult CanPurchase(IOnlinePlayer player);
public virtual void Start() { Shop.RegisterItem(this); }
}

View File

@@ -0,0 +1,6 @@
namespace ShopAPI.Configs;
public record CamoConfig : ShopItemConfig {
public override int Price { get; init; } = 100;
public float CamoVisibility { get; init; } = 0.5f;
}

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