Compare commits

...

186 Commits

Author SHA1 Message Date
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
MSWS
b427dc370e refactor: Remove debug logs and adjust event priorities
- Remove debug logging statements and simplify service registration logic in `ServiceCollectionExtensions.cs`
- Adjust the event handler's priority in `RoundShopClearer.cs` without functional changes
- Update wording in `README.md` for clearer public API usage
- Modify event handler priority in `PlayerStatsTracker.cs` while maintaining existing functionality
- Streamline `IOnlinePlayer.cs` by removing obsolete commented-out code and refining interface properties
- Lower event handler priority in `PlayerActionsLogger.cs` for player kills
2025-09-28 00:46:44 -07:00
MSWS
ede9badbd9 Add additional unit tests for shop 2025-09-28 00:34:55 -07:00
MSWS
5736588484 refactor: Rename Id to WeaponId across the codebase
- Rename property `Id` to `WeaponId` in `IWeapon.cs`, `BaseWeapon.cs`, and `OneShotDeagle.cs` for improved clarity.
- Update weapon removal method in `IInventoryManager.cs` to use `weapon.WeaponId`.
- Refactor `PlayerDamagedEvent.cs` to initialize `Weapon` property with `init` for stricter immutability.
- Revise `IsAlive` logic in `TestPlayer.cs` to adjust `Health` based on `IsAlive` status; deprecate the `Roles` property.
- Add `using` directive and `[UsedImplicitly]` attribute to `DeagleDamageListener.cs` for dependency management and traceability.
- Develop `DeagleTests.cs` to ensure proper functionality of Deagle weapon behaviors using Xunit.
2025-09-28 00:16:28 -07:00
MSWS
8a894c65e8 refactor: Adjust player color handling logic
- Adjust the alpha value handling in `SetColor` method within `PlayerExtensions.cs` to ensure it stays within limits.
- Simplify player color setting in `RoundTimerListener.cs` by using `Color.White` for improved readability.
- Update `BodySpawner.cs` to change player post-death color to fully opaque white and simplify round start color setting using `Color.White`.
2025-09-27 20:30:07 -07:00
MSWS
0634af8ad8 feat: Add weapon slot removal functionality
- Fix typo in comment and clarify kill detection process in `PlayerStatsTracker.cs`
- Add `RemoveWeaponInSlot` method for slot-based weapon removal in `FakeInventoryManager.cs` and update `IInventoryManager.cs` for enhanced functionality
- Reformat `BaseWeapon` constructor for readability and standardize file formatting
- Simplify `CS2Body.cs` API by removing overloaded weapon method and reinforce `IWeapon` interface usage
- Refactor `CS2InventoryManager.cs` for improved weapon management, add methods for slot conversion and weapon removal, and streamline code structure
- Add debug messaging to `DeagleDamageListener.cs` for better runtime clarity on friendly fire and one-shot kill conditions
- Modify `BodySpawner.cs` to incorporate `BaseWeapon` wrapper for improved weapon management without affecting core functionality
- Adjust `OnPlayerKill` event handler priority in `PlayerActionsLogger.cs` to ensure kill logging before game ends
2025-09-27 20:25:53 -07:00
MSWS
9f6c3f7be4 Check player count on round start before starting 2025-09-27 19:07:28 -07:00
MSWS
9eb313e9f1 feat: Introduce screen color features and state command
```
Enhance game management and command structure with new features and optimizations

- Add logic in `PlayerJoinStarting.cs` to handle failure in game instance creation and ensure game starts only upon successful creation.
- Introduce IMessenger service in `CS2GameManager.cs` using dependency injection; add debug logging for the game creation process.
- Implement new behavior for `ScreenColorApplier` in `CS2ServiceCollection.cs` to enhance listening capabilities.
- Create a new `StateCommand` in `StateCommand.cs` to check active game state and implement basic command lifecycle methods.
- Remove "screentext" command and add "state" and "screencolor" commands in `TestCommand.cs` to revamp command options.
- Simplify vector operations in `TextSpawner.cs` by replacing `GetRightVector`; enhance screen text positioning.
- Expand `VectorExtensions.cs` with `ToRight` and `ToUp` methods for better angle-to-vector conversions.
- Remove `ScreenTextCommand.cs` to streamline project and potentially refactor screen text feature.
- Add `ScreenColorApplier` to apply screen color effects upon role assignment, enhancing player interaction experience.
- Introduce `ScreenColorCommand` for player screen color effects with configurable parameters, enriching command functionality.
```
2025-09-26 20:28:35 -07:00
MSWS
9b5563aa8e feat: Refactor text positioning and add screen fade effects
- Refactor `TextSpawner.cs` to improve angle calculations and text positioning with respect to the player.
- Replace static `screenAngle` with local computations for better clarity and maintainability in `TextSpawner.cs`.
- Introduce `angle` object in `TextSpawner.cs` to enhance readability and explicitness in rotation management.
- Add `FadeFlags` enum and implement `ColorScreen` method in `PlayerExtensions.cs` to manage screen color fade effects.
- Enhance color fade handling in `PlayerExtensions.cs` by utilizing `UserMessage` with custom flag settings and improved color configurations.
2025-09-26 18:47:18 -07:00
MSWS
2058d0c780 Start work on screen text 2025-09-26 18:29:59 -07:00
Isaac
ee7f34b435 Bump the nuget group with 1 update (#58)
Pinned System.Text.Json at 8.0.5.

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts page](https://github.com/MSWS/TTT/network/alerts).

</details>
2025-09-26 18:00:40 -07:00
Isaac
783f6da6dd Merge branch 'main' into dependabot/nuget/Locale/nuget-9df58bc52d 2025-09-26 17:58:41 -07:00
MSWS
f245c61d01 AI is the future! 2025-09-25 22:18:41 -07:00
MSWS
b4076934d8 Fix yml? 2025-09-25 22:15:48 -07:00
MSWS
21b869507b Add GitVersion config, include gamedata in releases 2025-09-25 22:09:43 -07:00
dependabot[bot]
fca81c0577 Bump the nuget group with 1 update
Bumps System.Text.Json from 8.0.0 to 8.0.5

---
updated-dependencies:
- dependency-name: System.Text.Json
  dependency-version: 8.0.5
  dependency-type: direct:production
  dependency-group: nuget
- dependency-name: System.Text.Json
  dependency-version: 8.0.5
  dependency-type: direct:production
  dependency-group: nuget
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-26 04:40:30 +00:00
Isaac
db8fe8f069 Bug Fixes, Foundational Work for Shop and Karma (#57) 2025-09-25 21:37:10 -07:00
Isaac
dce6d759a8 Merge branch 'main' into dev 2025-09-25 21:26:55 -07:00
MSWS
f028074939 Tweak karma update interval 2025-09-25 21:26:34 -07:00
MSWS
1d96be0cb0 Remove verbose debug 2025-09-25 21:22:47 -07:00
MSWS
0cbb931aaa Additional buy test command 2025-09-25 21:22:22 -07:00
MSWS
6c2bd538a9 Fix buy tests 2025-09-25 21:20:58 -07:00
MSWS
6b0dcbd42f Resolve log-based test failures 2025-09-25 21:09:00 -07:00
MSWS
c512b60260 Reformat & Cleanup 2025-09-25 21:01:50 -07:00
MSWS
295f2bcad0 Fix compile error 2025-09-25 19:48:57 -07:00
MSWS
5b46fb1282 feat: Introduce player state spoofing feature
```
Implement player state spoofing and enhance body identification

- Introduce `CS2AliveSpoofer` class and `IAliveSpoofer` interface to manage "fake alive" player states in `CombatHandler` and `ForceAliveCommand`.
- Modify `BODY_IDENTIFIED` logic in `GameMsgs.cs` for nullable identifiers, providing a default "Someone" message when missing.
- Update `ForceAliveCommand` and `IdentifyAllCommand` to use new spoofing and identification utilities.
- Enhance body tracking with `BodyTracker` class, integrating event handling improvements for game state changes and body creation.
- Refine `OneShotDeagle` functionality by clearing player's inventory before assigning the weapon, improving game balance.
- Adjust `BodyPickupListener` to use `IBodyTracker` for efficient body management and consolidate event handling logic.
- Update various components to utilize dependency injection and new APIs, improving service management and system robustness.
```
2025-09-25 19:42:50 -07:00
MSWS
4b3d9335b5 Merge branch 'dev' of github.com:MSWS/TTT into dev 2025-09-25 16:36:57 -07:00
MSWS
ec48a9b243 Fix role formatting, pad names (resolves #52 & #56) 2025-09-25 16:36:47 -07:00
MSWS
254e5539b7 Fix role formatting (resolves #52) 2025-09-25 16:35:27 -07:00
MSWS
547228d09a Clarify name in combathandler 2025-09-25 16:23:43 -07:00
MSWS
99e4c6fc07 refactor: Refactor game logic (resolves #51)
- Modify `RoundBasedGame.cs` to improve encapsulation, consistency, and maintainability by refactoring game-ending logic and updating access modifiers for subclassing.
- Implement player data caching in `CS2Player.cs` to enhance performance and improve debug logging capabilities.
- Refactor message formatting in `CS2Messenger.cs` for improved consistency in debug modes.
- Enhance `PlayerConnectionsHandler.cs` by adding asynchronous behavior and a dependency on `IMessenger` for potential future enhancements.
- Add a validity check in `RoundTimerListener.cs` to ensure team scores are only updated with a defined winning role, improving robustness.
- Update `CS2Game.cs` to broadcast notifications for game start states and handle countdowns effectively.
- Introduce `CheckEndConditions()` method in `IGame.cs` interface and deprecate `IsInProgress()` to promote explicit state management.
- Centralize and simplify end game logic in `PlayerCausesEndListener.cs` by implementing `CheckEndConditions()`, eliminating redundant code.
2025-09-25 16:05:48 -07:00
MSWS
52b8d1d2ff refactor: Refactor player leave handling logic and tests (ref: #51)
- Rename test method `Round_EndsWhen_PlayerLeaves` to `Round_EndsWhen_PlayerLeavesAndDies` in `RoundEndingTest.cs` and add `PlayerDiesOnLeaveListener`.
- Improve code efficiency and readability in player iteration and event creation in `PlayerConnectionsHandler.cs`.
- Remove redundant console logging for player disconnections and streamline control flow in `disconnectFromServer` method in `PlayerConnectionsHandler.cs`.
- Move dispatching of `PlayerLeaveEvent` to the next world update for consistency in `PlayerConnectionsHandler.cs`.
2025-09-25 15:37:17 -07:00
MSWS
e901d82153 feat: Refactor spawn logic; add LateSpawnListener
- Reorder `AddModBehavior` entries in `CS2ServiceCollection.cs` for improved readability and logical grouping, adding `BodySpawner`, `DamageCanceler`, and `LateSpawnListener` earlier in their sections and maintaining order consistency by moving various handlers.
- Remove duplicated entries and ensure all necessary handlers and listeners are included in `CS2ServiceCollection.cs`.
- Add new class `LateSpawnListener` in `Listeners/LateSpawnListener.cs` to handle player join events, implementing a respawn mechanism for new players joining when the game is not in progress.
- Remove game state condition and deferred respawn logic in `PlayerConnectionsHandler.cs`, changing player respawn behavior upon joining the server.
2025-09-25 15:28:24 -07:00
MSWS
f56106f125 Merge branch 'dev' of github.com:MSWS/TTT into dev 2025-09-25 15:23:09 -07:00
MSWS
57660e0957 Fix kill listener priority conflicting with round states (resolves #55) 2025-09-25 15:23:03 -07:00
MSWS
1ecef5767f Fix kill listener priority conflicting with round states (resolves 55) 2025-09-25 15:22:40 -07:00
MSWS
d1359b79c0 feat: Introduce shop module with event-driven updates (fixes #54)
```
Integrate New Features and Enhance Codebase

- Add a new project reference to `CS2.csproj` for integration with the Shop project.
- Introduce a new configuration class `CS2ShopConfig` to manage shop settings and player interactions.
- Implement new player event handling with `PlayerPurchaseItemEvent` and `PlayerBalanceEvent`, enabling cancellable item purchases and balance updates.
- Enhance the `ShopServiceCollection` with new behaviors for extended functionality, including `RoleAssignCreditor`, `BuyCommand`, and `BalanceCommand`.
- Improve `Shop.cs` by replacing `IPlayer` references with string identifiers, introducing event-driven balance updates, and enhancing messaging logic.
- Comprehensive changes across multiple utilities and configurations to support new team score management and event-driven architecture.
```
2025-09-25 15:02:58 -07:00
Isaac
216d5a9d5a Update issue templates (#50) 2025-09-25 13:43:52 -07:00
Isaac
5d2bead09c Update issue templates 2025-09-25 13:42:42 -07:00
MSWS
a0ce6aa53e Additional cleaning up and unit tests 2025-09-25 08:01:42 -07:00
MSWS
cb626d7cfa Fix README typo 2025-09-24 23:54:26 -07:00
MSWS
d83ac95245 Merge branch 'dev' of github.com:MSWS/TTT into dev 2025-09-24 23:49:04 -07:00
MSWS
8db9b3154d Update licenses 2025-09-24 23:48:58 -07:00
Isaac
3abe6153e3 Feat/karma (#48) 2025-09-24 23:47:59 -07:00
MSWS
fc25405a81 Reorganize CS2ServiceCollection 2025-09-24 23:38:40 -07:00
MSWS
da3a94f3b7 Fix TTT/Plugin README link 2025-09-24 23:34:49 -07:00
MSWS
4bf7b0cd13 Merge dev 2025-09-24 23:29:16 -07:00
MSWS
758d3fe13c refactor: Refactor and clean up event and listener logic
Refactor and Simplify Codebase with Improved Readability and Efficiency

- **OneShotDeagle.cs:**
  - Remove event-related dependencies and streamline class to focus on core functionality.

- **PlayerKillListener.cs:**
  - Simplify `OnKill` method, preparing for future enhancements, and clean up unused code.

- **IListener.cs and IEventBus.cs:**
  - Remove `IDisposable` dependency from `IListener`. Add obsolescence message for `RegisterListener` in `IEventBus`.

- **LogsCommand.cs and TTTCommand.cs:**
  - Consolidate logic for game checking and logging, enhancing error handling and readability.

- **RoundTimerListener.cs and CombatHandler.cs:**
  - Refactor event handlers for clear separation of responsibilities and process optimization.

- **Various CS2 Handlers and Commands:**
  - Streamline and modularize event handling and role management with newly introduced helper methods across handlers.
  - Refactor command logic for improved organization and maintenance.

- **Locale/StringLocalizer.cs and EventBus.cs:**
  - Refactor string handling and listener registration for enhanced clarity and performance.
2025-09-24 23:27:05 -07:00
MSWS
9045913074 feat: Implement role management via dependency injection
```
- Simplify and enhance `PlayerActionsLogger.cs` by using pattern matching for game status checks and improve logging calls with a `Provider` parameter.
- Update `DeathAction.cs` to support dependency injection and role management by modifying constructors and formatting methods.
- Add role management in `FakeAction.cs` by introducing `PlayerRole` and `OtherRole` properties.
- Mark `IsGameActive` in `IGameManager.cs` as obsolete to guide towards direct game state checks.
- Modify logging in `BodyIdentifyLogger.cs` to use a `Provider` parameter for better context.
- Improve role management in `IdentifyBodyAction.cs` with new properties and constructor overloads supporting dependency injection.
- Refactor `ActionTest.cs` to accommodate constructor changes in `FakeAction`, ensuring backward compatibility.
- Expand `IAction` interface with role properties and enhance `Format` method.
- Add footer text in `en.yml` and `GameMsgs.cs` for clearer game log outputs.
- Enhance `DamagedAction.cs` with dependency injection and role properties, improving role management.
- Adjust `CombatHandler.cs` to refine player event handling and platform checks.
- Clear up documentation comments in `StringLocalizer.cs`.
- Enhance `SimpleLogger.cs` with role management and improved log formatting.
- Deprecate `IsInProgress()` in `IGame` for more explicit game state checks.
```
2025-09-24 20:04:22 -07:00
MSWS
8748401b6b Basic shop balance giving 2025-09-24 17:06:01 -07:00
MSWS
7d5218914f Merge branch 'dev' into feat/karma 2025-09-24 16:26:45 -07:00
MSWS
137144a052 Reformat Raytrace: 2025-09-24 16:22:00 -07:00
MSWS
61d7f667ff Reformat and cleanup 2025-09-23 18:11:46 -07:00
MSWS
f53c00c4d8 Reformat and cleanup 2025-09-23 18:10:51 -07:00
MSWS
3f5a675af1 Fix unit tests 2025-09-23 18:08:26 -07:00
MSWS
48ec384b38 Fix unit tests 2025-09-23 18:07:21 -07:00
MSWS
2edee31419 Fix EventBus impl 2025-09-23 17:59:18 -07:00
MSWS
d4006e5750 Resolve merge conflicts 2025-09-23 17:57:33 -07:00
MSWS
a1a37452ef refactor: Refactor round end logic and enhance message style.
- Enhance message styling in `en.yml` by adding grey color to `TASER_SCANNED` translation.
- Align logical flow in `RoundTimerListener.cs` by moving the remaining time check in `OnRoundEnd`.
- Clean up `RoundTimerListener.cs` by removing commented-out code related to `EventRoundEnd`.
- Maintain role setting and team switching functionality in `RoundTimerListener.cs` within `OnRoundEnd`.
- Adjust winning team logic and event firing in `RoundTimerListener.cs` based on end reason.
- Ensure correct round transitions in `RoundTimerListener.cs` using scheduled timing logic.
2025-09-23 17:38:03 -07:00
MSWS
d9f002febe Fix body systems 2025-09-23 17:20:26 -07:00
MSWS
26cca670ee Add gamedata 2025-09-23 16:46:51 -07:00
MSWS
cafd050a85 Remove debugs 2025-09-23 16:39:22 -07:00
MSWS
a28c3aa0dd Overhaul raytracing 2025-09-23 13:04:40 -07:00
MSWS
2918a9965d Update raytrace sigs 2025-09-23 12:42:17 -07:00
MSWS
b57630b899 feat: Add taser scan message and role assignment logic
- Add a new translation entry for "TASER_SCANNED" in TTT/CS2/lang/en.yml, ensuring a dynamic message format.
- Enhance TTT/Game/Listeners/BaseListener.cs with dependencies for role assignment and message localization.
- Refactor TTT/Game/Events/Player/PlayerDamagedEvent.cs to improve attacker retrieval logic and robustness.
- Improve TTT/Game/lang/GameMsgs.cs by extracting role prefix determination into a separate method for clarity.
- Introduce new methods and directives in TTT/CS2/lang/CS2Msgs.cs for better integration with TTT API and improved message clarity.
- Update TTT/CS2/GameHandlers/DamageCancelers/TaserListenCanceler.cs to refine event handling logic with taser-specific enhancements.
2025-09-23 12:38:38 -07:00
MSWS
65c12696ed Debug compile on dev 2025-09-23 12:09:57 -07:00
MSWS
3206c30078 Debug 2025-09-23 12:04:04 -07:00
MSWS
f9f0c4e954 Fix failing tests 2025-09-16 21:33:01 -07:00
MSWS
45f492e44e Resolve initial merge conflicts 2025-09-16 21:30:34 -07:00
MSWS
c6f45276c3 refactor: Refactor damage handlers to use events +semver:minor
```
- Add OutOfRoundCanceler and TaserListenCanceler classes for improved damage cancellation in specific scenarios
- Mark RegisterListener method in IEventBus as obsolete
- Update ShopServiceCollection to remove generic type parameter from ShopCommand addition
- Change color representation for DetectiveRole in GameMsgs to DarkBlue
- Enhance ServiceCollectionExtensions with transient service registration and ICommand type handling
- Transition DamageCanceler to an event-driven architecture for improved flexibility
- Allow Player property in PlayerEvent to be set during initialization
- Register new damage cancelers in CS2ServiceCollection and simplify command service addition
- Refactor RoleIconsHandler to improve icon management and refine role assignments
- Update PlayerDamagedEvent for dynamic initialization and error handling
- Remove excessive "Respawning..." message from PlayerConnectionsHandler
- Narrow ShopCommand registration to only the "buy" subcommand
```
2025-09-16 21:20:51 -07:00
MSWS
9eef949501 refactor: Refactor module and listener loading logic
```
Refactor and enhance module management and logging across multiple components

- Update `TTT.cs` to differentiate between base and plugin modules during loading, improve logging for module operations, and revise initialization and activation of plugin modules.
- Enhance debugging and registration in `ServiceCollectionExtensions.cs` by adding diagnostic logs and ensuring proper registration of plugin modules and listeners with transient lifetimes.
- Remove redundant `Name` property from multiple logger and listener classes (`PlayerActionsLogger.cs`, `BodyIdentifyLogger.cs`, and `PlayerCausesEndListener.cs`), cleaning up unused code and simplifying logic for handling game-related events.
- Improve event dispatching efficiency in `EventBus.cs` by optimizing handler checks, and update DEBUG logging to handle nullability issues.
- Simplify `DamageCanceler.cs` by removing incorrect interface implementation and clarifying constructor and method usage.
```
2025-09-16 20:40:13 -07:00
MSWS
f1c6a784d1 refactor: Refactor codebase to remove Name and Version properties
Refactor Codebase for Simplification and Improved Modularization

- **RoundEndHandler.cs**
  - Removed `Name` and `Version` properties to simplify the class.
  - Maintained functionality for handling end-of-round events.

- **BaseListener.cs**
  - Removed `ITerrorModule` interface implementation and unnecessary properties.
  - Updated `Start` method to change listener lifecycle management.

- **CS2GameConfig.cs**
  - Simplified by removing `Name` and `Version` properties, focusing solely on game parameters.

- **GameServiceCollection.cs**
  - Changed listener addition method from `AddListener` to `AddModBehavior`, updated service registration terms.

- **DamageCanceler.cs**
  - Simplified class by removing `Name` and `Version` properties.
  - Enhanced damage processing with additional validity checks.

- Further updates include transitioning from `AddListener` to `AddModBehavior` across files, deprecating old registration methods in `EventBus.cs`, and comprehensive removal of `Name` and `Version` properties from various classes to streamline and refactor code structure. Additionally, obsolete methods were removed, and dependency injection patterns were adopted for intuitive system integration.
2025-09-16 19:53:21 -07:00
MSWS
112422e479 Add debug code to events 2025-09-11 18:01:40 -07:00
MSWS
6abdce7246 Merge branch 'dev' of github.com:MSWS/TTT into dev 2025-09-11 12:51:38 -07:00
MSWS
d3a021f40b Fix init loop 2025-09-11 12:51:27 -07:00
Isaac
f657599a0e Add core TTT shop system with commands and One-Shot Deagle item (#49) 2025-09-11 11:24:49 -07:00
Isaac
4d66afaec9 Update TTT/Test/Shop/Commands/BuyTest.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-09-11 11:22:30 -07:00
Isaac
0667652ed4 Update TTT/Shop/Items/OneShotDeagle/DeagleDamageListener.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-09-11 11:21:55 -07:00
MSWS
b41092fd69 feat: Refactor purchase logic and add BuyCommand tests
```plaintext
Implement comprehensive testing and enhance purchase logic

- Add `BuyTest` class to thoroughly test `BuyCommand` in the shop, covering various scenarios and ensuring correct game rule validation.
- Modify `OneShotDeagle`'s `CanPurchase` method to return a `PurchaseResult` enum for detailed purchasing results.
- Update `IShopItem` interface's `CanPurchase` method signature to use `PurchaseResult` for consistent result handling.
- Add `TestShopItem` class implementing the `IShopItem` interface, complete with properties, methods, and a successful `CanPurchase` implementation for testing.
- Improve `TestMessenger` with a null check for the player parameter and message logging for null player scenarios.
- Move `PurchaseResult.cs` to a new directory within the `Shop` folder for better organization.
- Refactor `CommandManagerTests` to use `ICommandManager` interface, ensuring interface-based testing without functional changes.
- Enhance `BuyCommand` with new aliases, asynchronous `Execute` method, and improved transaction logic, including balance checks and item search refactor.
```
2025-09-11 11:17:40 -07:00
MSWS
cc345ac010 Merge branch 'feat/karma' of github.com:MSWS/TTT into feat/karma 2025-09-09 15:24:29 -07:00
MSWS
fa1d732724 test: Refactor tests and cleanup PropMover code
- Await the `bus.Dispatch(deathEvent)` call in `OnKill_WithoutGame_DoesNothing` test.
- Remove unused import and eliminate the unused `IMessenger` component in `PropMover.cs`.
- Rename `refreshBodies` method to `refreshBody` for clarity in `PropMover.cs`.
- Refactor the iteration over `playersPressingE` to call the updated method in `PropMover.cs`.
- Enhance code readability in `PropMover.cs` through minor code style improvements, including removal of unnecessary exclamation points.
2025-09-09 15:24:23 -07:00
MSWS
4d5109b6be test: Additional unit tests regarding karma stacking 2025-09-09 15:24:23 -07:00
MSWS
00970f6789 feat: Refactor handlers and add KarmaSyncer feature
- Rename `RoundStartHandler` to `RoundStart_GameStartHandler` and update `Dispose()` method in `RoundStartHandler.cs`
- Rename `RoundEndHandler` to `RoundEnd_GameEndHandler` in `RoundEndHandler.cs`
- Update target framework to .NET 8.0 and add project reference to Karma project in `CS2.csproj`
- Update service collection for specific game start/end handlers and add plugin for player karma in `CS2ServiceCollection.cs`
- Introduce `KarmaSyncer` class for handling player karma with appropriate dependencies and event handling in `KarmaSyncer.cs`
2025-09-09 15:24:23 -07:00
MSWS
29d2e8a46c Format & Cleanup 2025-09-09 15:24:22 -07:00
MSWS
1d6526730a All unit tests passing 2025-09-09 15:24:22 -07:00
MSWS
98dc08c667 Overhaul event bus to support async listeners 2025-09-09 15:24:22 -07:00
MSWS
4a50d662af test: Refactor karma handling and enhance test coverage
```
- Remove `KarmaTest.cs`, eliminating a test case for default player karma verification upon creation.
- Introduce `KarmaListenerTests.cs` file, laying the groundwork for future KarmaListener test cases.
- Adjust `KeyedMemoryStorage.cs` to enable subclass access and method overriding by changing `data` dictionary to protected and marking `Load` and `Write` methods as virtual.
- Update `MemoryKarmaStorage.cs` with `Xunit.Internal` import, modify generic type parameter to `int`, refine code with target-typed `new()`, and override `Load` method with default karma value handling.
- Refactor `KarmaStorage.cs` to batch process karma updates asynchronously, simplify default karma handling, enhance error management, and refine karma caching.
```
2025-09-09 15:24:22 -07:00
MSWS
45727da462 Start work on adding unit testing 2025-09-09 15:24:22 -07:00
MSWS
fc2104e71a feat: Introduce Karma module with DI and refactoring
```
- Add `KarmaServiceCollection` for dependency injection setup, facilitating the registration of `IKarmaService` with scoped lifetime.
- Update `TTTServiceCollection` to include new services for Karma management and necessary game services.
- Refactor `KarmaStorage` to implement `IKarmaService`, simplifying interface responsibilities and removing previous interface implementations.
- Configure `Plugin.csproj` with .NET 8.0, implicit usings, nullable reference types, and setup publish output handling.
- Introduce `IKarmaService` interface to streamline access and interaction with karma functionalities.
- Create `KarmaListener` class for managing karma adjustments during game events, implementing event handling and asynchronous tasks.
```
2025-09-09 15:24:22 -07:00
MSWS
375108dd85 Remove unnecessary null check 2025-09-09 15:24:22 -07:00
MSWS
aded3fb6a2 feat: Introduce Karma management system
- Create a new KarmaStorage class to handle player karma with MySQL and caching
- Add the Karma project file with .NET 8.0 target and necessary references
- Include the Karma project in TTT.sln with Debug and Release configurations
- Define a Karma configuration file with database and karma settings
- Introduce a KarmaUpdateEvent class for managing player karma updates with event cancellation support
2025-09-09 15:24:22 -07:00
MSWS
519578dcd9 Merge branch 'feat/shop' of github.com:MSWS/TTT into feat/shop 2025-09-09 15:24:10 -07:00
MSWS
2ebc25a692 refactor: Add Skip method across command classes
- Add a `Skip` method to `CS2CommandInfo` for argument skipping
- Rename `sub` to `subcommands` in `ShopCommand` for clarity and update execution logic
- Make `caller` parameter nullable in `TestCommandInfo` and introduce `Skip` method for better argument handling
- Add `Skip` method to `ICommandInfo` interface for consistent argument skipping functionality
2025-09-09 15:24:03 -07:00
MSWS
1810bd1473 feat: Add shop buy command 2025-09-09 15:24:03 -07:00
MSWS
8ea14e7960 Register services 2025-09-09 15:24:03 -07:00
MSWS
3dc3cd08f4 Manually copy over old work of shop command 2025-09-09 15:24:03 -07:00
MSWS
78e0b64bb3 refactor: Move classes into proper dirs 2025-09-09 15:24:03 -07:00
MSWS
5977d87216 Remove unused test 2025-09-09 15:23:53 -07:00
MSWS
8cdb19f23c refactor: Add Skip method across command classes
- Add a `Skip` method to `CS2CommandInfo` for argument skipping
- Rename `sub` to `subcommands` in `ShopCommand` for clarity and update execution logic
- Make `caller` parameter nullable in `TestCommandInfo` and introduce `Skip` method for better argument handling
- Add `Skip` method to `ICommandInfo` interface for consistent argument skipping functionality
2025-09-09 15:21:08 -07:00
MSWS
a44b5b00a2 feat: Add shop buy command 2025-09-09 14:56:31 -07:00
MSWS
352de1f667 Register services 2025-09-09 11:31:35 -07:00
MSWS
1b8d201567 Manually copy over old work of shop command 2025-09-09 11:28:40 -07:00
MSWS
f753cb01c4 refactor: Move classes into proper dirs 2025-09-09 11:26:08 -07:00
MSWS
529af8c776 test: Refactor tests and cleanup PropMover code
- Await the `bus.Dispatch(deathEvent)` call in `OnKill_WithoutGame_DoesNothing` test.
- Remove unused import and eliminate the unused `IMessenger` component in `PropMover.cs`.
- Rename `refreshBodies` method to `refreshBody` for clarity in `PropMover.cs`.
- Refactor the iteration over `playersPressingE` to call the updated method in `PropMover.cs`.
- Enhance code readability in `PropMover.cs` through minor code style improvements, including removal of unnecessary exclamation points.
2025-09-09 11:07:15 -07:00
MSWS
6016f62931 test: Additional unit tests regarding karma stacking 2025-09-09 11:02:30 -07:00
MSWS
50f5a835de feat: Refactor handlers and add KarmaSyncer feature
- Rename `RoundStartHandler` to `RoundStart_GameStartHandler` and update `Dispose()` method in `RoundStartHandler.cs`
- Rename `RoundEndHandler` to `RoundEnd_GameEndHandler` in `RoundEndHandler.cs`
- Update target framework to .NET 8.0 and add project reference to Karma project in `CS2.csproj`
- Update service collection for specific game start/end handlers and add plugin for player karma in `CS2ServiceCollection.cs`
- Introduce `KarmaSyncer` class for handling player karma with appropriate dependencies and event handling in `KarmaSyncer.cs`
2025-09-09 10:47:59 -07:00
MSWS
46104f142f Format & Cleanup 2025-09-09 10:37:47 -07:00
MSWS
6b7d89dbf0 All unit tests passing 2025-09-09 10:32:02 -07:00
Isaac
332b6e6501 build(deps): Bump actions/setup-dotnet from 4 to 5 (#46)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet)
from 4 to 5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-dotnet/releases">actions/setup-dotnet's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<h3>Breaking Changes</h3>
<ul>
<li>Upgrade to Node.js 24 and modernize async usage by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/654">actions/setup-dotnet#654</a></li>
</ul>
<p>Make sure your runner is updated to this version or newer to use this
release. v2.327.1 <a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></p>
<h3>Dependency Updates</h3>
<ul>
<li>Upgrade <code>@​action/cache</code> from 4.0.2 to 4.0.3 by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/622">actions/setup-dotnet#622</a></li>
<li>Upgrade husky from 8.0.3 to 9.1.7 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/591">actions/setup-dotnet#591</a></li>
<li>Upgrade <code>@​actions/glob</code> from 0.4.0 to 0.5.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/594">actions/setup-dotnet#594</a></li>
<li>Upgrade eslint-config-prettier from 9.1.0 to 10.1.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/639">actions/setup-dotnet#639</a></li>
<li>Upgrade undici from 5.28.5 to 5.29.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/641">actions/setup-dotnet#641</a></li>
<li>Upgrade form-data to bring in fix for critical vulnerability by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-dotnet/pull/652">actions/setup-dotnet#652</a></li>
<li>Upgrade actions/checkout from 4 to 5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/662">actions/setup-dotnet#662</a></li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li>Remove Support for older .NET Versions and Update installers scripts
by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-dotnet/pull/647">actions/setup-dotnet#647</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/647">actions/setup-dotnet#647</a></li>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/654">actions/setup-dotnet#654</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-dotnet/compare/v4...v5.0.0">https://github.com/actions/setup-dotnet/compare/v4...v5.0.0</a></p>
<h2>v4.3.1</h2>
<h2>What's Changed</h2>
<ul>
<li><code>v4</code> - Remove <code>azureedge.net</code> fallback logic
and update install scripts by <a
href="https://github.com/zaataylor"><code>@​zaataylor</code></a> in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/572">actions/setup-dotnet#572</a>
As outlined in<a
href="https://devblogs.microsoft.com/dotnet/critical-dotnet-install-links-are-changing/#call-to-action">
Critical .NET Install Links Are Changing</a>, remove the storage account
fallback logic added for v4 in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/566">actions/setup-dotnet#566</a>
and update the install scripts accordingly.
<strong>Related issue</strong>: <a
href="https://redirect.github.com/dotnet/install-scripts/issues/559">dotnet/install-scripts#559</a></li>
<li>upgrade <code>@​actions/cache</code> to 4.0.2 by <a
href="https://github.com/HarithaVattikuti"><code>@​HarithaVattikuti</code></a>
in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/615">actions/setup-dotnet#615</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-dotnet/compare/v4...v4.3.1">https://github.com/actions/setup-dotnet/compare/v4...v4.3.1</a></p>
<h2>v4.3.0</h2>
<h2>What's Changed</h2>
<ul>
<li>README update - add permissions section by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/587">actions/setup-dotnet#587</a></li>
<li>Configure Dependabot settings by <a
href="https://github.com/HarithaVattikuti"><code>@​HarithaVattikuti</code></a>
in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/585">actions/setup-dotnet#585</a></li>
<li>Upgrade <strong>cache</strong> from 3.2.4 to 4.0.0 by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/586">actions/setup-dotnet#586</a></li>
<li>Upgrade <strong>actions/publish-immutable-action</strong> from 0.0.3
to 0.0.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/590">actions/setup-dotnet#590</a></li>
<li>Upgrade <strong><code>@​actions/http-client</code></strong> from
2.2.1 to 2.2.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/592">actions/setup-dotnet#592</a></li>
<li>Upgrade <strong>undici</strong> from 5.28.4 to 5.28.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/596">actions/setup-dotnet#596</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/benwells"><code>@​benwells</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/587">actions/setup-dotnet#587</a></li>
<li><a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-dotnet/pull/586">actions/setup-dotnet#586</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="d4c94342e5"><code>d4c9434</code></a>
Update to Node.js 24 and modernize async usage (<a
href="https://redirect.github.com/actions/setup-dotnet/issues/654">#654</a>)</li>
<li><a
href="5c125af7da"><code>5c125af</code></a>
Bump actions/checkout from 4 to 5 (<a
href="https://redirect.github.com/actions/setup-dotnet/issues/662">#662</a>)</li>
<li><a
href="87c6e11776"><code>87c6e11</code></a>
Bumps form-data (<a
href="https://redirect.github.com/actions/setup-dotnet/issues/652">#652</a>)</li>
<li><a
href="06a5327ecf"><code>06a5327</code></a>
Bump undici from 5.28.5 to 5.29.0 (<a
href="https://redirect.github.com/actions/setup-dotnet/issues/641">#641</a>)</li>
<li><a
href="e8e5b8203e"><code>e8e5b82</code></a>
Bump eslint-config-prettier from 9.1.0 to 10.1.5 (<a
href="https://redirect.github.com/actions/setup-dotnet/issues/639">#639</a>)</li>
<li><a
href="bf4cd79173"><code>bf4cd79</code></a>
Bump <code>@​actions/glob</code> from 0.4.0 to 0.5.0 (<a
href="https://redirect.github.com/actions/setup-dotnet/issues/594">#594</a>)</li>
<li><a
href="4ddad1c881"><code>4ddad1c</code></a>
Bump husky from 8.0.3 to 9.1.7 (<a
href="https://redirect.github.com/actions/setup-dotnet/issues/591">#591</a>)</li>
<li><a
href="0f55b457d2"><code>0f55b45</code></a>
removes end-of-line dotnet versions (<a
href="https://redirect.github.com/actions/setup-dotnet/issues/647">#647</a>)</li>
<li><a
href="267870a9c4"><code>267870a</code></a>
upgrade actions/cache to 4.0.3 (<a
href="https://redirect.github.com/actions/setup-dotnet/issues/622">#622</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/setup-dotnet/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-dotnet&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>
2025-09-09 09:08:32 -07:00
MSWS
8c385934b3 Overhaul event bus to support async listeners 2025-09-09 09:04:54 -07:00
MSWS
04f79551f7 test: Refactor karma handling and enhance test coverage
```
- Remove `KarmaTest.cs`, eliminating a test case for default player karma verification upon creation.
- Introduce `KarmaListenerTests.cs` file, laying the groundwork for future KarmaListener test cases.
- Adjust `KeyedMemoryStorage.cs` to enable subclass access and method overriding by changing `data` dictionary to protected and marking `Load` and `Write` methods as virtual.
- Update `MemoryKarmaStorage.cs` with `Xunit.Internal` import, modify generic type parameter to `int`, refine code with target-typed `new()`, and override `Load` method with default karma value handling.
- Refactor `KarmaStorage.cs` to batch process karma updates asynchronously, simplify default karma handling, enhance error management, and refine karma caching.
```
2025-09-09 08:19:56 -07:00
MSWS
b08f9234ff Start work on adding unit testing 2025-09-08 21:22:36 -07:00
MSWS
87b83edb55 feat: Introduce Karma module with DI and refactoring
```
- Add `KarmaServiceCollection` for dependency injection setup, facilitating the registration of `IKarmaService` with scoped lifetime.
- Update `TTTServiceCollection` to include new services for Karma management and necessary game services.
- Refactor `KarmaStorage` to implement `IKarmaService`, simplifying interface responsibilities and removing previous interface implementations.
- Configure `Plugin.csproj` with .NET 8.0, implicit usings, nullable reference types, and setup publish output handling.
- Introduce `IKarmaService` interface to streamline access and interaction with karma functionalities.
- Create `KarmaListener` class for managing karma adjustments during game events, implementing event handling and asynchronous tasks.
```
2025-09-08 20:42:02 -07:00
dependabot[bot]
de7639d986 build(deps): Bump actions/setup-dotnet from 4 to 5
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4 to 5.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 00:34:50 +00:00
MSWS
a90e33b69d Remove unnecessary null check 2025-08-19 03:10:58 -07:00
MSWS
700074b130 feat: Introduce Karma management system
- Create a new KarmaStorage class to handle player karma with MySQL and caching
- Add the Karma project file with .NET 8.0 target and necessary references
- Include the Karma project in TTT.sln with Debug and Release configurations
- Define a Karma configuration file with database and karma settings
- Introduce a KarmaUpdateEvent class for managing player karma updates with event cancellation support
2025-08-19 03:09:59 -07:00
Isaac
6c06071d8a PlayerPawn -> Pawn, added DamageCanceler 2025-08-19 02:14:04 -07:00
MSWS
d89c70c41e Fix compilation error 2025-08-19 02:10:19 -07:00
Isaac
58012cb112 Update TTT/Game/Actions/DamagedAction.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-08-19 02:08:16 -07:00
Isaac
febf34fcda build(deps): Bump actions/checkout from 4 to 5 (#44)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to
5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
<li>Prepare v5.0.0 release by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2238">actions/checkout#2238</a></li>
</ul>
<h2>⚠️ Minimum Compatible Runner Version</h2>
<p><strong>v2.327.1</strong><br />
<a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></p>
<p>Make sure your runner is updated to this version or newer to use this
release.</p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4...v5.0.0">https://github.com/actions/checkout/compare/v4...v5.0.0</a></p>
<h2>v4.3.0</h2>
<h2>What's Changed</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
<li>Prepare release v4.3.0 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2237">actions/checkout#2237</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/motss"><code>@​motss</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li><a href="https://github.com/mouismail"><code>@​mouismail</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li><a href="https://github.com/benwells"><code>@​benwells</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li><a href="https://github.com/nebuk89"><code>@​nebuk89</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4...v4.3.0">https://github.com/actions/checkout/compare/v4...v4.3.0</a></p>
<h2>v4.2.2</h2>
<h2>What's Changed</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4.2.1...v4.2.2">https://github.com/actions/checkout/compare/v4.2.1...v4.2.2</a></p>
<h2>v4.2.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Jcambass"><code>@​Jcambass</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1919">actions/checkout#1919</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4.2.0...v4.2.1">https://github.com/actions/checkout/compare/v4.2.0...v4.2.1</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>V5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>V4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>v4.2.0</h2>
<ul>
<li>Add Ref and Commit outputs by <a
href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
<li>Dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
<a
href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
</ul>
<h2>v4.1.7</h2>
<ul>
<li>Bump the minor-npm-dependencies group across 1 directory with 4
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
<li>Bump actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
<li>Check out other refs/* by commit by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
<li>Pin actions/checkout's own workflows to a known, good, stable
version. by <a href="https://github.com/jww3"><code>@​jww3</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1776">actions/checkout#1776</a></li>
</ul>
<h2>v4.1.6</h2>
<ul>
<li>Check platform to set archive extension appropriately by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1732">actions/checkout#1732</a></li>
</ul>
<h2>v4.1.5</h2>
<ul>
<li>Update NPM dependencies by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1703">actions/checkout#1703</a></li>
<li>Bump github/codeql-action from 2 to 3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1694">actions/checkout#1694</a></li>
<li>Bump actions/setup-node from 1 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1696">actions/checkout#1696</a></li>
<li>Bump actions/upload-artifact from 2 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1695">actions/checkout#1695</a></li>
<li>README: Suggest <code>user.email</code> to be
<code>41898282+github-actions[bot]@users.noreply.github.com</code> by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1707">actions/checkout#1707</a></li>
</ul>
<h2>v4.1.4</h2>
<ul>
<li>Disable <code>extensions.worktreeConfig</code> when disabling
<code>sparse-checkout</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1692">actions/checkout#1692</a></li>
<li>Add dependabot config by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1688">actions/checkout#1688</a></li>
<li>Bump the minor-actions-dependencies group with 2 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1693">actions/checkout#1693</a></li>
<li>Bump word-wrap from 1.2.3 to 1.2.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1643">actions/checkout#1643</a></li>
</ul>
<h2>v4.1.3</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="08c6903cd8"><code>08c6903</code></a>
Prepare v5.0.0 release (<a
href="https://redirect.github.com/actions/checkout/issues/2238">#2238</a>)</li>
<li><a
href="9f265659d3"><code>9f26565</code></a>
Update actions checkout to use node 24 (<a
href="https://redirect.github.com/actions/checkout/issues/2226">#2226</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>
2025-08-19 02:05:21 -07:00
Isaac
44dafdd606 build(deps): Bump softprops/action-gh-release from 1 to 2 (#45)
Bumps
[softprops/action-gh-release](https://github.com/softprops/action-gh-release)
from 1 to 2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/softprops/action-gh-release/releases">softprops/action-gh-release's
releases</a>.</em></p>
<blockquote>
<h2>v2.0.0</h2>
<ul>
<li>update actions.yml declaration to node20 to address warnings</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md">softprops/action-gh-release's
changelog</a>.</em></p>
<blockquote>
<h2>0.1.12</h2>
<ul>
<li>fix bug leading to empty strings subsituted for inputs users don't
provide breaking api calls <a
href="https://redirect.github.com/softprops/action-gh-release/pull/144">#144</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="72f2c25fcb"><code>72f2c25</code></a>
release 2.3.2</li>
<li><a
href="552dc5524b"><code>552dc55</code></a>
fix: revert <code>fs:readableWebStream</code> change (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/632">#632</a>)</li>
<li><a
href="f3cad8bcbf"><code>f3cad8b</code></a>
release 2.3.1</li>
<li><a
href="07a2257003"><code>07a2257</code></a>
fix: fix file closing issue (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/629">#629</a>)</li>
<li><a
href="d5382d3e6f"><code>d5382d3</code></a>
release 2.3.0</li>
<li><a
href="a0e2122208"><code>a0e2122</code></a>
feat: migrate from jest to vitest (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/626">#626</a>)</li>
<li><a
href="8836085300"><code>8836085</code></a>
chore: replace <code>mime</code> with <code>mime-types</code> (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/624">#624</a>)</li>
<li><a
href="86463358d8"><code>8646335</code></a>
chore: bump node to 20.19.2</li>
<li><a
href="46b284799f"><code>46b2847</code></a>
chore(deps): bump the npm group across 1 directory with 5 updates (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/623">#623</a>)</li>
<li><a
href="37fd9d0351"><code>37fd9d0</code></a>
chore(deps): bump undici from 5.28.5 to 5.29.0 (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/621">#621</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/softprops/action-gh-release/compare/v1...v2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=softprops/action-gh-release&package-manager=github_actions&previous-version=1&new-version=2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>
2025-08-19 02:04:50 -07:00
dependabot[bot]
330db6aef1 build(deps): Bump softprops/action-gh-release from 1 to 2
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '2'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-18 01:15:03 +00:00
dependabot[bot]
cddfdf3ebc build(deps): Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-18 01:14:28 +00:00
MSWS
deb0d2e825 feat: Refactor death action and add client event notification
```
- Update player death description in DeathAction to ensure clarity in scenarios involving a killer.
- Add Format method in DeathAction for structured death action descriptions.
- Implement client event notification in CombatHandler with conditional checks for attacker presence.
- Remove role initialization constructor in CS2Player to streamline code and refactor formatting.
```
2025-08-14 01:44:38 -07:00
MSWS
089564c4d6 feat: Differentiate win results and update role icons
- Adjust the logic in `RoundTimerListener.cs` to set final event results based on the winning team, assigning 2 for Counter-Terrorist wins and 3 for others, and refine win panel settings.
- Update `README.md` to reflect completed statuses for "Basic Gameplay," including the subcategories Traitors, Detectives, and Innocents, showcasing project progress.
- Reorder operations in `PlayerStatsTracker.cs` to update `PawnIsAlive` earlier in the `revealDeaths` method for clarity and consistent player state updates.
- Expand `RoleIconsHandler.cs` by adding `RevealIconFor` and `HideIconFor` methods to enhance control over traitor visual representation, allowing dynamic updates of player roles.
2025-08-14 01:31:45 -07:00
MSWS
871f47376a refactor: Improve state management and enhance role interactions
```
- Refine game start logic in RoundBasedGame.cs to ensure proper handling of game states and manage player notifications.
- Adjust state management and disposal process in RoundBasedGame.cs for better state handling and resource cleanup.
- Comment out deprecated EventRoundEnd logic in RoundTimerListener.cs and adjust timer settings for improved round transitions.
- Enhance BodyPickupListener.cs with new dependencies and logic for team adjustments during OnPropPickup events.
- Clean up PropMover.cs by removing unnecessary debug logging while maintaining current functionality.
```
2025-08-14 01:04:18 -07:00
MSWS
7621d15bcc feat: Improve round end handling and player status tracking
- Introduce a server command for handling win conditions during round end in `RoundTimerListener.cs`
- Wrap round end logic processing within a server update block in `RoundTimerListener.cs`
- Improve timing logic for round endings using `Observable.Timer` in `RoundTimerListener.cs`
- Ensure events/messages indicate the winner and specifics in `RoundTimerListener.cs`
- Update `PlayerStatsTracker.cs` to enhance pawn alive state tracking and accuracy
2025-08-14 00:42:33 -07:00
MSWS
a0111b5bea refactor: Refactor role logic and refine round timing events
- Modify `GameMsgs.cs` to enhance role identification through improved color coding and type checking for roles.
- Update `RoundTimerListener.cs` to incorporate reactive programming, improve event handling at round end, and refine role assignment logic.
- Enhance version string format in `TTTCommand.cs` by adding a short Git SHA for detailed version information.
- Simplify `EndRound` function in `RoundUtil.cs` by removing unnecessary delay parameter.
2025-08-14 00:33:43 -07:00
MSWS
14c4c85cfd refactor: Refactor connection and add pluralization tests
- Reorganize `connectToServer` method in `PlayerConnectionsHandler.cs` to delay certain operations until the next world update.
- Add player validity check before printing respawn chat message and update respawn logic to occur during the next world update in `PlayerConnectionsHandler.cs`.
- Consolidate respawn message to a generic "Respawning..." in `PlayerConnectionsHandler.cs`.
- Add new test cases in `ColoredRoleTest.cs` to verify pluralization functionality for role names, including special character handling.
- Ensure `HandlePluralization` method in `ColoredRoleTest.cs` correctly manages singular and plural forms with color formatting and dashes.
2025-08-13 23:41:32 -07:00
MSWS
e2383c90a8 fix: Invert game state check in OnDeath handler +semver:patch
- Correct logic in the `OnDeath` event handler by inverting the condition for an active game state check in `TTT/CS2/GameHandlers/BodySpawner.cs`
- Optimize function implementations including `makeGameRagdoll` and `correctRagdoll` in `TTT/CS2/GameHandlers/BodySpawner.cs`
2025-08-13 22:33:56 -07:00
MSWS
9db57d726d ci: Add webhook trigger for GitLab pipeline in nightly build
- Add `post_webhook` job to nightly workflow to trigger GitLab pipeline on `dev` branch
2025-08-13 22:14:18 -07:00
MSWS
47e63bf686 feat: Reuse active game in WAITING state +semver:patch
- Add condition in CS2GameManager to reuse active game if in WAITING state
2025-08-13 22:12:06 -07:00
MSWS
8835a76e67 feat: Enhance game state handling and player feedback
- Add `IGameManager` dependency to `PlayerConnectionsHandler.cs` for enhanced game state management
- Modify `connectToServer` in `PlayerConnectionsHandler.cs` to handle game states and improve player reconnection experience
- Update `DamageCanceler.cs` to improve conditional logic handling during active games
- Simplify player respawn logic and round handling in `RoundTimerListener.cs` by removing `PawnIsAlive` state manipulation and streamlining round start procedures
2025-08-13 20:45:08 -07:00
MSWS
731f58755d feat: Implement damage canceler and refine game logic
```
- Modify `RoundTimerListener` to reorder service initialization
- Add `TTT.API.Game` dependency to `BodySpawner` and improve game state checks and player handling upon death
- Reorder initialization logic in `SimpleLogger` for improved readability
- Introduce `GAME_LOGS_HEADER` and refactor message handling in `GameMsgs`
- Group minor organizational code changes across multiple files, including removing unused imports and restructuring logic for consistency
```
2025-08-13 20:01:48 -07:00
MSWS
824993fb16 refactor: Refactor 'PlayerPawn' references to 'Pawn' +semver:minor
- Improve error handling and modify references from `PlayerPawn` to `Pawn` in `BodySpawner.cs` to enhance robustness and reflect changes in the player object model.
- Update logic in `SimpleLogger.cs` to correctly set the `epoch` variable when the `timestamp` is smaller, fixing potential update issues.
- Refactor `PlayerExtensions.cs` for consistency by renaming variables from `PlayerPawn` to `Pawn` and ensure updated variable usage across player activities.
- Rename member from `PlayerPawn` to `Pawn` in `CS2InventoryManager.cs` to align with updated player data model and improve clarity.
- Update `TextSpawner.cs` references from `PlayerPawn` to `Pawn` for consistency and refactor list handling for better code alignment with new player data access method.
- Simplify `CombatHandler.cs` by changing references to `Pawn`, adjusting health updates, and improving code clarity for handling player events.
- Simplify and improve readability in `CS2Player.cs` by changing how player properties are accessed and set, maintaining debugging capabilities.
2025-08-13 18:38:43 -07:00
MSWS
6a67eb1141 Merge branch 'main' of github.com:MSWS/TTT 2025-08-13 10:55:09 -07:00
MSWS
670643998e Merge branch 'dev' 2025-08-13 10:55:03 -07:00
MSWS
70e2b44941 ci: Update workflows to use full semantic versioning
- Update `.github/workflows/release.yml` to use full semantic version (`fullSemVer`) in workflow steps
2025-08-13 10:52:59 -07:00
Isaac
c5cd646bee feat: Add PlayerStatsTracker and enhance game logic (#42)
- Implement `PlayerStatsTracker` to monitor player statistics using
dependency injection and event handlers for tracking kills, deaths, and
assists.
- Update `CS2ServiceCollection` to add `PlayerStatsTracker` listener for
enhanced player statistics tracking.
- Modify `RoundTimerListener` to incorporate player color settings at
round start and ensure proper player state updates.
- Enhance `CombatHandler` with game activity checks in
`OnPlayerDeath_Pre` and `OnPlayerHurt` methods to ensure efficient and
correct handling of game operations.
2025-08-13 10:49:12 -07:00
MSWS
223daa1085 feat: Add PlayerStatsTracker and enhance game logic
- Implement `PlayerStatsTracker` to monitor player statistics using dependency injection and event handlers for tracking kills, deaths, and assists.
- Update `CS2ServiceCollection` to add `PlayerStatsTracker` listener for enhanced player statistics tracking.
- Modify `RoundTimerListener` to incorporate player color settings at round start and ensure proper player state updates.
- Enhance `CombatHandler` with game activity checks in `OnPlayerDeath_Pre` and `OnPlayerHurt` methods to ensure efficient and correct handling of game operations.
2025-08-13 10:32:39 -07:00
MSWS
fbda4fe38b Merge branch 'dev' 2025-08-13 10:01:35 -07:00
Isaac
49d45a12b9 Refactor CS2 roles to use localization for coloring, add beam for prop dragging 2025-08-13 09:54:35 -07:00
242 changed files with 8789 additions and 1070 deletions

14
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@@ -0,0 +1,14 @@
---
name: Bug Report
about: Report an unintended behavior
title: ''
labels: 'Type: Bug'
assignees: ''
---
**Summary**
**Reproduction**
**Expected Behavior**

View File

@@ -13,14 +13,14 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
fetch-tags: true
show-progress: true
- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: '8.0.x'
@@ -51,10 +51,10 @@ jobs:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: '8.0.x'

View File

@@ -48,11 +48,23 @@ jobs:
dotnet restore Locale/Locale.csproj
dotnet build Locale/Locale.csproj --no-restore -c Release
cp lang/*.json build/TTT/lang
- name: Copy Gamedata
run: |
mkdir -p build/TTT/gamedata
cp -r TTT/CS2/gamedata/* build/TTT/gamedata
- name: Publish Plugin
run: |
dotnet restore TTT/Plugin/Plugin.csproj
dotnet publish TTT/Plugin/Plugin.csproj --no-restore -c Release -o build/TTT
if [ "${GITHUB_REF##*/}" = "dev" ]; then
dotnet publish TTT/Plugin/Plugin.csproj --no-restore -c Debug -o build/TTT
elif [ "${GITHUB_REF##*/}" = "main" ]; then
dotnet publish TTT/Plugin/Plugin.csproj --no-restore -c Release -o build/TTT
else
echo "Branch not recognized, skipping publish."
exit 0
fi
- name: Upload Artifact
uses: actions/upload-artifact@v4
@@ -61,3 +73,16 @@ jobs:
path: build/TTT
if-no-files-found: error
post_webhook:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/dev'
steps:
- name: POST Webhook
run: |
curl -X POST \
--fail \
-F token=${{ secrets.GITLAB_SECRET_TOKEN }} \
-F ref=dev \
https://gitlab.edgegamers.io/api/v4/projects/2640/trigger/pipeline

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@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
fetch-tags: true
# 1. Calculate version using GitVersion
- name: Install GitVersion
@@ -34,6 +42,11 @@ jobs:
dotnet build Locale/Locale.csproj --no-restore -c Release
cp lang/*.json build/TTT/lang
- name: Copy Gamedata
run: |
mkdir -p build/TTT/gamedata
cp -r TTT/CS2/gamedata/* build/TTT/gamedata
- name: Publish Plugin
run: |
dotnet restore TTT/Plugin/Plugin.csproj
@@ -42,8 +55,8 @@ jobs:
- name: Zip Artifacts
run: |
cd build/TTT
zip -r TTT-${{ steps.gitversion.outputs.MajorMinorPatch }}.zip *
zip -r TTT-${{ steps.gitversion.outputs.fullSemVer }}.zip *
# 2. Get latest tag
- name: Get latest tag
id: latest_tag
@@ -56,38 +69,142 @@ jobs:
# 3. Tag if new version
- name: Create and push new tag
if: steps.gitversion.outputs.MajorMinorPatch != steps.latest_tag.outputs.tag
if: steps.gitversion.outputs.fullSemVer != steps.latest_tag.outputs.tag
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag ${{ steps.gitversion.outputs.MajorMinorPatch }}
git push origin ${{ steps.gitversion.outputs.MajorMinorPatch }}
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.MajorMinorPatch }} \
--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@v1
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.gitversion.outputs.MajorMinorPatch }}
tag_name: ${{ steps.gitversion.outputs.fullSemVer }}
body_path: CHANGELOG.md
prerelease: ${{ github.ref_name != 'main' }}
files: build/TTT/*.zip

42
GitVersion.yml Normal file
View File

@@ -0,0 +1,42 @@
mode: ContinuousDeployment
tag-prefix: "" # allow bare numeric tags like 2.0.0
commit-message-incrementing: Enabled
assembly-versioning-scheme: MajorMinorPatch
# Never allow commit messages to trigger a major bump
major-version-bump-message: "(?!)"
# Opt-in bumps via tokens
minor-version-bump-message: \+semver:\s?minor
patch-version-bump-message: \+semver:\s?patch
branches:
main:
label: "" # clears the default prerelease label
increment: None # no automatic bump unless +semver token
prevent-increment:
of-merged-branch: true
when-branch-merged: true
when-current-commit-tagged: true
develop:
label: "dev"
increment: None
prevent-increment:
of-merged-branch: true
when-branch-merged: true
when-current-commit-tagged: true
feature:
label: "feat"
increment: None
release:
label: "rc"
increment: None
hotfix:
label: "hotfix"
increment: None
pull-request:
label: "pr"
increment: None
assembly-informational-format: "{FullSemVer}+Branch.{BranchName}.Sha.{ShortSha}"

View File

@@ -1,14 +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/ |
| 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,6 +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="YamlDotNet" Version="16.3.0"/>
</ItemGroup>

View File

@@ -42,7 +42,10 @@ public partial class StringLocalizer : IMsgLocalizer {
private LocalizedString getString(string name, params object[] arguments) {
// Get the localized value
var value = localizer[name].Value;
string value;
try { value = localizer[name].Value; } catch (NullReferenceException e) {
return new LocalizedString(name, name, true);
}
// Replace placeholders like %key% with their respective values
var matches = percentRegex().Matches(value);
@@ -114,10 +117,14 @@ public partial class StringLocalizer : IMsgLocalizer {
value = value.Replace("%s%", "s");
// We have to do this chicanery due to support colors in the string
value = handleTrailingS(value);
return value;
}
private static string handleTrailingS(string value) {
var trailingIndex = -1;
// We have to do this chicanery due to supporting colors in the string
while ((trailingIndex =
value.IndexOf("'s", trailingIndex + 1, StringComparison.Ordinal)) != -1) {
var startingWordBoundary = value[..trailingIndex].LastIndexOf(' ');

View File

@@ -10,10 +10,10 @@ survive while eliminating the traitors among them.
## Features
- [X] Unit Testing
- [ ] Basic Gameplay
- [ ] Traitors
- [ ] Detectives
- [ ] Innocents
- [X] Basic Gameplay
- [X] Traitors
- [X] Detectives
- [X] Innocents
- [ ] Shop
- [ ] Karma
- [ ] Statistics
@@ -44,8 +44,8 @@ Due to this project being primarily developed with Counter-Strike 2 (and more
specifically, [CounterStrikeSharp](https://github.com/roflmuffin/CounterStrikeSharp)) in mind, localization has been
built with flat-file storage based around YML/JSON.
In short, we write our locales in `en.yml`, run `Locale.csproj` to convert and combine all `**/Lang/en.yml` -> a master
`lang/en.json`, and then run our tests / release pipeliens with it.
In short, we write our locales in `en.yml`, run `Locale.csproj` to convert and combine all `**/Lang/en.yml` into a master
`lang/en.json`, and then run our tests / release pipelines with it.
It is recommend to read the [Locale README](./Locale/README.md) for more information on how to use it.

12
TTT.sln
View File

@@ -19,6 +19,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Locale", "Locale\Locale.csp
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shop", "TTT\Shop\Shop.csproj", "{478416D7-4996-41CC-BDDF-5BF50B505D0F}"
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
@@ -60,6 +64,14 @@ Global
{478416D7-4996-41CC-BDDF-5BF50B505D0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{478416D7-4996-41CC-BDDF-5BF50B505D0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{478416D7-4996-41CC-BDDF-5BF50B505D0F}.Release|Any CPU.Build.0 = Release|Any CPU
{AFC791EC-750C-423F-9F35-87636657E990}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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

@@ -5,4 +5,7 @@
<Nullable>enable</Nullable>
<RootNamespace>TTT.API</RootNamespace>
</PropertyGroup>
<ItemGroup>
<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

@@ -7,7 +7,8 @@ public interface ICommandInfo {
string[] Args { get; }
IOnlinePlayer? CallingPlayer { get; }
CommandCallingContext CallingContext { get; set; }
string GetCommandString => string.Join(' ', Args);
string CommandString => string.Join(' ', Args);
int ArgCount => Args.Length;
void ReplySync(string message);
ICommandInfo Skip(int count = 1);
}

View File

@@ -6,10 +6,13 @@ namespace TTT.API.Command;
/// An interface that allows for registering and processing commands.
/// </summary>
public interface ICommandManager {
ISet<ICommand> Commands { get; }
/// <summary>
/// Registers a command with the manager.
/// </summary>
/// <param name="command">True if the command was successfully registered.</param>
[Obsolete("Registration is done via the ServiceProvider now.")]
bool RegisterCommand(ICommand command);
/// <summary>

View File

@@ -1,8 +1,10 @@
namespace TTT.API.Events;
public interface IEventBus {
[Obsolete("Registration should be done via the ServiceProvider")]
void RegisterListener(IListener listener);
void UnregisterListener(IListener listener);
void Dispatch(Event ev);
Task Dispatch(Event ev);
}

View File

@@ -1,3 +1,5 @@
namespace TTT.API.Events;
public interface IListener : IDisposable;
public interface IListener : ITerrorModule {
void ITerrorModule.Start() { }
}

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Events;
namespace TTT.API.Extensions;
@@ -8,60 +9,20 @@ namespace TTT.API.Extensions;
/// its interface, allowing us to get the list of all modules simply using ITerrorModule.
/// </summary>
public static class ServiceCollectionExtensions {
public static void AddPluginBehavior<TExtension>(
this IServiceCollection collection)
where TExtension : class, IPluginModule {
// Add the root extension itself as a scoped service.
// This means every time Load is called in the main Jailbreak loader,
// the extension will be fetched and kept as a singleton for the duration
// until "Unload" is called.
// collection.AddScoped<IPluginBehavior, PluginBehavior>();
// collection.AddScoped<TExtension>();
// collection.AddTransient<IPluginModule, TExtension>(provider
// => provider.GetRequiredService<TExtension>());
// collection.AddModBehavior<TExtension>();
collection.AddScoped<TExtension>();
collection.AddTransient<ITerrorModule>(p
=> p.GetRequiredService<TExtension>());
collection.AddTransient<IPluginModule, TExtension>(p
=> p.GetRequiredService<TExtension>());
}
public static void AddPluginBehavior<TInterface, TExtension>(
this IServiceCollection collection)
where TExtension : class, TInterface, IPluginModule
where TInterface : class {
// Add the root extension itself as a scoped service.
// This means every time Load is called in the main Jailbreak loader,
// the extension will be fetched and kept as a singleton for the duration
// until "Unload" is called.
// collection.AddScoped<IPluginBehavior, PluginBehavior>();
// collection.AddScoped<TExtension>();
// collection.AddTransient<IPluginModule, TExtension>(provider
// => provider.GetRequiredService<TExtension>());
// collection.AddModBehavior<TExtension>();
collection.AddPluginBehavior<TExtension>();
collection.AddTransient<TInterface, TExtension>(p
=> p.GetRequiredService<TExtension>());
}
/// <summary>
/// Add a <see cref="ITerrorModule" /> to the global service collection
/// </summary>
/// <param name="collection"></param>
/// <typeparam name="TExtension"></typeparam>
public static void AddModBehavior<TExtension>(
this IServiceCollection collection)
where TExtension : class, ITerrorModule {
// Add the root extension itself as a scoped service.
// This means every time Load is called in the main Jailbreak loader,
// the extension will be fetched and kept as a singleton for the duration
// until "Unload" is called.
if (typeof(IPluginModule).IsAssignableFrom(typeof(TExtension)))
if (typeof(TExtension).IsAssignableTo(typeof(IPluginModule)))
collection.AddTransient<IPluginModule>(provider
=> (provider.GetRequiredService<TExtension>() as IPluginModule)!);
if (typeof(TExtension).IsAssignableTo(typeof(IListener)))
collection.AddTransient<IListener>(provider
=> (provider.GetRequiredService<TExtension>() as IListener)!);
if (typeof(TExtension).IsAssignableTo(typeof(ICommand)))
collection.AddTransient<ICommand>(provider
=> (provider.GetRequiredService<TExtension>() as ICommand)!);
collection.AddScoped<TExtension>();
@@ -87,9 +48,4 @@ public static class ServiceCollectionExtensions {
collection.AddTransient<TInterface, TExtension>(p
=> p.GetRequiredService<TExtension>());
}
public static void AddListener<TListener>(this IServiceCollection collection)
where TListener : class, IListener {
collection.AddScoped<IListener, TListener>();
}
}

View File

@@ -1,17 +1,26 @@
using TTT.API.Player;
using TTT.API.Role;
namespace TTT.API.Game;
public interface IAction {
IPlayer Player { get; }
IPlayer? Other { get; }
IRole? PlayerRole { get; }
IRole? OtherRole { get; }
string Id { get; }
string Verb { get; }
string Details { get; }
public string Format() {
var pRole = PlayerRole != null ?
$" [{PlayerRole.Name.First(char.IsAsciiLetter)}]" :
"";
var oRole = OtherRole != null ?
$" [{OtherRole.Name.First(char.IsAsciiLetter)}]" :
"";
return Other is not null ?
$"{Player} {Verb} {Other} {Details}" :
$"{Player} {Verb} {Details}";
$"{Player}{pRole} {Verb} {Other}{oRole} {Details}" :
$"{Player}{pRole} {Verb} {Details}";
}
}

View File

@@ -26,11 +26,14 @@ public interface IGame : IDisposable {
/// Attempts to start a game.
/// Depending on implementation, this may start a countdown or immediately start the game.
/// </summary>
/// <param name="countdown"></param>
/// <param name="countdown">TimeSpan for countdown, null means start immediately</param>
IObservable<long>? Start(TimeSpan? countdown = null);
void EndGame(EndReason? reason = null);
bool CheckEndConditions();
[Obsolete("This method is ambiguous, check the game state directly.")]
bool IsInProgress() { return State is State.COUNTDOWN or State.IN_PROGRESS; }
ISet<IOnlinePlayer> GetAlive() {

View File

@@ -10,6 +10,7 @@ public interface IGameManager : IDisposable {
IGame? CreateGame();
[Obsolete("This method is ambiguous, check the game state directly.")]
bool IsGameActive() {
return ActiveGame is not null && ActiveGame.IsInProgress();
}

View File

@@ -1,7 +0,0 @@
namespace TTT.API;
public interface ITerrorApi : ITerrorModule {
IServiceProvider Services { get; }
string ITerrorModule.Name => "Core";
string ITerrorModule.Version => GitVersionInformation.FullSemVer;
}

View File

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

View File

@@ -4,7 +4,7 @@ public interface IWeapon {
/// <summary>
/// The internal ID of the weapon, should match the ID of the weapon in the underlying game.
/// </summary>
public string Id { get; }
public string WeaponId { get; }
/// <summary>
/// The amount of ammo that is in reserve for this weapon.

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,23 +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.Id);
Task RemoveWeapon(IOnlinePlayer player, IWeapon weapon) {
return RemoveWeapon(player, weapon.WeaponId);
}
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

@@ -1,10 +1,6 @@
namespace TTT.API.Player;
public interface IOnlinePlayer : IPlayer {
// [Obsolete(
// "Roles are now managed via IRoleAssigner. Use IRoleAssigner.GetRoles(IPlayer) instead.")]
// ICollection<IRole> Roles { get; }
public int Health { get; set; }
public int MaxHealth { get; set; }
public int Armor { get; set; }

View File

@@ -2,6 +2,11 @@ namespace TTT.API.Player;
public interface IPlayerFinder {
public IOnlinePlayer AddPlayer(IOnlinePlayer player);
public void AddPlayers(params IOnlinePlayer[] players) {
foreach (var p in players) AddPlayer(p);
}
public IPlayer RemovePlayer(IPlayer player);
ISet<IOnlinePlayer> GetOnline();

View File

@@ -19,7 +19,11 @@ public interface IRoleAssigner : IKeyedStorage<IPlayer, ICollection<IRole>>,
/// <param name="roles"></param>
public void AssignRoles(ISet<IOnlinePlayer> players, IList<IRole> roles);
public void SetRole(IOnlinePlayer player, IRole role) {
Write(player, new List<IRole> { role }).GetAwaiter().GetResult();
}
public ICollection<IRole> GetRoles(IPlayer player) {
return Load(player).GetAwaiter().GetResult() ?? Array.Empty<IRole>();
return Load(player).GetAwaiter().GetResult() ?? [];
}
}

View File

@@ -0,0 +1,9 @@
using CounterStrikeSharp.API.Core;
namespace TTT.CS2.API;
public interface IAliveSpoofer {
ISet<CCSPlayerController> FakeAlivePlayers { get; }
void SpoofAlive(CCSPlayerController player);
void UnspoofAlive(CCSPlayerController player);
}

View File

@@ -0,0 +1,24 @@
using System.Diagnostics.CodeAnalysis;
using CounterStrikeSharp.API.Core;
using TTT.Game;
namespace TTT.CS2.API;
public interface IBodyTracker {
public IDictionary<IBody, CRagdollProp> Bodies { get; }
public IBody? ReverseLookup(CRagdollProp ragdoll) {
return Bodies.FirstOrDefault(x => x.Value == ragdoll).Key;
}
public bool TryReverseLookup(CRagdollProp ragdoll,
[MaybeNullWhen(false)] out IBody body) {
body = ReverseLookup(ragdoll);
return body != null;
}
public bool TryLookup(string id, out IBody? body) {
body = Bodies.Keys.FirstOrDefault(x => x.Id == id);
return body != null;
}
}

View File

@@ -11,6 +11,12 @@
<ItemGroup>
<ProjectReference Include="..\API\API.csproj"/>
<ProjectReference Include="..\Game\Game.csproj"/>
<ProjectReference Include="..\Karma\Karma.csproj"/>
<ProjectReference Include="..\ShopAPI\ShopAPI.csproj"/>
</ItemGroup>
<ItemGroup>
<Folder Include="RayTrace\"/>
</ItemGroup>
</Project>

View File

@@ -2,7 +2,6 @@ using CounterStrikeSharp.API.Core;
using TTT.API;
using TTT.API.Player;
using TTT.Game;
using TTT.Game.Roles;
namespace TTT.CS2;
@@ -12,25 +11,17 @@ public class CS2Body(CRagdollProp ragdoll, IPlayer player) : IBody {
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;
return this;
}
public CS2Body WithWeapon(string weapon) {
return WithWeapon(new BaseWeapon(weapon));
}
public CS2Body WithKiller(IPlayer? killer) {
Killer = killer;
return this;
}
public CS2Body Identified(bool identified = true) {
IsIdentified = identified;
return this;
}
}

15
TTT/CS2/CS2Logger.cs Normal file
View File

@@ -0,0 +1,15 @@
using CounterStrikeSharp.API;
using TTT.API.Player;
using TTT.Game.Loggers;
namespace TTT.CS2;
public class CS2Logger(IServiceProvider provider) : SimpleLogger(provider) {
public override void PrintLogs() {
Server.NextWorldUpdate(() => base.PrintLogs());
}
public override void PrintLogs(IOnlinePlayer? player) {
Server.NextWorldUpdate(() => base.PrintLogs(player));
}
}

View File

@@ -1,14 +1,21 @@
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;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.CS2.API;
using TTT.CS2.Command;
using TTT.CS2.Command.Test;
using TTT.CS2.Configs;
using TTT.CS2.Configs.ShopItems;
using TTT.CS2.Game;
using TTT.CS2.GameHandlers;
using TTT.CS2.GameHandlers.DamageCancelers;
using TTT.CS2.Hats;
using TTT.CS2.lang;
using TTT.CS2.Listeners;
@@ -20,36 +27,55 @@ namespace TTT.CS2;
public static class CS2ServiceCollection {
public static void AddCS2Services(this IServiceCollection collection) {
// Base Requirements
collection.AddScoped<IGameManager, CS2GameManager>();
// TTT - CS2 Specific requirements
collection
.AddModBehavior<IPlayerConverter<CCSPlayerController>,
CCPlayerConverter>();
collection.AddScoped<IPlayerFinder, CS2PlayerFinder>();
collection.AddModBehavior<ICommandManager, CS2CommandManager>();
collection.AddModBehavior<IAliveSpoofer, CS2AliveSpoofer>();
collection.AddModBehavior<IIconManager, RoleIconsHandler>();
// Configs
collection.AddModBehavior<IStorage<TTTConfig>, CS2GameConfig>();
collection.AddPluginBehavior<ICommandManager, CS2CommandManager>();
collection.AddScoped<IMessenger, CS2Messenger>();
collection.AddScoped<IInventoryManager, CS2InventoryManager>();
collection.AddModBehavior<IStorage<ShopConfig>, CS2ShopConfig>();
collection
.AddModBehavior<IStorage<OneShotDeagleConfig>, CS2OneShotDeagleConfig>();
collection.AddModBehavior<IStorage<C4Config>, CS2C4Config>();
// TTT - CS2 Specific optionals
collection.AddScoped<ITextSpawner, TextSpawner>();
// GameHandlers
collection.AddPluginBehavior<PlayerConnectionsHandler>();
collection.AddPluginBehavior<RoundEndHandler>();
collection.AddPluginBehavior<RoundStartHandler>();
collection.AddPluginBehavior<CombatHandler>();
collection.AddPluginBehavior<PropMover>();
collection.AddPluginBehavior<BodySpawner>();
collection.AddPluginBehavior<RoleIconsHandler>();
collection.AddModBehavior<BodySpawner>();
collection.AddModBehavior<CombatHandler>();
collection.AddModBehavior<DamageCanceler>();
collection.AddModBehavior<PlayerConnectionsHandler>();
collection.AddModBehavior<PropMover>();
collection.AddModBehavior<RoundEnd_GameEndHandler>();
collection.AddModBehavior<RoundStart_GameStartHandler>();
// Damage Cancelers
collection.AddModBehavior<OutOfRoundCanceler>();
collection.AddModBehavior<TaserListenCanceler>();
// Listeners
collection.AddListener<RoundTimerListener>();
collection.AddListener<BodyPickupListener>();
collection.AddModBehavior<BodyPickupListener>();
collection.AddModBehavior<IBodyTracker, BodyTracker>();
collection.AddModBehavior<LateSpawnListener>();
collection.AddModBehavior<PlayerStatsTracker>();
collection.AddModBehavior<RoundTimerListener>();
collection.AddModBehavior<ScreenColorApplier>();
// Commands
#if DEBUG
collection.AddModBehavior<TestCommand>();
#endif
collection.AddScoped<IGameManager, CS2GameManager>();
collection.AddScoped<IInventoryManager, CS2InventoryManager>();
collection.AddScoped<IMessenger, CS2Messenger>();
collection.AddScoped<IMsgLocalizer, StringLocalizer>();
collection.AddScoped<IPermissionManager, CS2PermManager>();
collection.AddScoped<IPlayerFinder, CS2PlayerFinder>();
}
}

View File

@@ -68,7 +68,7 @@ public class CS2CommandInfo : ICommandInfo {
public int ArgCount => Args.Length;
public string GetCommandString => string.Join(' ', Args);
public string CommandString => string.Join(' ', Args);
public void ReplySync(string message) {
switch (CallingContext) {
@@ -80,4 +80,8 @@ public class CS2CommandInfo : ICommandInfo {
break;
}
}
public ICommandInfo Skip(int count = 1) {
return new CS2CommandInfo(provider, this, count);
}
}

View File

@@ -5,9 +5,8 @@ using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Command;
using TTT.API.Player;
using TTT.CS2.Command.Test;
using TTT.Game;
using TTT.Game.Commands;
using TTT.Game.lang;
namespace TTT.CS2.Command;
@@ -23,21 +22,12 @@ public class CS2CommandManager(IServiceProvider provider)
public void Start(BasePlugin? basePlugin, bool hotReload) {
plugin = basePlugin;
base.Start();
RegisterCommand(new TTTCommand(Provider));
RegisterCommand(new TestCommand(Provider));
foreach (var command in Provider.GetServices<ICommand>()) command.Start();
}
public override string Name => "CommandManager";
public override string Version => GitVersionInformation.FullSemVer;
public override bool RegisterCommand(ICommand command) {
command.Start();
var registration = command.Aliases.All(alias
=> Commands.TryAdd(COMMAND_PREFIX + alias, command));
if (registration == false) return false;
=> cmdMap.TryAdd(COMMAND_PREFIX + alias, command));
if (!registration) return false;
foreach (var alias in command.Aliases)
plugin?.AddCommand(COMMAND_PREFIX + alias,
command.Description ?? string.Empty, processInternal);
@@ -52,14 +42,14 @@ public class CS2CommandManager(IServiceProvider provider)
converter.GetPlayer(executor) as IOnlinePlayer;
Task.Run(async () => {
try {
Console.WriteLine($"Processing command: {cs2Info.GetCommandString}");
Console.WriteLine($"Processing command: {cs2Info.CommandString}");
return await ProcessCommand(cs2Info);
} catch (Exception e) {
var msg = e.Message;
cs2Info.ReplySync(Localizer[GameMsgs.GENERIC_ERROR(msg)]);
await Server.NextWorldUpdateAsync(() => {
Console.WriteLine(
$"Encountered an error when processing command: \"{cs2Info.GetCommandString}\" by {wrapper?.Id}");
$"Encountered an error when processing command: \"{cs2Info.CommandString}\" by {wrapper?.Id}");
Console.WriteLine(e);
});
return CommandResult.ERROR;

View File

@@ -0,0 +1,29 @@
using CounterStrikeSharp.API;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Player;
using TTT.CS2.API;
namespace TTT.CS2.Command.Test;
public class ForceAliveCommand(IServiceProvider provider) : ICommand {
private readonly IAliveSpoofer spoofer =
provider.GetRequiredService<IAliveSpoofer>();
public void Dispose() { }
public string Id => "forcealive";
public void Start() { }
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
Server.NextWorldUpdate(() => {
foreach (var player in Utilities.GetPlayers()) spoofer.SpoofAlive(player);
});
info.ReplySync("Attempted to force alive.");
return Task.FromResult(CommandResult.SUCCESS);
}
}

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

@@ -0,0 +1,33 @@
using CounterStrikeSharp.API;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Events;
using TTT.API.Player;
using TTT.CS2.API;
using TTT.Game.Events.Body;
namespace TTT.CS2.Command.Test;
public class IdentifyAllCommand(IServiceProvider provider) : ICommand {
private readonly IBodyTracker bodies =
provider.GetRequiredService<IBodyTracker>();
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
public string Id => "identifyall";
public void Dispose() { }
public void Start() { }
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
foreach (var body in bodies.Bodies.Keys) {
if (body.IsIdentified) continue;
var bodyIdentifyEvent = new BodyIdentifyEvent(body, executor);
Server.NextWorldUpdate(() => bus.Dispatch(bodyIdentifyEvent));
}
info.ReplySync("Identified all bodies.");
return Task.FromResult(CommandResult.SUCCESS);
}
}

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

@@ -0,0 +1,35 @@
using System.Drawing;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Player;
using TTT.CS2.Extensions;
namespace TTT.CS2.Command.Test;
public class ScreenColorCommand(IServiceProvider provider) : ICommand {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public string Id => "screencolor";
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(() => {
var player = converter.GetPlayer(executor);
float hold = 0.5f, fade = 0.5f;
if (info.ArgCount >= 2) float.TryParse(info.Args[1], out hold);
if (info.ArgCount >= 3) float.TryParse(info.Args[2], out fade);
player?.ColorScreen(Color.Red, hold, fade);
info.ReplySync("Colored your screen red.");
});
return Task.FromResult(CommandResult.SUCCESS);
}
}

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

@@ -4,7 +4,6 @@ using TTT.API.Command;
using TTT.API.Events;
using TTT.API.Player;
using TTT.API.Role;
using TTT.CS2.Roles;
using TTT.Game.Events.Player;
using TTT.Game.Roles;
@@ -18,8 +17,7 @@ public class SetRoleCommand(IServiceProvider provider) : ICommand {
public void Dispose() { }
public string Name => "setrole";
public string Version => GitVersionInformation.FullSemVer;
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

@@ -0,0 +1,26 @@
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Game;
using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class StateCommand(IServiceProvider provider) : ICommand {
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
public string Id => "state";
public void Dispose() { }
public void Start() { }
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (games.ActiveGame == null) {
info.ReplySync("ActiveGame is null.");
return Task.FromResult(CommandResult.SUCCESS);
}
info.ReplySync($"Current game state: {games.ActiveGame?.State}");
return Task.FromResult(CommandResult.SUCCESS);
}
}

View File

@@ -7,19 +7,20 @@ using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class StopCommand(IServiceProvider provider) : ICommand {
public void Dispose() { }
public string Name => "stop";
public string Version => GitVersionInformation.FullSemVer;
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
public void Dispose() { }
public string Id => "stop";
public void Start() { }
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
Server.NextWorldUpdate(() => {
if (!games.IsGameActive()) {
if (games.ActiveGame is not {
State: State.COUNTDOWN or State.IN_PROGRESS
}) {
info.ReplySync("No game is currently running.");
return;
}

View File

@@ -1,5 +1,4 @@
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Command;
using TTT.API.Player;
@@ -7,23 +6,24 @@ using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class TestCommand(IServiceProvider provider) : ICommand, IPluginModule {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
private readonly IDictionary<string, ICommand> subCommands =
new Dictionary<string, ICommand>(StringComparer.OrdinalIgnoreCase);
public void Dispose() { }
public string Name => "test";
public string Version => GitVersionInformation.FullSemVer;
public string Id => "test";
public void Start() {
subCommands.Add("setrole", new SetRoleCommand(provider));
subCommands.Add("stop", new StopCommand(provider));
subCommands.Add("forcealive", new ForceAliveCommand(provider));
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>
@@ -33,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);
}
@@ -43,6 +43,12 @@ public class TestCommand(IServiceProvider provider) : ICommand, IPluginModule {
return Task.FromResult(CommandResult.INVALID_ARGS);
}
return cmd.Execute(executor, new CS2CommandInfo(provider, info, 1));
return cmd.Execute(executor, info.Skip());
}
public void Start(BasePlugin? plugin, bool hotload) {
((IPluginModule)this).Start();
foreach (var cmd in subCommands.Values.OfType<IPluginModule>())
cmd.Start(plugin, hotload);
}
}

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(
@@ -84,8 +84,6 @@ public class CS2GameConfig : IStorage<TTTConfig>, IPluginModule {
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 60));
public void Dispose() { }
public string Name => "CS2GameConfig";
public string Version => GitVersionInformation.FullSemVer;
public void Start() { }

View File

@@ -0,0 +1,114 @@
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;
namespace TTT.CS2.Configs;
public class CS2ShopConfig : IStorage<ShopConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_STARTING_INNOCENT_CREDITS = new(
"css_ttt_shop_start_innocent", "Starting credits for Innocents", 100,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_STARTING_TRAITOR_CREDITS = new(
"css_ttt_shop_start_traitor", "Starting credits for Traitors", 120,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_STARTING_DETECTIVE_CREDITS = new(
"css_ttt_shop_start_detective", "Starting credits for Detectives", 150,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_INNO_V_INNO = new(
"css_ttt_shop_inno_v_inno", "Credits change when Innocent kills Innocent",
-4, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-10000, 10000));
public static readonly FakeConVar<int> CV_INNO_V_TRAITOR = new(
"css_ttt_shop_inno_v_traitor", "Credits change when Innocent kills Traitor",
8, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-10000, 10000));
public static readonly FakeConVar<int> CV_INNO_V_DETECTIVE = new(
"css_ttt_shop_inno_v_detective",
"Credits change when Innocent kills Detective", -6, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(-10000, 10000));
public static readonly FakeConVar<int> CV_TRAITOR_V_TRAITOR = new(
"css_ttt_shop_traitor_v_traitor",
"Credits change when Traitor kills Traitor", -5, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(-10000, 10000));
public static readonly FakeConVar<int> CV_TRAITOR_V_INNO = new(
"css_ttt_shop_traitor_v_inno", "Credits change when Traitor kills Innocent",
4, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-10000, 10000));
public static readonly FakeConVar<int> CV_TRAITOR_V_DETECTIVE = new(
"css_ttt_shop_traitor_v_detective",
"Credits change when Traitor kills Detective", 6, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(-10000, 10000));
public static readonly FakeConVar<int> CV_DETECTIVE_V_DETECTIVE = new(
"css_ttt_shop_detective_v_detective",
"Credits change when Detective kills Detective", -8, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(-10000, 10000));
public static readonly FakeConVar<int> CV_DETECTIVE_V_INNO = new(
"css_ttt_shop_detective_v_inno",
"Credits change when Detective kills Innocent", -6, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(-10000, 10000));
public static readonly FakeConVar<int> CV_DETECTIVE_V_TRAITOR = new(
"css_ttt_shop_detective_v_traitor",
"Credits change when Detective kills Traitor", 8, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(-10000, 10000));
public static readonly FakeConVar<int> CV_ANY_KILL = new(
"css_ttt_shop_any_kill",
"Credits granted for any kill when roles are unknown", 2,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-10000, 10000));
public static readonly FakeConVar<float> CV_ASSIST_MULTIPLIER = new(
"css_ttt_shop_assist_multiplier", "Multiplier applied to assister credits",
0.5f, ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0f, 10f));
public static readonly FakeConVar<float> CV_SOLO_KILL_MULTIPLIER = new(
"css_ttt_shop_solo_kill_multiplier",
"Multiplier applied to killer credits when there is no assist", 1.5f,
ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0f, 10f));
private readonly IServiceProvider _provider;
public CS2ShopConfig(IServiceProvider provider) { _provider = provider; }
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<ShopConfig?> Load() {
var cfg = new ShopConfig(_provider) {
StartingInnocentCredits = CV_STARTING_INNOCENT_CREDITS.Value,
StartingTraitorCredits = CV_STARTING_TRAITOR_CREDITS.Value,
StartingDetectiveCredits = CV_STARTING_DETECTIVE_CREDITS.Value,
CreditsForInnoVInnoKill = CV_INNO_V_INNO.Value,
CreditsForInnoVTraitorKill = CV_INNO_V_TRAITOR.Value,
CreditsForInnoVDetectiveKill = CV_INNO_V_DETECTIVE.Value,
CreditsForTraitorVTraitorKill = CV_TRAITOR_V_TRAITOR.Value,
CreditsForTraitorVInnoKill = CV_TRAITOR_V_INNO.Value,
CreditsForTraitorVDetectiveKill = CV_TRAITOR_V_DETECTIVE.Value,
CreditsForDetectiveVDetectiveKill = CV_DETECTIVE_V_DETECTIVE.Value,
CreditsForDetectiveVInnoKill = CV_DETECTIVE_V_INNO.Value,
CreditsForDetectiveVTraitorKill = CV_DETECTIVE_V_TRAITOR.Value,
CreditsForAnyKill = CV_ANY_KILL.Value,
CreditMultiplierForAssisting = CV_ASSIST_MULTIPLIER.Value,
CreditsMultiplierForNotAssisted = CV_SOLO_KILL_MULTIPLIER.Value
};
return Task.FromResult<ShopConfig?>(cfg);
}
}

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,47 @@
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 CS2OneShotDeagleConfig : IStorage<OneShotDeagleConfig>,
IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"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");
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 Weapon", "weapon_revolver",
ConVarFlags.FCVAR_NONE, new ItemValidator(allowEmpty: false));
public void Dispose() { }
public void Start() { }
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,
KillShooterOnFF = CV_KILL_SHOOTER_ON_FF.Value
};
return Task.FromResult<OneShotDeagleConfig?>(cfg);
}
}

View File

@@ -1,14 +1,19 @@
using System.Drawing;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
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;
var pawn = player.PlayerPawn.Value;
var pawn = player.Pawn.Value;
if (pawn == null || !pawn.IsValid) return null;
return pawn.WeaponServices?.MyWeapons
@@ -25,13 +30,57 @@ 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.PlayerPawn.Value;
var pawn = player.Pawn.Value;
if (!player.IsValid || pawn == null || !pawn.IsValid) return;
if (color.A == 255)
color = Color.FromArgb(pawn.Render.A, color.R, color.G, color.B);
player.PlayerPawn.Value.SetColor(color);
color = Color.FromArgb(pawn.Render.A == 255 ? 255 : 254, color.R, color.G,
color.B);
pawn.SetColor(color);
}
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) {
var fadeMsg = UserMessage.FromId(106);
fadeMsg.SetInt("duration", Convert.ToInt32(fade * 512));
fadeMsg.SetInt("hold_time", Convert.ToInt32(hold * 512));
var flag = flags switch {
FadeFlags.FADE_IN => 0x0001,
FadeFlags.FADE_OUT => 0x0002,
FadeFlags.FADE_STAYOUT => 0x0008,
_ => 0x0001
};
if (withPurge) flag |= 0x0010;
fadeMsg.SetInt("flags", flag);
fadeMsg.SetInt("color",
color.R | color.G << 8 | color.B << 16 | color.A << 24);
fadeMsg.Send(player);
}
}

View File

@@ -1,4 +1,6 @@
using CounterStrikeSharp.API.Modules.Utils;
using System.Numerics;
using CounterStrikeSharp.API.Modules.Utils;
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
namespace TTT.CS2.Extensions;
@@ -44,6 +46,40 @@ public static class VectorExtensions {
(float)(Math.Sin(yaw) * cosPitch), (float)-Math.Sin(pitch));
}
public static Vector ToRight(this QAngle angle) {
var pitch = angle.X * (Math.PI / 180.0);
var yaw = angle.Y * (Math.PI / 180.0);
var roll = angle.Z * (Math.PI / 180.0);
var sinPitch = Math.Sin(pitch);
var cosPitch = Math.Cos(pitch);
var sinYaw = Math.Sin(yaw);
var cosYaw = Math.Cos(yaw);
var sinRoll = Math.Sin(roll);
var cosRoll = Math.Cos(roll);
return new Vector((float)(sinYaw * sinPitch * cosRoll - cosYaw * sinRoll),
(float)(-cosYaw * sinPitch * cosRoll - sinYaw * sinRoll),
(float)(cosPitch * -sinRoll));
}
public static Vector ToUp(this QAngle angle) {
var pitch = angle.X * (Math.PI / 180.0);
var yaw = angle.Y * (Math.PI / 180.0);
var roll = angle.Z * (Math.PI / 180.0);
var sinPitch = Math.Sin(pitch);
var cosPitch = Math.Cos(pitch);
var sinYaw = Math.Sin(yaw);
var cosYaw = Math.Cos(yaw);
var sinRoll = Math.Sin(roll);
var cosRoll = Math.Cos(roll);
return new Vector((float)(-cosYaw * sinPitch * cosRoll - sinYaw * sinRoll),
(float)(-sinYaw * sinPitch * cosRoll + cosYaw * sinRoll),
(float)(cosPitch * cosRoll));
}
public static Vector Lerp(this Vector from, Vector to, float t) {
return new Vector(from.X + (to.X - from.X) * t,
from.Y + (to.Y - from.Y) * t, from.Z + (to.Z - from.Z) * t);
@@ -53,8 +89,12 @@ public static class VectorExtensions {
float maxDelta) {
var toVector = target - current;
var dist = toVector.Length();
if (dist <= maxDelta || dist == 0f) { return target; }
if (dist <= maxDelta || dist == 0f) return target;
return current + toVector / dist * maxDelta;
}
public static Vector toVector(this Vector3 vec) {
return new Vector(vec.X, vec.Y, vec.Z);
}
}

View File

@@ -6,11 +6,14 @@ 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;
public class CS2Game(IServiceProvider provider) : RoundBasedGame(provider) {
public override IActionLogger Logger { get; } = new CS2Logger(provider);
public override IList<IRole> Roles { get; } = [
new SpectatorRole(provider), new InnocentRole(provider),
new TraitorRole(provider), new DetectiveRole(provider)
@@ -40,6 +43,10 @@ public class CS2Game(IServiceProvider provider) : RoundBasedGame(provider) {
return;
}
if (countdown != null)
Messenger?.MessageAll(
Locale[GameMsgs.GAME_STATE_STARTING(countdown.Value)]);
timer.Subscribe(_ => {
Server.NextWorldUpdate(() => {
if (RoundUtil.IsWarmup()) return;

View File

@@ -1,15 +1,26 @@
using TTT.API.Game;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Game;
using TTT.API.Messages;
using TTT.Game;
using TTT.Game.Events.Game;
namespace TTT.CS2.Game;
public class CS2GameManager(IServiceProvider provider) : GameManager(provider) {
public override IGame CreateGame() {
if (((IGameManager)this).IsGameActive())
throw new InvalidOperationException(
"A game is already active. Please end the current game before starting a new one.");
protected readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
public override IGame CreateGame() {
messenger.Debug("Attempting to create a new CS2 game...");
switch (ActiveGame) {
case { State: State.IN_PROGRESS or State.COUNTDOWN }:
throw new InvalidOperationException(
"A game is already active. End the current game before starting a new one.");
case { State: State.WAITING }:
return ActiveGame;
}
messenger.Debug("Creating a new CS2 game instance...");
ActiveGame = new CS2Game(Provider);
var ev = new GameInitEvent(ActiveGame);

View File

@@ -4,12 +4,15 @@ 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;
using TTT.API.Game;
using TTT.API.Player;
using TTT.CS2.Extensions;
using TTT.Game.Events.Body;
using TTT.Game.Roles;
namespace TTT.CS2.GameHandlers;
@@ -19,18 +22,20 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly PropMover mover = provider.GetRequiredService<PropMover>();
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
public void Dispose() { }
public string Name => nameof(BodySpawner);
public string Version => GitVersionInformation.FullSemVer;
public void Start() { }
[UsedImplicitly]
[GameEventHandler]
public HookResult OnDeath(EventPlayerDeath ev, GameEventInfo _) {
if (games.ActiveGame is not { State: State.IN_PROGRESS })
return HookResult.Continue;
var player = ev.Userid;
if (player == null || !player.IsValid) return HookResult.Continue;
player.SetColor(Color.FromArgb(0, 0, 0, 0));
player.SetColor(Color.FromArgb(0, 255, 255, 255));
var ragdollBody = makeGameRagdoll(player);
var body = new CS2Body(ragdollBody, converter.GetPlayer(player));
@@ -38,45 +43,41 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
if (ev.Attacker != null && ev.Attacker.IsValid)
body.WithKiller(converter.GetPlayer(ev.Attacker));
body.WithWeapon(ev.Weapon);
body.WithWeapon(new BaseWeapon(ev.Weapon));
var bodyCreatedEvent = new BodyCreateEvent(body);
bus.Dispatch(bodyCreatedEvent);
if (bodyCreatedEvent.IsCanceled) {
ragdollBody.AcceptInput("Kill");
return HookResult.Continue;
}
mover.MapEntities.Add(ragdollBody);
if (bodyCreatedEvent.IsCanceled) ragdollBody.AcceptInput("Kill");
return HookResult.Continue;
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnStart(EventRoundStart ev, GameEventInfo _) {
Server.NextWorldUpdate(() => {
foreach (var player in Utilities.GetPlayers())
player.SetColor(Color.FromArgb(254, 255, 255, 255));
player.SetColor(Color.White);
});
return HookResult.Continue;
}
private CRagdollProp makeGameRagdoll(CCSPlayerController playerController) {
var ragdoll = Utilities.CreateEntityByName<CRagdollProp>("prop_ragdoll");
var pawn = playerController.PlayerPawn.Value;
var pawn = playerController.Pawn.Value;
if (ragdoll == null || !ragdoll.IsValid || playerController == null)
throw new ArgumentNullException(nameof(ragdoll));
if (pawn == null || !pawn.IsValid)
throw new ArgumentException("PlayerPawn is not valid",
throw new ArgumentException("Pawn is not valid",
nameof(playerController));
var origin = pawn.AbsOrigin.Clone();
var rotation = pawn.AbsRotation.Clone();
if (origin == null)
throw new ArgumentException("PlayerPawn AbsOrigin is null",
throw new ArgumentException("Pawn AbsOrigin is null",
nameof(playerController));
origin.Z += 30;

View File

@@ -5,7 +5,9 @@ using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.CS2.API;
using TTT.Game.Events.Player;
namespace TTT.CS2.GameHandlers;
@@ -16,8 +18,11 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public string Name => "CombatListeners";
public string Version => GitVersionInformation.FullSemVer;
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
private readonly IAliveSpoofer spoofer =
provider.GetRequiredService<IAliveSpoofer>();
public void Start() { }
@@ -31,6 +36,8 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
[UsedImplicitly]
[GameEventHandler(HookMode.Pre)]
public HookResult OnPlayerDeath_Pre(EventPlayerDeath ev, GameEventInfo info) {
if (games.ActiveGame is not { State: State.IN_PROGRESS })
return HookResult.Continue;
var player = ev.Userid;
if (player == null) return HookResult.Continue;
var deathEvent = new PlayerDeathEvent(converter, ev);
@@ -38,6 +45,15 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
Server.NextWorldUpdateAsync(() => bus.Dispatch(deathEvent));
info.DontBroadcast = true;
hideAndTrackStats(ev, player);
spoofer.SpoofAlive(player);
return HookResult.Continue;
}
private void hideAndTrackStats(EventPlayerDeath ev,
CCSPlayerController player) {
var victimStats = player.ActionTrackingServices?.MatchStats;
if (victimStats != null) {
victimStats.Deaths -= 1;
@@ -46,41 +62,30 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
}
var killerStats = ev.Attacker?.ActionTrackingServices?.MatchStats;
if (killerStats != null) {
killerStats.Kills -= 1;
killerStats.Damage -= ev.DmgHealth;
if (killerStats == null) return;
killerStats.Kills -= 1;
killerStats.Damage -= ev.DmgHealth;
if (ev.Attacker != null)
Utilities.SetStateChanged(ev.Attacker, "CCSPlayerController",
"m_pActionTrackingServices");
var assisterStats = ev.Assister?.ActionTrackingServices?.MatchStats;
if (assisterStats != null && assisterStats != killerStats)
assisterStats.Assists -= 1;
if (ev.Assister != null)
Utilities.SetStateChanged(ev.Assister, "CCSPlayerController",
"m_pActionTrackingServices");
if (ev.Attacker != null) {
Utilities.SetStateChanged(ev.Attacker, "CCSPlayerController",
"m_pActionTrackingServices");
ev.FireEventToClient(ev.Attacker);
}
// These delays are necessary for the game engine
Server.NextWorldUpdate(() => {
var pawn = player.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid) return;
pawn.DeathTime = 0;
Utilities.SetStateChanged(pawn, "CBasePlayerPawn", "m_flDeathTime");
var assisterStats = ev.Assister?.ActionTrackingServices?.MatchStats;
if (assisterStats != null && assisterStats != killerStats)
assisterStats.Assists -= 1;
Server.NextWorldUpdate(() => {
player.PawnIsAlive = true;
Utilities.SetStateChanged(player, "CCSPlayerController",
"m_bPawnIsAlive");
});
});
return HookResult.Continue;
if (ev.Assister != null)
Utilities.SetStateChanged(ev.Assister, "CCSPlayerController",
"m_pActionTrackingServices");
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnPlayerHurt(EventPlayerHurt ev, GameEventInfo _) {
// DamageCanceler already handles this on non-Windows platforms
if (!OperatingSystem.IsWindows()) return HookResult.Continue;
var player = ev.Userid;
if (player == null) return HookResult.Continue;
@@ -88,15 +93,9 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
bus.Dispatch(dmgEvent);
var pawn = player.Pawn.Value;
if (pawn != null && pawn.IsValid) {
pawn.Health = dmgEvent.HpLeft;
if (player.PlayerPawn.Value != null && player.PlayerPawn.Value.IsValid)
player.PlayerPawn.Value.ArmorValue = dmgEvent.ArmorRemaining;
}
ev.Health = dmgEvent.HpLeft;
ev.Armor = dmgEvent.ArmorRemaining;
if (dmgEvent.IsCanceled) return HookResult.Handled;
return HookResult.Continue;
return dmgEvent.IsCanceled ? HookResult.Handled : HookResult.Continue;
}
}

View File

@@ -0,0 +1,42 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Events;
using TTT.API.Player;
using TTT.Game.Events.Player;
namespace TTT.CS2.GameHandlers;
public class DamageCanceler(IServiceProvider provider) : IPluginModule {
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
if (OperatingSystem.IsWindows()) return;
VirtualFunctions.CBaseEntity_TakeDamageOldFunc.Hook(onTakeDamage,
HookMode.Pre);
}
private HookResult onTakeDamage(DynamicHook hook) {
var damagedEvent = new PlayerDamagedEvent(converter, hook);
bus.Dispatch(damagedEvent);
if (damagedEvent.IsCanceled) return HookResult.Handled;
if (!damagedEvent.HpModified
|| damagedEvent.Player is not IOnlinePlayer onlinePlayer)
return HookResult.Continue;
onlinePlayer.Health = damagedEvent.HpLeft;
return HookResult.Handled;
}
}

View File

@@ -0,0 +1,17 @@
using TTT.API.Events;
using TTT.API.Game;
using TTT.CS2.Utils;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
namespace TTT.CS2.GameHandlers.DamageCancelers;
public class OutOfRoundCanceler(IServiceProvider provider)
: BaseListener(provider) {
[EventHandler]
public void OnHurt(PlayerDamagedEvent ev) {
if (RoundUtil.IsWarmup()) return;
if (Games.ActiveGame is not { State: State.IN_PROGRESS })
ev.IsCanceled = true;
}
}

View File

@@ -0,0 +1,27 @@
using TTT.API.Events;
using TTT.API.Game;
using TTT.CS2.lang;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
namespace TTT.CS2.GameHandlers.DamageCancelers;
public class TaserListenCanceler(IServiceProvider provider)
: BaseListener(provider) {
[EventHandler]
public void OnHurt(PlayerDamagedEvent ev) {
if (Games.ActiveGame is not { State: State.IN_PROGRESS }) return;
if (ev.Weapon == null) return;
if (!ev.Weapon.Contains("taser", StringComparison.OrdinalIgnoreCase))
return;
ev.IsCanceled = true;
var victim = ev.Player;
var attacker = ev.Attacker;
if (attacker == null) return;
Messenger.Message(attacker,
Locale[CS2Msgs.TASER_SCANNED(victim, Roles.GetRoles(victim).First())]);
}
}

View File

@@ -0,0 +1,47 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Player;
using TTT.Karma;
namespace TTT.CS2.GameHandlers;
public class KarmaSyncer(IServiceProvider provider) : IPluginModule {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IKarmaService? karma = provider.GetService<IKarmaService>();
private readonly IPlayerFinder players =
provider.GetRequiredService<IPlayerFinder>();
public void Dispose() { }
public string Id => nameof(KarmaSyncer);
public string Version => GitVersionInformation.FullSemVer;
public void Start() { }
[GameEventHandler]
public HookResult OnRoundStart(EventRoundStart _, GameEventInfo _1) {
if (karma == null) return HookResult.Continue;
foreach (var p in Utilities.GetPlayers()) {
if (!p.IsValid || p.IsBot) continue;
var apiPlayer = converter.GetPlayer(p);
Task.Run(async () => {
var pk = await karma.Load(apiPlayer);
await Server.NextFrameAsync(() => {
p.Score = pk;
Utilities.SetStateChanged(p, "CCSPlayerController",
"m_pActionTrackingServices");
});
});
}
return HookResult.Continue;
}
}

View File

@@ -15,9 +15,6 @@ public class PlayerConnectionsHandler(IServiceProvider provider)
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public string Name => nameof(PlayerConnectionsHandler);
public string Version => GitVersionInformation.FullSemVer;
public void Start() { }
public void Start(BasePlugin? plugin, bool hotReload) {
@@ -29,27 +26,23 @@ public class PlayerConnectionsHandler(IServiceProvider provider)
CounterStrikeSharp.API.Core.Listeners.OnClientDisconnect>(
disconnectFromServer);
Server.NextWorldUpdate(() => {
foreach (var player in Utilities.GetPlayers()) {
var gamePlayer = converter.GetPlayer(player);
var ev = 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() { }
private void disconnectFromServer(int playerSlot) {
var player = Utilities.GetPlayerFromSlot(playerSlot);
Console.WriteLine($"Player {playerSlot} disconnected from server.");
if (player == null || !player.IsValid) {
Console.WriteLine($"Player {playerSlot} does not exist.");
return;
}
if (player == null || !player.IsValid) return;
var gamePlayer = converter.GetPlayer(player);
bus.Dispatch(new PlayerLeaveEvent(gamePlayer));
Server.NextWorldUpdate(()
=> bus.Dispatch(new PlayerLeaveEvent(gamePlayer)));
}
private void connectToServer(int playerSlot) {
@@ -61,6 +54,7 @@ public class PlayerConnectionsHandler(IServiceProvider provider)
}
var gamePlayer = converter.GetPlayer(player);
bus.Dispatch(new PlayerJoinEvent(gamePlayer));
Server.NextWorldUpdate(() => bus.Dispatch(new PlayerJoinEvent(gamePlayer)));
}
}

View File

@@ -1,120 +1,81 @@
using System.Drawing;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Timers;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Events;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.CS2.Events;
using TTT.CS2.Extensions;
using TTT.CS2.Utils;
using TTT.CS2.RayTrace.Class;
using TTT.CS2.RayTrace.Enum;
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
namespace TTT.CS2.GameHandlers;
public class PropMover(IServiceProvider provider) : IPluginModule {
// TODO: Make this configurable
public static readonly float MIN_LOOK_ACCURACY = 2000f;
public static readonly float MAX_DISTANCE = 100f;
public static readonly float MIN_HOLDING_DISTANCE = 100f;
public static readonly float MAX_HOLDING_DISTANCE = 10000f;
public static readonly float MAX_DISTANCE = 200;
public static readonly float MIN_HOLDING_DISTANCE = 80;
public static readonly float MAX_HOLDING_DISTANCE = 150;
private static readonly QAngle DEAD_ANGLE = new(90, 45, 90);
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public readonly HashSet<CBaseEntity> MapEntities = [];
private readonly IMessenger msg = provider.GetRequiredService<IMessenger>();
private readonly Dictionary<CCSPlayerController, MovementInfo>
playersPressingE = new();
public void Dispose() { }
public string Name => nameof(PropMover);
public string Version => GitVersionInformation.FullSemVer;
public void Start() { }
public void Start(BasePlugin? plugin, bool hotReload) {
plugin?.AddTimer(Server.TickInterval, refreshLines, TimerFlags.REPEAT);
plugin?.RegisterListener<CounterStrikeSharp.API.Core.Listeners.OnTick>(
refreshBodies);
refreshHeld);
plugin
?.RegisterListener<
CounterStrikeSharp.API.Core.Listeners.OnPlayerButtonsChanged>(
buttonsChanged);
if (!hotReload) return;
OnRoundStart(null!, null!);
onButtonsChanged);
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnRoundStart(EventRoundStart _, GameEventInfo _1) {
var entities =
Utilities.GetAllEntities().Where(ent => ent.IsValid).ToList();
foreach (var propMultiplayer in from ent in entities
where ent.DesignerName.Equals("prop_physics_multiplayer")
select new CPhysicsPropMultiplayer(ent.Handle))
MapEntities.Add(propMultiplayer);
foreach (var propMultiplayer in from ent in entities
where ent.DesignerName.Equals("prop_ragdoll")
select new CRagdollProp(ent.Handle))
MapEntities.Add(propMultiplayer);
return HookResult.Continue;
}
private void buttonsChanged(CCSPlayerController player, PlayerButtons pressed,
PlayerButtons released) {
if (playersPressingE.TryGetValue(player, out var e)) {
if (!released.HasFlag(PlayerButtons.Use)) return;
playersPressingE.Remove(player);
if (!e.Ragdoll.IsValid) return;
e.Ragdoll.AcceptInput("EnableMotion");
if (e.Beam != null && e.Beam.IsValid) e.Beam.AcceptInput("Kill");
private void onButtonsChanged(CCSPlayerController player,
PlayerButtons pressed, PlayerButtons released) {
if (playersPressingE.TryGetValue(player, out var heldItem)) {
onCeaseUse(player, released, heldItem);
return;
}
var playerPos = player.PlayerPawn.Value?.AbsOrigin;
if (playerPos == null) return;
if (!pressed.HasFlag(PlayerButtons.Use)) return;
var target = RayTrace.FindRayTraceIntersection(player);
onStartUse(player);
}
private void onStartUse(CCSPlayerController player) {
var playerPos = player.PlayerPawn.Value?.AbsOrigin;
if (playerPos == null) return;
var target = player.GetGameTraceByEyePosition(TraceMask.MaskSolid,
Contents.NoDraw, player);
if (target == null) return;
msg.DebugInform(target.ToString());
target.Value.HitEntityByDesignerName(out CBaseEntity? hitEntity,
"prop_ragdoll");
CBaseEntity? foundEntity = null;
MapEntities.RemoveWhere(ent => !ent.IsValid);
var closestDist = double.MaxValue;
foreach (var ent in MapEntities) {
if (!ent.IsValid) continue;
var rayPointDist =
ent.AbsOrigin?.DistanceSquared(target) ?? double.MaxValue;
msg.Debug($"Checking entity {ent.DesignerName} at {ent.AbsOrigin}, "
+ $"distance squared: {rayPointDist}");
if (rayPointDist >= MIN_LOOK_ACCURACY || rayPointDist >= closestDist)
continue;
if (hitEntity == null || !hitEntity.IsValid)
target.Value.HitEntityByDesignerName(out hitEntity,
"prop_physics_multiplayer");
closestDist = rayPointDist;
foundEntity = ent;
}
var playerDist = playerPos.Distance(target);
msg.Debug($"Player distance squared to target: {playerDist}");
var playerDist = target.Value.Distance();
if (playerDist > MAX_DISTANCE) return;
if (foundEntity == null) return;
if (hitEntity == null) return;
var apiPlayer = converter.GetPlayer(player);
var pickupEvent = new PropPickupEvent(apiPlayer, foundEntity);
var pickupEvent = new PropPickupEvent(apiPlayer, hitEntity);
bus.Dispatch(pickupEvent);
if (pickupEvent.IsCanceled) return;
@@ -124,40 +85,76 @@ public class PropMover(IServiceProvider provider) : IPluginModule {
pickupEvent.Prop.AcceptInput("DisableMotion");
}
private void refreshBodies() {
foreach (var (player, info) in playersPressingE)
refreshBodies(player, info);
private void onCeaseUse(CCSPlayerController player, PlayerButtons released,
MovementInfo heldItem) {
if (!released.HasFlag(PlayerButtons.Use)) return;
playersPressingE.Remove(player);
if (!heldItem.Ragdoll.IsValid) return;
heldItem.Ragdoll.AcceptInput("EnableMotion");
if (heldItem.Beam != null && heldItem.Beam.IsValid)
heldItem.Beam.AcceptInput("Kill");
}
private void refreshBodies(CCSPlayerController player, MovementInfo info) {
private void refreshHeld() {
foreach (var (player, info) in playersPressingE) refreshHeld(player, info);
}
private void refreshHeld(CCSPlayerController player, MovementInfo info) {
var ent = info.Ragdoll;
if (!player.IsValid || !ent.IsValid) {
if (!player.IsValid || !ent.IsValid || ent.AbsOrigin == null) {
playersPressingE.Remove(player);
return;
}
var playerPawn = player.PlayerPawn.Value;
if (playerPawn == null || !playerPawn.IsValid) return;
var playerOrigin = playerPawn.AbsOrigin;
var playerOrigin = player.GetEyePosition();
if (playerOrigin == null) {
playersPressingE.Remove(player);
return;
}
playerOrigin = playerOrigin.Clone()!;
playerOrigin.Z += 64;
var raytrace = player.GetGameTraceByEyePosition(TraceMask.MaskSolid,
Contents.NoDraw, player);
var eyeAngles = playerPawn!.EyeAngles;
if (raytrace == null) return;
var targetVector = playerOrigin + eyeAngles.Clone()!.ToForward()
* Math.Clamp(info.Distance, MIN_HOLDING_DISTANCE, MAX_HOLDING_DISTANCE);
var isOnSelf =
raytrace.Value.HitEntityByDesignerName(out CBaseEntity? _,
ent.DesignerName);
targetVector.Z = Math.Max(targetVector.Z, playerOrigin.Z - 48);
var endPos = raytrace.Value.EndPos.toVector();
if (ent.AbsOrigin == null) return;
var lerpedVector = ent.AbsOrigin.Lerp(targetVector, 0.3f);
if (isOnSelf || raytrace.Value.Distance() > MAX_HOLDING_DISTANCE)
endPos = playerOrigin
+ playerPawn.EyeAngles.ToForward() * MAX_HOLDING_DISTANCE;
ent.Teleport(lerpedVector, QAngle.Zero, Vector.Zero);
if (ent.DesignerName == "prop_physics_multiplayer") {
ent.Teleport(endPos, QAngle.Zero, Vector.Zero);
return;
}
moveBody(endPos, ent);
}
private void moveBody(Vector endPos, CBaseEntity ent) {
var deadRot = DEAD_ANGLE.Clone()!;
var rotDeg = Server.CurrentTime * 64f % 360;
var rotRad = (rotDeg + 0) * (MathF.PI / 180);
deadRot.Y += rotDeg;
var xOff = MathF.Cos(rotRad) * 32;
var yOff = MathF.Sin(rotRad) * 32;
var xBias = MathF.Cos(rotRad + MathF.PI / 2) * 16;
var yBias = MathF.Sin(rotRad + MathF.PI / 2) * 16;
endPos.X += xBias;
endPos.Y += yBias;
endPos -= new Vector(xOff, yOff, 0);
ent.Teleport(endPos, deadRot, Vector.Zero);
}
private void refreshLines() {
@@ -182,7 +179,7 @@ public class PropMover(IServiceProvider provider) : IPluginModule {
playerOrigin = playerOrigin.Clone()!;
playerOrigin.Z += 64;
var eyeAngles = playerPawn!.EyeAngles;
var eyeAngles = playerPawn.EyeAngles;
var targetVector = playerOrigin + eyeAngles.Clone()!.ToForward()
* Math.Clamp(info.Distance, MIN_HOLDING_DISTANCE, MAX_HOLDING_DISTANCE);

View File

@@ -1,22 +1,36 @@
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;
using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.CS2.Extensions;
using TTT.CS2.Hats;
using TTT.CS2.Roles;
using TTT.Game.Events.Game;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
using TTT.Game.Roles;
namespace TTT.CS2.GameHandlers;
public class RoleIconsHandler(IServiceProvider provider)
: IPluginModule, IListener {
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
: 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>> icons =
// new Dictionary<int, IEnumerable<CPointWorldText>>();
private readonly IEnumerable<CPointWorldText>?[] icons =
new IEnumerable<CPointWorldText>[64];
private readonly IPlayerConverter<CCSPlayerController> players =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
@@ -24,52 +38,80 @@ 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 Dispose() { bus.UnregisterListener(this); }
public ulong GetVisiblePlayers(int client) {
if (client < 1 || client >= visibilities.Length)
throw new ArgumentOutOfRangeException(nameof(client));
return visibilities[client];
}
public string Name => nameof(RoleIconsHandler);
public string Version => GitVersionInformation.FullSemVer;
public void SetVisiblePlayers(int client, ulong playersBitmask) {
guardRange(client, nameof(client));
visibilities[client] = playersBitmask;
}
public void Start() { }
public void RevealToAll(int client) {
guardRange(client, nameof(client));
for (var i = 0; i < visibilities.Length; i++)
visibilities[i] |= 1UL << client;
}
public void Start(BasePlugin? plugin) {
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);
bus.RegisterListener(this);
}
[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();
for (var i = 0; i < icons.Length; i++) removeIcon(i);
ClearAllVisibility();
traitorsThisRound.Clear();
}
[UsedImplicitly]
[EventHandler(IgnoreCanceled = true)]
public void OnAssigned(PlayerRoleAssignEvent ev) {
var player = players.GetPlayer(ev.Player);
if (player == null || !player.IsValid) return;
if (player.Team == CsTeam.Spectator) {
ev.Role = new SpectatorRole(provider);
ev.Role = new SpectatorRole(Provider);
return;
}
traitorIcons.TryGetValue(player.Slot, out var icons);
if (icons != null) {
foreach (var icon in icons) {
if (!icon.IsValid) continue;
icon.Remove();
}
}
traitors.Remove(player.Slot);
// Remove in case we're re-assigning for some reason
removeIcon(player.Slot);
player.SwitchTeam(ev.Role is DetectiveRole ?
CsTeam.CounterTerrorist :
@@ -79,21 +121,46 @@ public class RoleIconsHandler(IServiceProvider provider)
var pawn = player.Pawn.Value;
if (pawn == null || !pawn.IsValid) return;
pawn.SetModel(ev.Role is DetectiveRole ?
"characters/models/ctm_fbi/ctm_fbi_varianth.vmdl" :
"characters/models/tm_phoenix/tm_phoenix.vmdl");
pawn.SetModel(ev.Role is DetectiveRole ? CT_MODEL : T_MODEL);
assignIcon(player, ev.Role);
if (ev.Role is InnocentRole) return;
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) {
var textSettings = new TextSetting {
msg = ev.Role.Name.First(char.IsAsciiLetter) + "", color = ev.Role.Color
msg = role.Name.First(char.IsAsciiLetter).ToString(), color = role.Color
};
var roleIcon = textSpawner?.CreateTextHat(textSettings, player);
if (roleIcon == null) return;
if (ev.Role is not TraitorRole) return;
traitors.Add(player.Slot);
traitorIcons[player.Slot] = roleIcon;
icons[player.Slot] = roleIcon;
}
[EventHandler(Priority = Priority.MONITOR)]
@@ -101,26 +168,29 @@ public class RoleIconsHandler(IServiceProvider provider)
var gamePlayer = players.GetPlayer(ev.Victim);
if (gamePlayer == null || !gamePlayer.IsValid) return;
if (!traitors.Contains(gamePlayer.Slot)) return;
traitorIcons.TryGetValue(gamePlayer.Slot, out var icons);
if (icons != null) {
foreach (var icon in icons) {
if (!icon.IsValid) continue;
icon.Remove();
}
}
traitors.Remove(gamePlayer.Slot);
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

@@ -1,5 +1,6 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Game;
@@ -7,24 +8,24 @@ using TTT.Game.Roles;
namespace TTT.CS2.GameHandlers;
public class RoundEndHandler(IServiceProvider provider) : IPluginModule {
public class RoundEnd_GameEndHandler(IServiceProvider provider)
: IPluginModule {
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
public void Dispose() { }
public string Name => nameof(RoundEndHandler);
public string Version => GitVersionInformation.FullSemVer;
public void Start() { }
[UsedImplicitly]
[GameEventHandler]
public HookResult OnRoundEnd(EventRoundEnd _, GameEventInfo _1) {
if (!games.IsGameActive()) return HookResult.Continue;
if (games.ActiveGame is not { State: State.IN_PROGRESS })
return HookResult.Continue;
var game = games.ActiveGame ?? throw new InvalidOperationException(
"Active game is null, but round end event was triggered.");
if (game.FinishedAt != null)
// The game's round ended due to our TTT game ending
// We caused this round to end already, don't end it again
return HookResult.Continue;
game.EndGame(EndReason.TIMEOUT(new InnocentRole(provider)));

View File

@@ -1,30 +1,39 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game;
namespace TTT.CS2.GameHandlers;
public class RoundStartHandler(IServiceProvider provider) : IPluginModule {
public class RoundStart_GameStartHandler(IServiceProvider provider)
: IPluginModule {
private readonly TTTConfig config =
provider.GetService<IStorage<TTTConfig>>()?.Load().GetAwaiter().GetResult()
?? new TTTConfig();
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
public void Dispose() { throw new NotImplementedException(); }
public string Name => nameof(RoundStartHandler);
public string Version => GitVersionInformation.FullSemVer;
public void Dispose() { }
public void Start() { }
[UsedImplicitly]
[GameEventHandler]
public HookResult OnRoundStart(EventRoundStart _, GameEventInfo _1) {
if (games.IsGameActive()) return HookResult.Continue;
if (games.ActiveGame is { State: State.IN_PROGRESS or State.COUNTDOWN })
return HookResult.Continue;
var count = finder.GetOnline().Count;
if (count < config.RoundCfg.MinimumPlayers) return HookResult.Continue;
var game = games.CreateGame();
game?.Start(config.RoundCfg.CountDownDuration);

View File

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

View File

@@ -2,6 +2,7 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using TTT.CS2.Extensions;
using TTT.CS2.RayTrace.Class;
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
namespace TTT.CS2.Hats;
@@ -36,29 +37,45 @@ public class TextSpawner : ITextSpawner {
return [one, two];
}
public IEnumerable<CPointWorldText> CreateTextScreen(TextSetting setting,
CCSPlayerController player) {
var screen = spawnScreen(setting, player);
return [screen];
}
public CPointWorldText spawnScreen(TextSetting setting,
CCSPlayerController player, float xOff = 0, float yOff = 0,
float zDist = 50) {
if (player.Pawn.Value == null || player.Pawn.Value.AbsRotation == null)
throw new Exception("Failed to get player rotation");
var eyes = player.GetEyePosition().Clone()!;
var localAngle = player.Pawn.Value.AbsRotation.Clone()!;
var forward = localAngle.Clone()!.ToForward();
var right = localAngle.ToRight();
var up = localAngle.ToUp();
var inFront = eyes + forward * zDist;
var centered = inFront + right * xOff + up * yOff;
var ent = CreateText(setting, centered,
new QAngle(localAngle.X + 180, localAngle.Y + 90, localAngle.Z + 270));
ent.AcceptInput("SetParent", player.Pawn.Value, null, "!activator");
return ent;
}
private CPointWorldText spawnHatPart(TextSetting setting,
CCSPlayerController player, float yRot) {
var position = player.PlayerPawn.Value?.AbsOrigin;
var rotation = player.PlayerPawn.Value?.AbsRotation;
var position = player.Pawn.Value?.AbsOrigin;
var rotation = player.Pawn.Value?.AbsRotation;
if (position == null || rotation == null)
throw new Exception("Failed to get player position");
position = position.Clone()!;
position.Add(new Vector(0, 0, 72));
rotation = new QAngle(rotation.X, rotation.Y + yRot, rotation.Z + 90);
position.Add(GetRightVector(rotation) * -10);
position.Add(rotation.ToRight() * -10);
var ent = CreateText(setting, position, rotation);
ent.AcceptInput("SetParent", player.PlayerPawn.Value, null, "!activator");
ent.AcceptInput("SetParent", player.Pawn.Value, null, "!activator");
return ent;
}
public static Vector GetRightVector(QAngle rotation) {
var forward = new Vector {
X = (float)Math.Cos(rotation.Y * Math.PI / 180),
Y = (float)Math.Sin(rotation.Y * Math.PI / 180),
Z = 0
};
return forward;
}
}

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,42 @@
using System.Drawing;
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) {
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

@@ -1,89 +1,67 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.API.Role;
using TTT.CS2.API;
using TTT.CS2.Events;
using TTT.CS2.Extensions;
using TTT.Game;
using TTT.Game.Events.Body;
using TTT.Game.Events.Game;
using TTT.Locale;
using TTT.Game.lang;
using TTT.Game.Listeners;
using TTT.Game.Roles;
namespace TTT.CS2.Listeners;
public class BodyPickupListener(IServiceProvider provider) : IListener {
private readonly Dictionary<CBaseEntity, IBody> bodyCache = new();
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
public class BodyPickupListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly IBodyTracker bodies =
provider.GetRequiredService<IBodyTracker>();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IMsgLocalizer locale =
provider.GetRequiredService<IMsgLocalizer>();
private readonly IMessenger msg = provider.GetRequiredService<IMessenger>();
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
public void Dispose() { bus.UnregisterListener(this); }
private readonly IAliveSpoofer? spoofer =
provider.GetService<IAliveSpoofer>();
[EventHandler]
public void OnGameState(GameStateUpdateEvent ev) {
if (ev.NewState != State.IN_PROGRESS) return;
bodyCache.Clear();
}
[EventHandler]
public void OnBodyCreate(BodyCreateEvent ev) {
if (!int.TryParse(ev.Body.Id, out var index))
throw new ArgumentException(
$"Body ID '{ev.Body.Id}' is not a valid entity index.");
var entity = Utilities.GetEntityFromIndex<CRagdollProp>(index);
if (entity == null || !entity.IsValid)
throw new InvalidOperationException(
$"Could not find valid entity for body ID '{ev.Body.Id}'.");
bodyCache[entity] = ev.Body;
}
[EventHandler(Priority = Priority.HIGH)]
public void OnPropPickup(PropPickupEvent ev) {
var prop = ev.Prop as CRagdollProp;
if (prop == null || !prop.IsValid) return;
if (!bodyCache.TryGetValue(prop, out var body)) return;
if (body.IsIdentified) return;
if (!bodies.TryLookup(ev.Prop.Index.ToString(), out var body)) return;
if (body == null || body.IsIdentified) return;
if (ev.Player is not IOnlinePlayer online)
throw new InvalidOperationException("Player is not an online player.");
var identifyEvent = new BodyIdentifyEvent(body, online);
bus.Dispatch(identifyEvent);
Bus.Dispatch(identifyEvent);
if (identifyEvent.IsCanceled) return;
}
body.IsIdentified = true;
var role = roles.GetRoles(body.OfPlayer);
[EventHandler]
public void OnIdentify(BodyIdentifyEvent ev) {
ev.Body.IsIdentified = true;
var role = Roles.GetRoles(ev.Body.OfPlayer);
if (role.Count == 0) return;
var primaryRole = role.First();
prop.SetColor(primaryRole.Color);
msg.MessageAll(
locale[GameMsgs.BODY_IDENTIFIED(online, body.OfPlayer, primaryRole)]);
var primary = role.First();
var onlinePlayer = converter.GetPlayer(body.OfPlayer);
if (onlinePlayer == null || !onlinePlayer.IsValid) return;
Messenger.MessageAll(Locale[
GameMsgs.BODY_IDENTIFIED(ev.Identifier, ev.Body.OfPlayer, primary)]);
onlinePlayer.PawnIsAlive = false;
onlinePlayer.SetClan(primaryRole.Name);
Utilities.SetStateChanged(onlinePlayer, "CCSPlayerController",
"m_bPawnIsAlive");
if (!bodies.Bodies.TryGetValue(ev.Body, out var ragdoll)) return;
if (ragdoll.IsValid) ragdoll.SetColor(primary.Color);
var online = converter.GetPlayer(ev.Body.OfPlayer);
if (online is not { IsValid: true }) return;
if (primary is InnocentRole) online.SwitchTeam(CsTeam.CounterTerrorist);
spoofer?.UnspoofAlive(online);
online.PawnIsAlive = false;
online.SetClan(primary.Name);
Utilities.SetStateChanged(online, "CCSPlayerController", "m_bPawnIsAlive");
}
}

View File

@@ -0,0 +1,38 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using TTT.API.Events;
using TTT.API.Game;
using TTT.CS2.API;
using TTT.Game;
using TTT.Game.Events.Body;
using TTT.Game.Events.Game;
using TTT.Game.Listeners;
namespace TTT.CS2.Listeners;
public class BodyTracker(IServiceProvider provider)
: BaseListener(provider), IBodyTracker {
private readonly Dictionary<IBody, CRagdollProp> bodyCache = new();
public IDictionary<IBody, CRagdollProp> Bodies => bodyCache;
[EventHandler]
public void OnGameState(GameStateUpdateEvent ev) {
if (ev.NewState != State.IN_PROGRESS) return;
bodyCache.Clear();
}
[EventHandler]
public void OnBodyCreate(BodyCreateEvent ev) {
if (!int.TryParse(ev.Body.Id, out var index))
throw new ArgumentException(
$"Body ID '{ev.Body.Id}' is not a valid entity index.");
var entity = Utilities.GetEntityFromIndex<CRagdollProp>(index);
if (entity == null || !entity.IsValid)
throw new InvalidOperationException(
$"Could not find valid entity for body ID '{ev.Body.Id}'.");
bodyCache[ev.Body] = entity;
}
}

View File

@@ -0,0 +1,27 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
namespace TTT.CS2.Listeners;
public class LateSpawnListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
[EventHandler]
public void OnJoin(PlayerJoinEvent ev) {
if (Games.ActiveGame is { State: State.IN_PROGRESS }) return;
Server.NextWorldUpdate(() => {
var player = converter.GetPlayer(ev.Player);
if (player == null || !player.IsValid) return;
player.Respawn();
});
}
}

View File

@@ -0,0 +1,116 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.Game.Events.Body;
using TTT.Game.Events.Game;
using TTT.Game.Events.Player;
namespace TTT.CS2.Listeners;
public class PlayerStatsTracker(IServiceProvider provider) : IListener {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
private readonly ISet<int> revealedDeaths = new HashSet<int>();
private readonly IDictionary<int, (int, int)> roundKillsAndAssists =
new Dictionary<int, (int, int)>();
public void Dispose() { }
[EventHandler(Priority = Priority.MONITOR)]
public void OnIdentify(BodyIdentifyEvent ev) {
var gamePlayer = converter.GetPlayer(ev.Body.OfPlayer);
if (gamePlayer == null || !gamePlayer.IsValid) return;
var stats = gamePlayer.ActionTrackingServices?.MatchStats;
if (stats == null) return;
stats.Deaths++;
Utilities.SetStateChanged(gamePlayer, "CCSPlayerController",
"m_pActionTrackingServices");
revealedDeaths.Add(gamePlayer.Slot);
}
// Needs to be higher so we detect the kill before the game ends
// in the case that this is the last player
[EventHandler(Priority = Priority.HIGH)]
public void OnKill(PlayerDeathEvent ev) {
var killer = ev.Killer == null ? null : converter.GetPlayer(ev.Killer);
var assister =
ev.Assister == null ? null : converter.GetPlayer(ev.Assister);
if (killer != null) {
roundKillsAndAssists.TryGetValue(killer.Slot, out var def);
def.Item1++;
roundKillsAndAssists[killer.Slot] = def;
}
if (assister != null && assister != killer) {
roundKillsAndAssists.TryGetValue(assister.Slot, out var def);
def.Item2++;
roundKillsAndAssists[assister.Slot] = def;
}
}
[EventHandler]
public void OnRoundEnd(GameStateUpdateEvent ev) {
if (ev.NewState == State.IN_PROGRESS) {
revealedDeaths.Clear();
roundKillsAndAssists.Clear();
return;
}
if (ev.NewState != State.FINISHED) return;
revealDeaths();
revealKills();
}
private void revealDeaths() {
var online = finder.GetOnline()
.Where(p => !p.IsAlive)
.Select(p => converter.GetPlayer(p))
.OfType<CCSPlayerController>()
.Where(p => p.IsValid && !revealedDeaths.Contains(p.Slot));
foreach (var player in online) {
player.PawnIsAlive = false;
Utilities.SetStateChanged(player, "CCSPlayerController",
"m_bPawnIsAlive");
var stats = player.ActionTrackingServices?.MatchStats;
if (stats == null) continue;
stats.Deaths++;
Utilities.SetStateChanged(player, "CCSPlayerController",
"m_pActionTrackingServices");
}
}
private void revealKills() {
var online = finder.GetOnline()
.Select(p => converter.GetPlayer(p))
.OfType<CCSPlayerController>()
.Where(p => p.IsValid && roundKillsAndAssists.ContainsKey(p.Slot));
foreach (var player in online) {
var stats = player.ActionTrackingServices?.MatchStats;
if (stats == null) continue;
var (kills, assists) = roundKillsAndAssists[player.Slot];
stats.Kills += kills;
stats.Assists += assists;
Utilities.SetStateChanged(player, "CCSPlayerController",
"m_pActionTrackingServices");
}
}
}

View File

@@ -1,4 +1,6 @@
using CounterStrikeSharp.API;
using System.Drawing;
using System.Reactive.Linq;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Utils;
@@ -7,22 +9,18 @@ using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
using TTT.CS2.Extensions;
using TTT.CS2.Utils;
using TTT.Game;
using TTT.Game.Events.Game;
using TTT.Game.Listeners;
using TTT.Game.Roles;
namespace TTT.CS2.Listeners;
public class RoundTimerListener(IServiceProvider provider) : IListener {
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
public class RoundTimerListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly TTTConfig config = provider
.GetRequiredService<IStorage<TTTConfig>>()
.Load()
@@ -32,8 +30,6 @@ public class RoundTimerListener(IServiceProvider provider) : IListener {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public void Dispose() { bus.UnregisterListener(this); }
[UsedImplicitly]
[EventHandler(IgnoreCanceled = true)]
public void OnRoundStart(GameStateUpdateEvent ev) {
@@ -45,6 +41,9 @@ public class RoundTimerListener(IServiceProvider provider) : IListener {
foreach (var player in Utilities.GetPlayers()
.Where(p => p.LifeState != (int)LifeState_t.LIFE_ALIVE))
player.Respawn();
foreach (var player in Utilities.GetPlayers())
player.SetColor(Color.White);
});
return;
@@ -63,29 +62,57 @@ public class RoundTimerListener(IServiceProvider provider) : IListener {
[EventHandler(IgnoreCanceled = true)]
public void OnRoundEnd(GameStateUpdateEvent ev) {
if (ev.NewState != State.FINISHED) return;
revealRoles(ev.Game);
// If CS caused the round to end, we will have 0 time left
// in this case, CS automatically handles the end of round stuff
// so we don't need to do anything
if (RoundUtil.GetTimeRemaining() <= 1) return;
foreach (var player in ev.Game.Players) {
var csPlayer = converter.GetPlayer(player);
if (csPlayer == null || !csPlayer.IsValid) continue;
var role = roles.GetRoles(player).FirstOrDefault();
if (role == null) continue;
csPlayer.SetClan(role.Name, false);
}
Server.NextWorldUpdate(() => {
var endReason = endRound(ev);
foreach (var inno in ev.Game.GetAlive(typeof(InnocentRole))) {
var player = converter.GetPlayer(inno);
player?.SwitchTeam(CsTeam.CounterTerrorist);
}
if (ev.Game.WinningRole != null)
RoundUtil.AddTeamScore(
endReason == RoundEndReason.CTsWin ?
CsTeam.CounterTerrorist :
CsTeam.Terrorist, 1);
new EventNextlevelChanged(true).FireEvent(false);
var timer = Observable.Timer(
config.RoundCfg.TimeBetweenRounds, Scheduler);
timer.Subscribe(_
=> Server.NextWorldUpdate(() => RoundUtil.EndRound(endReason)));
});
}
private RoundEndReason endRound(GameStateUpdateEvent ev) {
var endReason =
ev.Game.WinningRole != null && ev.Game.WinningRole.GetType()
.IsAssignableTo(typeof(TraitorRole)) ?
RoundEndReason.TerroristsWin :
RoundEndReason.CTsWin;
RoundUtil.EndRound(endReason);
var panelWinEvent = new EventCsWinPanelRound(true);
var winningTeam = endReason == RoundEndReason.TerroristsWin ?
CsTeam.Terrorist :
CsTeam.CounterTerrorist;
panelWinEvent.Set("final_event",
winningTeam == CsTeam.CounterTerrorist ? 2 : 3);
panelWinEvent.FireEvent(false);
return endReason;
}
private void revealRoles(IGame game) {
foreach (var player in game.Players) {
var csPlayer = converter.GetPlayer(player);
if (csPlayer == null || !csPlayer.IsValid) continue;
var role = Roles.GetRoles(player).FirstOrDefault();
if (role == null) continue;
csPlayer.SetClan(role.Name, false);
if (role is InnocentRole) csPlayer.SwitchTeam(CsTeam.CounterTerrorist);
}
new EventNextlevelChanged(true).FireEvent(false);
}
}

View File

@@ -0,0 +1,27 @@
using System.Drawing;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Player;
using TTT.CS2.Extensions;
using TTT.CS2.Roles;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
namespace TTT.CS2.Listeners;
public class ScreenColorApplier(IServiceProvider provider)
: BaseListener(provider) {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
[EventHandler]
public void OnRoleAssign(PlayerRoleAssignEvent ev) {
if (ev.Role is SpectatorRole) return;
var player = converter.GetPlayer(ev.Player);
var alphaColor = Color.FromArgb(64, ev.Role.Color);
if (player != null)
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,18 +29,23 @@ 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;
}
public void Dispose() { playerCache.Clear(); }
public string Name => "PlayerConverter";
public string Version => GitVersionInformation.FullSemVer;
public void Start() { }
}

View File

@@ -0,0 +1,63 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using TTT.API;
using TTT.CS2.API;
namespace TTT.CS2.Player;
public class CS2AliveSpoofer : IAliveSpoofer, IPluginModule {
private readonly HashSet<CCSPlayerController> _fakeAlivePlayers = new();
public ISet<CCSPlayerController> FakeAlivePlayers => _fakeAlivePlayers;
public void SpoofAlive(CCSPlayerController player) {
if (player.IsBot) {
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);
}
public void UnspoofAlive(CCSPlayerController player) {
if (player.IsBot) {
if (player.Pawn.Value != null && player.Pawn.Value.Health > 0) return;
player.PawnIsAlive = false;
Utilities.SetStateChanged(player, "CCSPlayerController",
"m_bPawnIsAlive");
return;
}
FakeAlivePlayers.Remove(player);
}
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
plugin?.RegisterListener<CounterStrikeSharp.API.Core.Listeners.OnTick>(
onTick);
}
private void onTick() {
_fakeAlivePlayers.RemoveWhere(p => !p.IsValid);
foreach (var player in _fakeAlivePlayers) {
player.PawnIsAlive = true;
Utilities.SetStateChanged(player, "CCSPlayerController",
"m_bPawnIsAlive");
}
}
}

View File

@@ -1,4 +1,6 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using TTT.API;
using TTT.API.Player;
using TTT.CS2.Extensions;
@@ -7,49 +9,126 @@ namespace TTT.CS2.Player;
public class CS2InventoryManager(
IPlayerConverter<CCSPlayerController> converter) : IInventoryManager {
public void GiveWeapon(IOnlinePlayer player, IWeapon weapon) {
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
public Task GiveWeapon(IOnlinePlayer player, IWeapon weapon) {
return Server.NextWorldUpdateAsync(() => {
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
giveWeapon(gamePlayer, weapon);
});
}
public Task RemoveWeapon(IOnlinePlayer player, string weaponId) {
return Server.NextWorldUpdateAsync(() => {
if (!player.IsAlive) return;
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
var pawn = gamePlayer.Pawn.Value;
if (pawn == null || pawn.WeaponServices == null) return;
var matchedWeapon =
pawn.WeaponServices.MyWeapons.FirstOrDefault(x
=> x.Value?.DesignerName == weaponId);
if (matchedWeapon?.Value == null || !matchedWeapon.IsValid) return;
pawn.WeaponServices.ActiveWeapon.Raw = matchedWeapon.Raw;
// Make them equip the desired weapon
var activeWeaponEntity =
pawn.WeaponServices.ActiveWeapon.Value?.As<CBaseEntity>();
gamePlayer.DropActiveWeapon();
activeWeaponEntity?.AddEntityIOEvent("Kill", activeWeaponEntity);
});
}
public Task RemoveWeaponInSlot(IOnlinePlayer player, int slot) {
return Server.NextWorldUpdateAsync(() => {
if (!player.IsAlive) return;
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
clearSlot(gamePlayer, IntToSlot(slot));
});
}
public Task RemoveAllWeapons(IOnlinePlayer player) {
return Server.NextWorldUpdateAsync(() => {
if (!player.IsAlive) return;
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
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");
gamePlayer.GiveNamedItem(weapon.Id);
if (weapon.ReserveAmmo == null && weapon.CurrentAmmo == null) return;
var weaponBase = gamePlayer.GetWeaponBase(weapon.Id);
if (weaponBase == null) return;
if (weapon.CurrentAmmo != null) weaponBase.Clip1 = weapon.CurrentAmmo.Value;
if (weapon.ReserveAmmo != null) weaponBase.Clip2 = weapon.ReserveAmmo.Value;
if (weapon.ReserveAmmo != null)
weaponBase.ReserveAmmo[0] = weapon.ReserveAmmo.Value;
Utilities.SetStateChanged(weaponBase, "CBasePlayerWeapon", "m_iClip1");
Utilities.SetStateChanged(weaponBase, "CBasePlayerWeapon",
"m_pReserveAmmo");
}
public void RemoveWeapon(IOnlinePlayer player, string weaponId) {
if (!player.IsAlive) return;
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
var pawn = gamePlayer.PlayerPawn.Value;
if (pawn == null || pawn.WeaponServices == null) return;
var matchedWeapon =
pawn.WeaponServices.MyWeapons.FirstOrDefault(x
=> x.Value?.DesignerName == weaponId);
if (matchedWeapon?.Value == null || !matchedWeapon.IsValid) return;
pawn.WeaponServices.ActiveWeapon.Raw = matchedWeapon.Raw;
// Make them equip the desired weapon
var activeWeaponEntity =
pawn.WeaponServices.ActiveWeapon.Value?.As<CBaseEntity>();
gamePlayer.DropActiveWeapon();
activeWeaponEntity?.AddEntityIOEvent("Kill", activeWeaponEntity);
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 void RemoveAllWeapons(IOnlinePlayer player) {
if (!player.IsAlive) return;
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
};
}
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
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;
gamePlayer.RemoveWeapons();
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

@@ -11,6 +11,8 @@ namespace TTT.CS2.Player;
/// Note that slot numbers are not guaranteed to be stable across server restarts.
/// </summary>
public class CS2Player : IOnlinePlayer {
private CCSPlayerController? cachePlayer;
protected CS2Player(string id, string name) {
Id = id;
Name = name;
@@ -33,28 +35,41 @@ public class CS2Player : IOnlinePlayer {
private CCSPlayerController? Player {
get {
if (cachePlayer != null && cachePlayer.IsValid) return cachePlayer;
var player = Utilities.GetPlayerFromSteamId(ulong.Parse(Id))
?? Utilities.GetPlayerFromIndex(int.Parse(Id));
#if DEBUG
if (player == null || !player.IsValid)
Console.WriteLine("Failed to find player with ID: " + Id);
#endif
if (player != null && player.IsValid) cachePlayer = player;
return player is { IsValid: true } ? player : null;
}
}
private int namePadding
=> Math.Min(Utilities.GetPlayers().Select(p => p.PlayerName.Length).Max(),
24);
public string Id { get; }
public string Name { get; }
// public ICollection<IRole> Roles { get; } = [];
public int Health {
get => Player?.Pawn.Value != null ? Player.Pawn.Value.Health : 0;
set {
if (Player?.Pawn.Value == null) return;
Player.Pawn.Value.Health = value;
Utilities.SetStateChanged(Player.Pawn.Value, "CBaseEntity", "m_iHealth");
Server.NextWorldUpdate(() => {
if (Player?.Pawn.Value == null) return;
if (value <= 0) {
Player.CommitSuicide(false, true);
return;
}
Player.Pawn.Value.Health = value;
Utilities.SetStateChanged(Player.Pawn.Value, "CBaseEntity",
"m_iHealth");
});
}
}
@@ -70,21 +85,17 @@ public class CS2Player : IOnlinePlayer {
}
public int Armor {
get
=> Player?.PlayerPawn.Value != null ?
Player.PlayerPawn.Value.ArmorValue :
0;
get => Player?.PawnArmor ?? 0;
set {
if (Player?.PlayerPawn.Value == null) return;
Player.PlayerPawn.Value.ArmorValue = value;
Utilities.SetStateChanged(Player.PlayerPawn.Value, "CCSPlayerPawn",
"m_ArmorValue");
if (Player == null) return;
Player.PawnArmor = value;
Utilities.SetStateChanged(Player, "CCSPlayerController", "m_iPawnArmor");
}
}
public bool IsAlive {
get => Player != null && Player.PlayerPawn.Value is { Health: > 0 };
get => Player != null && Player.Pawn.Value is { Health: > 0 };
set
=> throw new NotSupportedException(
@@ -96,7 +107,18 @@ public class CS2Player : IOnlinePlayer {
return player.SteamID.ToString();
}
public override string ToString() { return $"({getSuffix(Id, 5)}) {Name}"; }
public override string ToString() { return createPaddedName(); }
// Goal: Pad the name to a fixed width for better alignment in logs
// Left-align ID, right-align name
private string createPaddedName() {
var idPart = $"({getSuffix(Id, 5)})";
var effectivePadding = namePadding - idPart.Length;
var namePart = Name.Length >= effectivePadding ?
getSuffix(Name, effectivePadding) :
Name.PadLeft(effectivePadding);
return $"{idPart} {namePart}";
}
private string getSuffix(string s, int len) {
return s.Length <= len ? s : s[^len..];

View File

@@ -0,0 +1,12 @@
namespace TTT.CS2.RayTrace.Class;
internal static class Address {
public static unsafe IntPtr GetAbsoluteAddress(IntPtr addr, IntPtr offset,
int size) {
if (addr == IntPtr.Zero)
throw new Exception("Failed to find RayTrace signature.");
var code = *(int*)(addr + offset);
return addr + code + size;
}
}

View File

@@ -0,0 +1,226 @@
using System.Numerics;
using CounterStrikeSharp.API.Core;
using TTT.CS2.RayTrace.Enum;
using TTT.CS2.RayTrace.Struct;
namespace TTT.CS2.RayTrace.Class;
/// <summary>
/// Provides extension methods for <see cref="CGameTrace" /> class
/// </summary>
public static class GameTraceExtensions {
/// <summary>
/// Determines if the trace hit anything.
/// </summary>
public static bool DidHit(this CGameTrace gameTrace) {
return gameTrace is { Fraction: < 1.0f, AllSolid: false };
}
/// <summary>
/// Gets the distance between the start and end positions of the trace.
/// </summary>
public static float Distance(this CGameTrace gametrace) {
return Vector3.Distance(gametrace.StartPos, gametrace.EndPos);
}
/// <summary>
/// Gets the normalized direction vector of the trace.
/// </summary>
public static Vector3 Direction(this CGameTrace gametrace) {
return Vector3.Normalize(gametrace.EndPos - gametrace.StartPos);
}
/// <summary>
/// Attempts to get the entity of type <typeparamref name="T" /> if the trace hit an entity
/// with a designer name matching the specified pattern.
/// </summary>
/// <typeparam name="T">The type of entity to check for.</typeparam>
/// <param name="gametrace">The gametrace</param>
/// <param name="entity">The entity that was hit, if any.</param>
/// <param name="designerName">The designer name pattern to match.</param>
/// <param name="matchType">The type of matching to perform.</param>
/// <returns>True if an entity of type <typeparamref name="T" /> matching the pattern was hit, false otherwise.</returns>
public static bool HitEntityByDesignerName<T>(this CGameTrace gametrace,
out T? entity, string designerName,
DesignerNameMatchType matchType = DesignerNameMatchType.Equals)
where T : CEntityInstance {
if ((T?)Activator.CreateInstance(typeof(T), gametrace.HitEntity)
is { } entityInstance) {
var isMatch = matchType switch {
DesignerNameMatchType.Equals => entityInstance.DesignerName
== designerName,
DesignerNameMatchType.StartsWith => entityInstance.DesignerName
.StartsWith(designerName, StringComparison.OrdinalIgnoreCase),
DesignerNameMatchType.EndsWith => entityInstance.DesignerName.EndsWith(
designerName, StringComparison.OrdinalIgnoreCase),
_ => false
};
if (isMatch) {
entity = entityInstance;
return true;
}
}
entity = null;
return false;
}
/// <summary>
/// Attempts to get the player controller if the trace hit a player.
/// </summary>
/// <param name="gametrace">The gametrace</param>
/// <param name="player">The player controller that was hit, if any.</param>
/// <returns>True if a player was hit, false otherwise.</returns>
public static bool HitPlayer(this CGameTrace gametrace,
out CCSPlayerController? player) {
if (gametrace.HitEntityByDesignerName(out CCSPlayerPawn? playerPawn,
"player")) {
player = playerPawn?.OriginalController.Value;
return player != null;
}
player = null;
return false;
}
/// <summary>
/// Attempts to get the weapon if the trace hit a weapon.
/// </summary>
/// <param name="gametrace">The gametrace</param>
/// <param name="weapon">The weapon that was hit, if any.</param>
/// <returns>True if a weapon was hit, false otherwise.</returns>
public static bool
HitWeapon(this CGameTrace gametrace, out CBasePlayerWeapon? weapon) {
return gametrace.HitEntityByDesignerName(out weapon, "weapon_",
DesignerNameMatchType.StartsWith);
}
/// <summary>
/// Attempts to get the chicken if the trace hit a chicken.
/// </summary>
/// <param name="gametrace">The gametrace</param>
/// <param name="chicken">The chicken that was hit, if any.</param>
/// <returns>True if a chicken was hit, false otherwise.</returns>
public static bool HitChicken(this CGameTrace gametrace,
out CChicken? chicken) {
return gametrace.HitEntityByDesignerName(out chicken, "chicken");
}
/// <summary>
/// Attempts to get the button if the trace hit a button.
/// </summary>
/// <param name="gametrace">The gametrace</param>
/// <param name="button">The button that was hit, if any.</param>
/// <returns>True if a button was hit, false otherwise.</returns>
public static bool HitButton(this CGameTrace gametrace,
out CBaseButton? button) {
return gametrace.HitEntityByDesignerName(out button, "func_door");
}
/// <summary>
/// Attempts to get the buyzone if the trace hit a buyzone.
/// </summary>
/// <param name="gametrace">The gametrace</param>
/// <param name="buyzone">The buyzone that was hit, if any.</param>
/// <returns>True if a buyzone was hit, false otherwise.</returns>
public static bool
HitBuyzone(this CGameTrace gametrace, out CBuyZone? buyzone) {
return gametrace.HitEntityByDesignerName(out buyzone, "func_buyzone");
}
/// <summary>
/// Attempts to get the sky if the trace hit the sky.
/// </summary>
/// <param name="gametrace">The gametrace</param>
/// <param name="sky">The sky that was hit, if any.</param>
/// <returns>True if the sky was hit, false otherwise.</returns>
public static bool HitSky(this CGameTrace gametrace, out CEnvSky? sky) {
return gametrace.HitEntityByDesignerName(out sky, "env_sky");
}
/// <summary>
/// Attempts to get the door if the trace hit a door.
/// </summary>
/// <param name="gametrace">The gametrace</param>
/// <param name="door">The door that was hit, if any.</param>
/// <returns>True if a door was hit, false otherwise.</returns>
public static bool HitDoor(this CGameTrace gametrace, out CBaseDoor? door) {
return gametrace.HitEntityByDesignerName(out door, "func_door");
}
/// <summary>
/// Attempts to get the rotating door if the trace hit a rotating door.
/// </summary>
/// <param name="gametrace">The gametrace</param>
/// <param name="door">The rotating door that was hit, if any.</param>
/// <returns>True if a rotating door was hit, false otherwise.</returns>
public static bool HitDoor(this CGameTrace gametrace, out CRotDoor? door) {
return gametrace.HitEntityByDesignerName(out door, "func_door_rotating");
}
/// <summary>
/// Attempts to get the ladder if the trace hit a ladder.
/// </summary>
/// <param name="gametrace">The gametrace</param>
/// <param name="ladder">The ladder that was hit, if any.</param>
/// <returns>True if a ladder was hit, false otherwise.</returns>
public static bool HitLadder(this CGameTrace gametrace,
out CFuncLadder? ladder) {
return gametrace.HitEntityByDesignerName(out ladder, "func_ladder");
}
/// <summary>
/// Attempts to get the grenade if the trace hit a grenade.
/// </summary>
/// <param name="gametrace">The gametrace</param>
/// <param name="grenade">The grenade that was hit, if any.</param>
/// <returns>True if a grenade was hit, false otherwise.</returns>
public static bool HitGrenade(this CGameTrace gametrace,
out CBaseCSGrenade? grenade) {
return gametrace.HitEntityByDesignerName(out grenade, "grenade");
}
/// <summary>
/// Attempts to get the planted C4 if the trace hit a planted C4.
/// </summary>
/// <param name="gametrace">The gametrace</param>
/// <param name="c4">The planted C4 that was hit, if any.</param>
/// <returns>True if a planted C4 was hit, false otherwise.</returns>
public static bool
HitPlantedC4(this CGameTrace gametrace, out CPlantedC4? c4) {
return gametrace.HitEntityByDesignerName(out c4, "planted_c4");
}
/// <summary>
/// Attempts to get the world text if the trace hit a point world text.
/// </summary>
/// <param name="gametrace">The gametrace</param>
/// <param name="pointWorldText">The point world text that was hit, if any.</param>
/// <returns>True if a point world text was hit, false otherwise.</returns>
public static bool HitPointWorldText(this CGameTrace gametrace,
out CPointWorldText? pointWorldText) {
return gametrace.HitEntityByDesignerName(out pointWorldText,
"point_worldtext");
}
/// <summary>
/// Attempts to get the C4 if the trace hit a C4 weapon.
/// </summary>
/// <param name="gametrace">The gametrace</param>
/// <param name="c4">The C4 weapon that was hit, if any.</param>
/// <returns>True if a C4 weapon was hit, false otherwise.</returns>
public static bool HitC4(this CGameTrace gametrace, out CC4? c4) {
return gametrace.HitEntityByDesignerName(out c4, "weapon_c4");
}
/// <summary>
/// Attempts to get the world entity if the trace hit the world.
/// </summary>
/// <param name="gametrace">The gametrace</param>
/// <param name="world">The world entity that was hit, if any.</param>
/// <returns>True if the world was hit, false otherwise.</returns>
public static bool HitWorld(this CGameTrace gametrace, out CWorld? world) {
return gametrace.HitEntityByDesignerName(out world, "worldent");
}
}

View File

@@ -0,0 +1,113 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using TTT.CS2.RayTrace.Enum;
using TTT.CS2.RayTrace.Struct;
namespace TTT.CS2.RayTrace.Class;
/// <summary>
/// Provides extension methods for <see cref="CCSPlayerController" /> and <see cref="CCSPlayerPawn" /> classes
/// to support various player-related operations including trace rays and position calculations.
/// </summary>
public static class PlayerExtensions {
/// <summary>
/// Performs a game trace from the player's eye position in the direction they are looking.
/// </summary>
/// <param name="player">The player controller to trace from.</param>
/// <param name="mask">The trace mask to use for collision detection.</param>
/// <param name="contents">The content flags to filter the trace.</param>
/// <param name="skipPlayer">Optional player whose pawn should be ignored in the trace.</param>
/// <returns>A <see cref="CGameTrace" /> object containing the trace results, or null if the trace couldn't be performed.</returns>
public static CGameTrace? GetGameTraceByEyePosition(
this CCSPlayerController player, TraceMask mask, Contents contents,
CCSPlayerController? skipPlayer) {
return player.PlayerPawn.Value?.GetGameTraceByEyePosition(mask, contents,
skipPlayer);
}
/// <summary>
/// Performs a game trace from the player pawn's eye position in the direction they are looking.
/// </summary>
/// <param name="playerPawn">The player pawn to trace from.</param>
/// <param name="mask">The trace mask to use for collision detection.</param>
/// <param name="contents">The contents flags to filter the trace.</param>
/// <param name="skipPlayer">Optional player whose pawn should be ignored in the trace.</param>
/// <returns>A <see cref="CGameTrace" /> object containing the trace results, or null if the trace couldn't be performed.</returns>
public static CGameTrace? GetGameTraceByEyePosition(
this CCSPlayerPawn playerPawn, TraceMask mask, Contents contents,
CCSPlayerController? skipPlayer) {
if (playerPawn.GetEyePosition() is not { } eyePosition) return null;
var skip = skipPlayer?.PlayerPawn.Value?.Handle ?? IntPtr.Zero;
var eyeAngles = playerPawn.EyeAngles;
var _trace =
TraceRay.TraceShape(eyePosition, eyeAngles, mask, contents, skip);
return _trace;
}
/// <summary>
/// Gets the eye position of the player in world coordinates.
/// </summary>
/// <param name="player">The player controller to get the eye position from.</param>
/// <returns>A <see cref="Vector" /> representing the eye position, or null if the position couldn't be determined.</returns>
public static Vector? GetEyePosition(this CCSPlayerController player) {
return player.PlayerPawn.Value?.GetEyePosition();
}
/// <summary>
/// Gets the eye position of the player pawn in world coordinates.
/// </summary>
/// <param name="playerPawn">The player pawn to get the eye position from.</param>
/// <returns>A <see cref="Vector" /> representing the eye position, or null if the position couldn't be determined.</returns>
public static Vector? GetEyePosition(this CCSPlayerPawn playerPawn) {
return playerPawn.AbsOrigin is not { } absOrigin ?
null :
new Vector(absOrigin.X, absOrigin.Y,
absOrigin.Z + playerPawn.ViewOffset.Z);
}
/// <summary>
/// Gets the vertical distance from the player to the ground below them.
/// </summary>
/// <param name="player">The player controller to measure from.</param>
/// <returns>The distance in units, or 0 if the player is on the ground or the measurement couldn't be taken.</returns>
public static float GetGroundDistance(this CCSPlayerController player) {
return player.PlayerPawn.Value?.GetGroundDistance() ?? 0;
}
/// <summary>
/// Gets the vertical distance from the player pawn to the ground below them.
/// </summary>
/// <param name="playerPawn">The player pawn to measure from.</param>
/// <returns>The distance in units, or 0 if the player is on the ground or the measurement couldn't be taken.</returns>
public static float GetGroundDistance(this CCSPlayerPawn playerPawn) {
if (playerPawn.GroundEntity.IsValid
|| playerPawn.AbsOrigin is not { } absOrigin)
return 0.0f;
var _trace = TraceRay.TraceShape(absOrigin, new QAngle(90, 0, 0),
TraceMask.MaskAll, Contents.Sky, 0);
return _trace.Distance();
}
/// <summary>
/// Retrieves the bitmask representing the content layers this pawn interacts with (trace mask).
/// Commonly used in trace and collision filtering logic.
/// </summary>
/// <param name="pawn">The player pawn instance.</param>
/// <returns>The interaction bitmask from the pawn's collision attributes.</returns>
public static ulong GetInteractsWith(this CCSPlayerPawn pawn) {
return pawn.Collision.CollisionAttribute.InteractsWith;
}
/// <summary>
/// Retrieves the hierarchy ID used for organizing entity relationships during collision detection.
/// This ID helps optimize trace results by skipping or including entities based on hierarchy context.
/// </summary>
/// <param name="pawn">The player pawn instance.</param>
/// <returns>The hierarchy ID from the pawn's collision attributes.</returns>
public static ushort GetHierarchyId(this CCSPlayerPawn pawn) {
return pawn.Collision.CollisionAttribute.HierarchyId;
}
}

View File

@@ -0,0 +1,185 @@
using System.Runtime.InteropServices;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
using TTT.CS2.RayTrace.Struct;
namespace TTT.CS2.RayTrace.Class;
/// <summary>
/// Provides static methods for performing trace operations in CS2.
/// </summary>
public static unsafe partial class TraceRay {
private static readonly IntPtr CTraceFilterVtable;
private static readonly IntPtr GameTraceManager;
private static readonly TraceShapeDelegate _traceShape;
private static readonly TraceShapeRayFilterDelegate _traceShapeRayFilter;
static TraceRay() {
var traceFunc = NativeAPI.FindSignature(Addresses.ServerPath,
GameData.GetSignature("TraceFunc"));
var traceShape = NativeAPI.FindSignature(Addresses.ServerPath,
GameData.GetSignature("TraceShape"));
CTraceFilterVtable = NativeAPI.FindSignature(Addresses.ServerPath,
GameData.GetSignature("CTraceFilterVtable"));
GameTraceManager = NativeAPI.FindSignature(Addresses.ServerPath,
GameData.GetSignature("GameTraceManager"));
_traceShape =
Marshal.GetDelegateForFunctionPointer<TraceShapeDelegate>(traceFunc);
_traceShapeRayFilter =
Marshal.GetDelegateForFunctionPointer<TraceShapeRayFilterDelegate>(
traceShape);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and content flags
/// </summary>
/// <param name="origin">Starting position of the trace</param>
/// <param name="angle">Direction of the trace</param>
/// <param name="mask">Trace mask flags as ulong</param>
/// <param name="content">Content flags as ulong</param>
/// <param name="skip">Entity to skip (IntPtr handle)</param>
/// <returns>CGameTrace containing the trace results</returns>
public static CGameTrace TraceShape(Vector origin, QAngle angle, ulong mask,
ulong content, IntPtr skip) {
Vector _forward = new();
NativeAPI.AngleVectors(angle.Handle, _forward.Handle, 0, 0);
Vector _endOrigin = new(origin.X + _forward.X * 8192,
origin.Y + _forward.Y * 8192, origin.Z + _forward.Z * 8192);
return TraceShape(origin, _endOrigin, mask, content, skip);
}
/// <summary>
/// Performs a trace from origin to end with specified mask and content flags
/// </summary>
/// <param name="start">Starting position of the trace</param>
/// <param name="end">Ending position of the trace</param>
/// <param name="mask">Trace mask flags as ulong</param>
/// <param name="content">Content flags as ulong</param>
/// <param name="skip">Entity to skip (IntPtr handle)</param>
/// <returns>CGameTrace containing the trace results</returns>
public static CGameTrace TraceShape(Vector start, Vector end, ulong mask,
ulong content, IntPtr skip) {
var _trace = stackalloc CGameTrace[1];
var _gameTraceManagerAddress =
Address.GetAbsoluteAddress(GameTraceManager, 3, 7);
_traceShape(*(IntPtr*)_gameTraceManagerAddress, start.Handle, end.Handle,
skip, mask, content, _trace);
return *_trace;
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and content flags
/// </summary>
/// <param name="origin">Starting position of the trace</param>
/// <param name="angle">Direction of the trace</param>
/// <param name="mask">Trace mask flags as ulong</param>
/// <param name="content">Content flags as ulong</param>
/// <param name="skip">Entity to skip (IntPtr handle)</param>
/// <param name="result">Return of _traceShape</param>
/// <returns>CGameTrace containing the trace results</returns>
public static CGameTrace TraceShapeWithResult(Vector origin, QAngle angle,
ulong mask, ulong content, IntPtr skip, out bool result) {
Vector _forward = new();
NativeAPI.AngleVectors(angle.Handle, _forward.Handle, 0, 0);
Vector _endOrigin = new(origin.X + _forward.X * 8192,
origin.Y + _forward.Y * 8192, origin.Z + _forward.Z * 8192);
return TraceShapeWithResult(origin, _endOrigin, mask, content, skip,
out result);
}
/// <summary>
/// Performs a trace from origin to end with specified mask and content flags
/// </summary>
/// <param name="start">Starting position of the trace</param>
/// <param name="end">Ending position of the trace</param>
/// <param name="mask">Trace mask flags as ulong</param>
/// <param name="content">Content flags as ulong</param>
/// <param name="skip">Entity to skip (IntPtr handle)</param>
/// <param name="result">Return of _traceShape</param>
/// <returns>CGameTrace containing the trace results</returns>
public static CGameTrace TraceShapeWithResult(Vector start, Vector end,
ulong mask, ulong content, IntPtr skip, out bool result) {
var _trace = stackalloc CGameTrace[1];
var _gameTraceManagerAddress =
Address.GetAbsoluteAddress(GameTraceManager, 3, 7);
result = _traceShape(*(IntPtr*)_gameTraceManagerAddress, start.Handle,
end.Handle, skip, mask, content, _trace);
return *_trace;
}
/// <summary>
/// Performs a hull-based ray trace using the provided shape, direction, and filter information.
/// This method wraps the native _traceShapeRayFilter call, setting up the necessary filter and trace data on the stack.
/// </summary>
/// <param name="start">Starting position of the trace</param>
/// <param name="end">Starting position of the trace</param>
/// <param name="filter"> The filter used to determine which entities or collisions should be excluded during the trace./// </param>
/// <param name="ray">A pointer to the shape of the ray (e.g., line, sphere, hull, capsule, mesh) to be traced.</param>
/// <returns>
/// Returns a <see cref="CGameTrace" /> structure containing the result of the trace operation, including hit data,
/// entity, and surface details.
/// </returns>
public static CGameTrace TraceHull(Vector start, Vector end,
CTraceFilter filter, Ray ray) {
var _trace = stackalloc CGameTrace[1];
var _filter = stackalloc CTraceFilter[1];
var _vtable = Address.GetAbsoluteAddress(CTraceFilterVtable, 3, 7);
var _gameTraceManager = Address.GetAbsoluteAddress(GameTraceManager, 3, 7);
*_filter = filter;
_filter->Vtable = (void*)_vtable;
_traceShapeRayFilter(*(nint*)_gameTraceManager, &ray, start.Handle,
end.Handle, _filter, _trace);
return *_trace;
}
/// <summary>
/// Performs a hull-based ray trace using the provided shape, direction, and filter information.
/// This method wraps the native _traceShapeRayFilter call, setting up the necessary filter and trace data on the stack.
/// </summary>
/// <param name="start">Starting position of the trace</param>
/// <param name="end">Starting position of the trace</param>
/// <param name="filter"> The filter used to determine which entities or collisions should be excluded during the trace./// </param>
/// <param name="ray">A pointer to the shape of the ray (e.g., line, sphere, hull, capsule, mesh) to be traced.</param>
/// <param name="result">Return of _traceShape</param>
/// <returns>
/// Returns a <see cref="CGameTrace" /> structure containing the result of the trace operation, including hit data,
/// entity, and surface details.
/// </returns>
public static CGameTrace TraceHullWithResult(Vector start, Vector end,
CTraceFilter filter, Ray ray, out bool result) {
var _trace = stackalloc CGameTrace[1];
var _filter = stackalloc CTraceFilter[1];
var _vtable = Address.GetAbsoluteAddress(CTraceFilterVtable, 3, 7);
var _gameTraceManager = Address.GetAbsoluteAddress(GameTraceManager, 3, 7);
*_filter = filter;
_filter->Vtable = (void*)_vtable;
result = _traceShapeRayFilter(*(nint*)_gameTraceManager, &ray, start.Handle,
end.Handle, _filter, _trace);
return *_trace;
}
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate bool TraceShapeDelegate(IntPtr GameTraceManager,
IntPtr vecStart, IntPtr vecEnd, IntPtr skip, ulong mask, ulong content,
CGameTrace* pGameTrace);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate bool TraceShapeRayFilterDelegate(IntPtr GameTraceManager,
Ray* trace, IntPtr vecStart, IntPtr vecEnd, CTraceFilter* traceFilter,
CGameTrace* pGameTrace);
}

View File

@@ -0,0 +1,561 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using TTT.CS2.RayTrace.Enum;
using TTT.CS2.RayTrace.Struct;
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
namespace TTT.CS2.RayTrace.Class;
/// <summary>
/// Provides extension methods for <see cref="TraceRay" /> class
/// </summary>
public static partial class TraceRay {
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and content flags, skipping a player
/// controller
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
TraceMask mask, Contents content, CCSPlayerController skip) {
return TraceShape(origin, angle, (ulong)mask, (ulong)content,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and content flags
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
TraceMask mask, ulong content, IntPtr skip) {
return TraceShape(origin, angle, (ulong)mask, content, skip);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and content flags
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle, ulong mask,
Contents content, IntPtr skip) {
return TraceShape(origin, angle, mask, (ulong)content, skip);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and content flags
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
TraceMask mask, Contents content, IntPtr skip) {
return TraceShape(origin, angle, (ulong)mask, (ulong)content, skip);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and content flags
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
Contents mask, ulong content, IntPtr skip) {
return TraceShape(origin, angle, (ulong)mask, content, skip);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and content flags
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
Contents mask, Contents content, IntPtr skip) {
return TraceShape(origin, angle, (ulong)mask, (ulong)content, skip);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified content flags
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
Contents content, IntPtr skip) {
return TraceShape(origin, angle, (ulong)content, (ulong)content, skip);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask flags
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
TraceMask mask, IntPtr skip) {
return TraceShape(origin, angle, (ulong)mask, (ulong)mask, skip);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with raw mask value
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle, ulong mask,
IntPtr skip) {
return TraceShape(origin, angle, mask, mask, skip);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified content flags, skipping a player controller
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
Contents content, CCSPlayerController skip) {
return TraceShape(origin, angle, (ulong)content, (ulong)content,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and raw content value, skipping a player
/// controller
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
TraceMask mask, ulong content, CCSPlayerController skip) {
return TraceShape(origin, angle, (ulong)mask, content,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin in the direction of angle with raw mask and content flags, skipping a player controller
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle, ulong mask,
Contents content, CCSPlayerController skip) {
return TraceShape(origin, angle, mask, (ulong)content,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and raw content value, skipping a player
/// controller
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
Contents mask, ulong content, CCSPlayerController skip) {
return TraceShape(origin, angle, (ulong)mask, content,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin in the direction of angle with raw mask and content values, skipping a player controller
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle, ulong mask,
ulong content, CCSPlayerController skip) {
return TraceShape(origin, angle, mask, content, GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and content flags, skipping a player
/// controller
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
Contents mask, Contents content, CCSPlayerController skip) {
return TraceShape(origin, angle, (ulong)mask, (ulong)content,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask flags, skipping a player controller
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
TraceMask mask, CCSPlayerController skip) {
return TraceShape(origin, angle, (ulong)mask, (ulong)mask,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin in the direction of angle with raw mask value, skipping a player controller
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle, ulong mask,
CCSPlayerController skip) {
return TraceShape(origin, angle, mask, mask, GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and content flags, skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
TraceMask mask, Contents content, CCSPlayerPawn skip) {
return TraceShape(origin, angle, (ulong)mask, (ulong)content, skip.Handle);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified content flags, skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
Contents content, CCSPlayerPawn skip) {
return TraceShape(origin, angle, (ulong)content, (ulong)content,
skip.Handle);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and raw content value, skipping a player
/// pawn
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
TraceMask mask, ulong content, CCSPlayerPawn skip) {
return TraceShape(origin, angle, (ulong)mask, content, skip.Handle);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with raw mask and content flags, skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle, ulong mask,
Contents content, CCSPlayerPawn skip) {
return TraceShape(origin, angle, mask, (ulong)content, skip.Handle);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and raw content value, skipping a player
/// pawn
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
Contents mask, ulong content, CCSPlayerPawn skip) {
return TraceShape(origin, angle, (ulong)mask, content, skip.Handle);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with raw mask and content values, skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle, ulong mask,
ulong content, CCSPlayerPawn skip) {
return TraceShape(origin, angle, mask, content, skip.Handle);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and content flags, skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
Contents mask, Contents content, CCSPlayerPawn skip) {
return TraceShape(origin, angle, (ulong)mask, (ulong)content, skip.Handle);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask flags, skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
TraceMask mask, CCSPlayerPawn skip) {
return TraceShape(origin, angle, (ulong)mask, (ulong)mask, skip.Handle);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with raw mask value, skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle, ulong mask,
CCSPlayerPawn skip) {
return TraceShape(origin, angle, mask, mask, skip.Handle);
}
/// <summary>
/// Performs a trace from origin to end with specified mask and content flags
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, TraceMask mask,
ulong content, IntPtr skip) {
return TraceShape(start, end, (ulong)mask, content, skip);
}
/// <summary>
/// Performs a trace from origin to end with raw mask and content flags
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, ulong mask,
Contents content, IntPtr skip) {
return TraceShape(start, end, mask, (ulong)content, skip);
}
/// <summary>
/// Performs a trace from origin to end with specified mask and content flags
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, TraceMask mask,
Contents content, IntPtr skip) {
return TraceShape(start, end, (ulong)mask, (ulong)content, skip);
}
/// <summary>
/// Performs a trace from origin to end with specified mask and raw content value
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, Contents mask,
ulong content, IntPtr skip) {
return TraceShape(start, end, (ulong)mask, content, skip);
}
/// <summary>
/// Performs a trace from origin to end with specified mask and content flags
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, Contents mask,
Contents content, IntPtr skip) {
return TraceShape(start, end, (ulong)mask, (ulong)content, skip);
}
/// <summary>
/// Performs a trace from origin to end with specified content flags
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end,
Contents content, IntPtr skip) {
return TraceShape(start, end, (ulong)content, (ulong)content, skip);
}
/// <summary>
/// Performs a trace from origin to end with specified mask flags
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, TraceMask mask,
IntPtr skip) {
return TraceShape(start, end, (ulong)mask, (ulong)mask, skip);
}
/// <summary>
/// Performs a trace from origin to end with raw mask value
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, ulong mask,
IntPtr skip) {
return TraceShape(start, end, mask, mask, skip);
}
/// <summary>
/// Performs a trace from origin to end with specified mask and content flags, skipping a player controller
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, TraceMask mask,
Contents content, CCSPlayerController skip) {
return TraceShape(start, end, (ulong)mask, (ulong)content,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin to end with specified content flags, skipping a player controller
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end,
Contents content, CCSPlayerController skip) {
return TraceShape(start, end, (ulong)content, (ulong)content,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin to end with specified mask and raw content value, skipping a player controller
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, TraceMask mask,
ulong content, CCSPlayerController skip) {
return TraceShape(start, end, (ulong)mask, content,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin to end with raw mask and content flags, skipping a player controller
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, ulong mask,
Contents content, CCSPlayerController skip) {
return TraceShape(start, end, mask, (ulong)content,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin to end with specified mask and raw content value, skipping a player controller
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, Contents mask,
ulong content, CCSPlayerController skip) {
return TraceShape(start, end, (ulong)mask, content,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin to end with raw mask and content values, skipping a player controller
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, ulong mask,
ulong content, CCSPlayerController skip) {
return TraceShape(start, end, mask, content, GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin to end with specified mask and content flags, skipping a player controller
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, Contents mask,
Contents content, CCSPlayerController skip) {
return TraceShape(start, end, (ulong)mask, (ulong)content,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin to end with specified mask flags, skipping a player controller
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, TraceMask mask,
CCSPlayerController skip) {
return TraceShape(start, end, (ulong)mask, (ulong)mask, skip.Handle);
}
/// <summary>
/// Performs a trace from origin to end with raw mask value, skipping a player controller
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, ulong mask,
CCSPlayerController skip) {
return TraceShape(start, end, mask, mask, GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin to end with specified mask and content flags, skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, TraceMask mask,
Contents content, CCSPlayerPawn skip) {
return TraceShape(start, end, (ulong)mask, (ulong)content, skip.Handle);
}
/// <summary>
/// Performs a trace from origin to end with specified content flags, skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end,
Contents content, CCSPlayerPawn skip) {
return TraceShape(start, end, (ulong)content, (ulong)content, skip.Handle);
}
/// <summary>
/// Performs a trace from origin to end with specified mask and raw content value, skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, TraceMask mask,
ulong content, CCSPlayerPawn skip) {
return TraceShape(start, end, (ulong)mask, content, skip.Handle);
}
/// <summary>
/// Performs a trace from origin to end with raw mask and content flags, skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, ulong mask,
Contents content, CCSPlayerPawn skip) {
return TraceShape(start, end, mask, (ulong)content, skip.Handle);
}
/// <summary>
/// Performs a trace from origin to end with specified mask and raw content value, skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, Contents mask,
ulong content, CCSPlayerPawn skip) {
return TraceShape(start, end, (ulong)mask, content, skip.Handle);
}
/// <summary>
/// Performs a trace from origin to end with raw mask and content values, skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, ulong mask,
ulong content, CCSPlayerPawn skip) {
return TraceShape(start, end, mask, content, skip.Handle);
}
/// <summary>
/// Performs a trace from origin to end with specified mask and content flags, skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, Contents mask,
Contents content, CCSPlayerPawn skip) {
return TraceShape(start, end, (ulong)mask, (ulong)content, skip.Handle);
}
/// <summary>
/// Performs a trace from origin to end with specified mask flags, skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, TraceMask mask,
CCSPlayerPawn skip) {
return TraceShape(start, end, (ulong)mask, (ulong)mask, skip.Handle);
}
/// <summary>
/// Performs a trace from origin to end with raw mask value, skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, ulong mask,
CCSPlayerPawn skip) {
return TraceShape(start, end, mask, mask, skip.Handle);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and content flags (both as TraceMask)
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
TraceMask mask, TraceMask content, IntPtr skip) {
return TraceShape(origin, angle, (ulong)mask, (ulong)content, skip);
}
/// <summary>
/// Performs a trace from origin to end with specified mask (as Contents) and content (as TraceMask), skipping a player
/// controller
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, Contents mask,
TraceMask content, CCSPlayerController skip) {
return TraceShape(start, end, (ulong)mask, (ulong)content,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and content flags (both as TraceMask),
/// skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
TraceMask mask, TraceMask content, CCSPlayerPawn skip) {
return TraceShape(origin, angle, (ulong)mask, (ulong)content, skip.Handle);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask (as Contents) and content (as TraceMask)
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
Contents mask, TraceMask content, IntPtr skip) {
return TraceShape(origin, angle, (ulong)mask, (ulong)content, skip);
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask and content flags (both as TraceMask),
/// skipping a player controller
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
TraceMask mask, TraceMask content, CCSPlayerController skip) {
return TraceShape(origin, angle, (ulong)mask, (ulong)content,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask (as Contents) and content (as TraceMask),
/// skipping a player controller
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
Contents mask, TraceMask content, CCSPlayerController skip) {
return TraceShape(origin, angle, (ulong)mask, (ulong)content,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin in the direction of angle with specified mask (as Contents) and content (as TraceMask),
/// skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector origin, QAngle angle,
Contents mask, TraceMask content, CCSPlayerPawn skip) {
return TraceShape(origin, angle, (ulong)mask, (ulong)content, skip.Handle);
}
/// <summary>
/// Performs a trace from origin to end with specified mask and content flags (both as TraceMask)
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, TraceMask mask,
TraceMask content, IntPtr skip) {
return TraceShape(start, end, (ulong)mask, (ulong)content, skip);
}
/// <summary>
/// Performs a trace from origin to end with specified mask (as Contents) and content (as TraceMask)
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, Contents mask,
TraceMask content, IntPtr skip) {
return TraceShape(start, end, (ulong)mask, (ulong)content, skip);
}
/// <summary>
/// Performs a trace from origin to end with specified mask and content flags (both as TraceMask), skipping a player
/// controller
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, TraceMask mask,
TraceMask content, CCSPlayerController skip) {
return TraceShape(start, end, (ulong)mask, (ulong)content,
GetSafeSkipHandle(skip));
}
/// <summary>
/// Performs a trace from origin to end with specified mask and content flags (both as TraceMask), skipping a player pawn
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, TraceMask mask,
TraceMask content, CCSPlayerPawn skip) {
return TraceShape(start, end, (ulong)mask, (ulong)content, skip.Handle);
}
/// <summary>
/// Performs a trace from origin to end with specified mask (as Contents) and content (as TraceMask), skipping a player
/// pawn
/// </summary>
public static CGameTrace TraceShape(Vector start, Vector end, Contents mask,
TraceMask content, CCSPlayerPawn skip) {
return TraceShape(start, end, (ulong)mask, (ulong)content, skip.Handle);
}
private static IntPtr GetSafeSkipHandle(CCSPlayerController player) {
return player.PlayerPawn.Value is not { } playerPawn ?
IntPtr.Zero :
playerPawn.Handle;
}
}

View File

@@ -0,0 +1,131 @@
namespace TTT.CS2.RayTrace.Enum;
/// <summary>
/// Bitmask flags representing collision layers and content types in CS2.
/// Used for trace operations to filter what should be hit.
/// </summary>
[Flags]
public enum Contents : ulong {
/// <summary>Empty</summary>
Empty = 0,
/// <summary>Solid</summary>
Solid = 1ul << LayerIndex.Solid,
/// <summary>Hitbox</summary>
Hitbox = 1ul << LayerIndex.Hitbox,
/// <summary>Trigger</summary>
Trigger = 1ul << LayerIndex.Trigger,
/// <summary>Sky</summary>
Sky = 1ul << LayerIndex.Sky,
/// <summary>PlayerClip</summary>
PlayerClip = 1ul << StandardLayerIndex.PlayerClip,
/// <summary>NpcClip</summary>
NpcClip = 1ul << StandardLayerIndex.NpcClip,
/// <summary>BlockLos</summary>
BlockLos = 1ul << StandardLayerIndex.BlockLos,
/// <summary>BlockLight</summary>
BlockLight = 1ul << StandardLayerIndex.BlockLight,
/// <summary>Ladder</summary>
Ladder = 1ul << StandardLayerIndex.Ladder,
/// <summary>Pickup</summary>
Pickup = 1ul << StandardLayerIndex.Pickup,
/// <summary>BlockSound</summary>
BlockSound = 1ul << StandardLayerIndex.BlockSound,
/// <summary>NoDraw</summary>
NoDraw = 1ul << StandardLayerIndex.NoDraw,
/// <summary>Window</summary>
Window = 1ul << StandardLayerIndex.Window,
/// <summary>PassBullets</summary>
PassBullets = 1ul << StandardLayerIndex.PassBullets,
/// <summary>WorldGeometry</summary>
WorldGeometry = 1ul << StandardLayerIndex.WorldGeometry,
/// <summary>Water</summary>
Water = 1ul << StandardLayerIndex.Water,
/// <summary>Slime</summary>
Slime = 1ul << StandardLayerIndex.Slime,
/// <summary>TouchAll</summary>
TouchAll = 1ul << StandardLayerIndex.TouchAll,
/// <summary>Player</summary>
Player = 1ul << StandardLayerIndex.Player,
/// <summary>Npc</summary>
Npc = 1ul << StandardLayerIndex.Npc,
/// <summary>Debris</summary>
Debris = 1ul << StandardLayerIndex.Debris,
/// <summary>PhysicsProp</summary>
PhysicsProp = 1ul << StandardLayerIndex.PhysicsProp,
/// <summary>NavIgnore</summary>
NavIgnore = 1ul << StandardLayerIndex.NavIgnore,
/// <summary>NavLocalIgnore</summary>
NavLocalIgnore = 1ul << StandardLayerIndex.NavLocalIgnore,
/// <summary>PostProcessingVolume</summary>
PostProcessingVolume = 1ul << StandardLayerIndex.PostProcessingVolume,
/// <summary>UnusedLayer3</summary>
UnusedLayer3 = 1ul << StandardLayerIndex.UnusedLayer3,
/// <summary>CarriedObject</summary>
CarriedObject = 1ul << StandardLayerIndex.CarriedObject,
/// <summary>Pushaway</summary>
Pushaway = 1ul << StandardLayerIndex.Pushaway,
/// <summary>ServerEntityOnClient</summary>
ServerEntityOnClient = 1ul << StandardLayerIndex.ServerEntityOnClient,
/// <summary>CarriedWeapon</summary>
CarriedWeapon = 1ul << StandardLayerIndex.CarriedWeapon,
/// <summary>StaticLevel</summary>
StaticLevel = 1ul << StandardLayerIndex.StaticLevel,
/// <summary>CsgoTeam1</summary>
CsgoTeam1 = 1ul << CsgoLayerIndex.Team1,
/// <summary>CsgoTeam2</summary>
CsgoTeam2 = 1ul << CsgoLayerIndex.Team2,
/// <summary>CsgoGrenadeClip</summary>
CsgoGrenadeClip = 1ul << CsgoLayerIndex.GrenadeClip,
/// <summary>CsgoDroneClip</summary>
CsgoDroneClip = 1ul << CsgoLayerIndex.DroneClip,
/// <summary>CsgoMoveable</summary>
CsgoMoveable = 1ul << CsgoLayerIndex.Moveable,
/// <summary>CsgoOpaque</summary>
CsgoOpaque = 1ul << CsgoLayerIndex.Opaque,
/// <summary>CsgoMonster</summary>
CsgoMonster = 1ul << CsgoLayerIndex.Monster,
/// <summary>CsgoUnusedLayer</summary>
CsgoUnusedLayer = 1ul << CsgoLayerIndex.UnusedLayer,
/// <summary>CsgoThrownGrenade</summary>
CsgoThrownGrenade = 1ul << CsgoLayerIndex.ThrownGrenade
}

View File

@@ -0,0 +1,33 @@
namespace TTT.CS2.RayTrace.Enum;
/// <summary>
/// Specific layer indices used for content masking
/// </summary>
public enum CsgoLayerIndex {
/// <summary>Team 1 layer</summary>
Team1 = StandardLayerIndex.FirstModSpecific,
/// <summary>Team 2 layer</summary>
Team2,
/// <summary>Grenade collision layer</summary>
GrenadeClip,
/// <summary>Drone collision layer</summary>
DroneClip,
/// <summary>Movable physics objects layer</summary>
Moveable,
/// <summary>Opaque surfaces layer</summary>
Opaque,
/// <summary>Monster/NPC layer</summary>
Monster,
/// <summary>Unused/reserved layer</summary>
UnusedLayer,
/// <summary>Thrown grenade entities layer</summary>
ThrownGrenade
}

View File

@@ -0,0 +1,21 @@
namespace TTT.CS2.RayTrace.Enum;
/// <summary>
/// Specifies the type of matching to perform on the designer name.
/// </summary>
public enum DesignerNameMatchType {
/// <summary>
/// Matches if the designer name is exactly equal to the specified string.
/// </summary>
Equals,
/// <summary>
/// Matches if the designer name starts with the specified string.
/// </summary>
StartsWith,
/// <summary>
/// Matches if the designer name ends with the specified string.
/// </summary>
EndsWith
}

View File

@@ -0,0 +1,27 @@
namespace TTT.CS2.RayTrace.Enum;
/// <summary>
/// Defines base layer indices used for collision detection and tracing
/// </summary>
public enum LayerIndex {
/// <summary>Solid objects layer</summary>
Solid = 0,
/// <summary>Hitbox collision layer</summary>
Hitbox,
/// <summary>Trigger volume layer</summary>
Trigger,
/// <summary>Skybox layer</summary>
Sky,
/// <summary>First available layer for user-defined content</summary>
FirstUser,
/// <summary>Special value indicating layer not found</summary>
NotFound = -1,
/// <summary>Maximum allowed layer index</summary>
MaxAllowed = 64
}

View File

@@ -0,0 +1,32 @@
namespace TTT.CS2.RayTrace.Enum;
/// <summary>
/// Specifies the geometric shape of a ray used in tracing and collision detection operations.
/// Determines how the underlying ray data should be interpreted during a trace.
/// </summary>
public enum RayType {
/// <summary>
/// A straight line with optional thickness (radius). Default shape for basic traces.
/// </summary>
Line,
/// <summary>
/// A spherical shape used for proximity or point-radius-based traces.
/// </summary>
Sphere,
/// <summary>
/// An axis-aligned bounding box (AABB) used for volume-based tracing.
/// </summary>
Hull,
/// <summary>
/// A capsule shape defined by two points and a radius. Suitable for player bounding volumes.
/// </summary>
Capsule,
/// <summary>
/// A custom mesh composed of multiple vertices for complex trace geometry.
/// </summary>
Mesh
}

View File

@@ -0,0 +1,90 @@
namespace TTT.CS2.RayTrace.Enum;
/// <summary>
/// Standard layer indices used for content masking
/// </summary>
public enum StandardLayerIndex {
/// <summary>PlayerClip</summary>
PlayerClip = LayerIndex.FirstUser,
/// <summary>NpcClip</summary>
NpcClip,
/// <summary>BlockLos</summary>
BlockLos,
/// <summary>BlockLight</summary>
BlockLight,
/// <summary>Ladder</summary>
Ladder,
/// <summary>Pickup</summary>
Pickup,
/// <summary>BlockSound</summary>
BlockSound,
/// <summary>NoDraw</summary>
NoDraw,
/// <summary>Window</summary>
Window,
/// <summary>PassBullets</summary>
PassBullets,
/// <summary>WorldGeometry</summary>
WorldGeometry,
/// <summary>Water</summary>
Water,
/// <summary>Slime</summary>
Slime,
/// <summary>TouchAll</summary>
TouchAll,
/// <summary>Player</summary>
Player,
/// <summary>Npc</summary>
Npc,
/// <summary>Debris</summary>
Debris,
/// <summary>PhysicsProp</summary>
PhysicsProp,
/// <summary>NavIgnore</summary>
NavIgnore,
/// <summary>NavLocalIgnore</summary>
NavLocalIgnore,
/// <summary>PostProcessingVolume</summary>
PostProcessingVolume,
/// <summary>UnusedLayer3</summary>
UnusedLayer3,
/// <summary>CarriedObject</summary>
CarriedObject,
/// <summary>Pushaway</summary>
Pushaway,
/// <summary>ServerEntityOnClient</summary>
ServerEntityOnClient,
/// <summary>CarriedWeapon</summary>
CarriedWeapon,
/// <summary>StaticLevel</summary>
StaticLevel,
/// <summary>FirstModSpecific</summary>
FirstModSpecific
}

View File

@@ -0,0 +1,55 @@
namespace TTT.CS2.RayTrace.Enum;
/// <summary>
/// Predefined trace masks for common collision detection scenarios
/// </summary>
[Flags]
public enum TraceMask : ulong {
/// <summary>Matches everything</summary>
MaskAll = ~0ul,
/// <summary>Everything that is normally solid</summary>
MaskSolid = Contents.Solid | Contents.Window | Contents.Player | Contents.Npc
| Contents.PassBullets,
/// <summary>Everything that blocks player movement</summary>
MaskPlayerSolid = Contents.Solid | Contents.PlayerClip | Contents.Window
| Contents.Player | Contents.Npc | Contents.PassBullets,
/// <summary>Blocks NPC movement</summary>
MaskNpcSolid = Contents.Solid | Contents.NpcClip | Contents.Window
| Contents.Player | Contents.Npc | Contents.PassBullets,
/// <summary>Blocks fluid movement</summary>
MaskNpcFluid = Contents.Solid | Contents.NpcClip | Contents.Window
| Contents.Player | Contents.Npc,
/// <summary>Water physics contents</summary>
MaskWater = Contents.Water | Contents.Slime,
/// <summary>Contents that bullets see as solid</summary>
MaskShot = Contents.Solid | Contents.Player | Contents.Npc | Contents.Window
| Contents.Debris | Contents.Hitbox,
/// <summary>Bullet collision (world+brush only, no monsters)</summary>
MaskShotBrushOnly = Contents.Solid | Contents.Window | Contents.Debris,
/// <summary>Non-raycasted weapons collision (includes grates)</summary>
MaskShotHull = Contents.Solid | Contents.Player | Contents.Npc
| Contents.Window | Contents.Debris | Contents.PassBullets,
/// <summary>Portal gun trace collision</summary>
MaskShotPortal =
Contents.Solid | Contents.Window | Contents.Player | Contents.Npc,
/// <summary>Solid contents (world+brush only, no monsters)</summary>
MaskSolidBrushOnly = Contents.Solid | Contents.Window | Contents.PassBullets,
/// <summary>Player movement collision (world+brush only, no monsters)</summary>
MaskPlayerSolidBrushOnly = Contents.Solid | Contents.Window
| Contents.PlayerClip | Contents.PassBullets,
/// <summary>NPC movement collision (world+brush only, no monsters)</summary>
MaskNpcSolidBrushOnly = Contents.Solid | Contents.Window | Contents.NpcClip
| Contents.PassBullets
}

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