Compare commits

...

133 Commits

Author SHA1 Message Date
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
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
175 changed files with 6054 additions and 847 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

@@ -13,7 +13,7 @@ jobs:
auto-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
@@ -34,6 +34,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 +47,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,12 +61,12 @@ 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
- name: Determine previous relevant tag
@@ -78,16 +83,16 @@ jobs:
# 5. Generate changelog
- name: Generate changelog
run: |
gh api repos/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.tag }}...${{ steps.gitversion.outputs.MajorMinorPatch }} \
gh api repos/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.tag }}...${{ steps.gitversion.outputs.fullSemVer }} \
--jq '.commits[].commit.message' > CHANGELOG.md
env:
GH_TOKEN: ${{ github.token }}
# 6. Create release
- 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,6 +1,7 @@
| Package | Version | License Information Origin | License Expression | License Url | Copyright | Authors | Package Project Url |
|-------------------------------------------------------|----------|----------------------------|--------------------|-----------------------------------------|-------------------------------------------------|----------------------------------|--------------------------------------------------------------------------------------------------|
| ----------------------------------------------------- | -------- | -------------------------- | ------------------ | --------------------------------------- | ----------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------ |
| CounterStrikeSharp.API | 1.0.332 | Expression | GPL-3.0-only | https://licenses.nuget.org/GPL-3.0-only | | Roflmuffin | http://docs.cssharp.dev/ |
| CounterStrikeSharp.API | 1.0.340 | Expression | GPL-3.0-only | https://licenses.nuget.org/GPL-3.0-only | | Roflmuffin | http://docs.cssharp.dev/ |
| JetBrains.Annotations | 2025.2.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) 2016-2025 JetBrains s.r.o. | JetBrains | https://www.jetbrains.com/help/resharper/Code_Analysis__Code_Annotations.html |
| Microsoft.Extensions.DependencyInjection.Abstractions | 9.0.7 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://dot.net/ |
| Microsoft.Extensions.Localization.Abstractions | 8.0.3 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://asp.net/ |

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.

View File

@@ -19,6 +19,8 @@ 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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -60,6 +62,10 @@ 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
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,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,35 @@ 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))) {
# if DEBUG
Console.WriteLine(
$"[DEBUG] Registering {typeof(TExtension).Name} as IPluginModule");
# endif
collection.AddTransient<IPluginModule>(provider
=> (provider.GetRequiredService<TExtension>() as IPluginModule)!);
}
if (typeof(TExtension).IsAssignableTo(typeof(IListener))) {
#if DEBUG
Console.WriteLine(
$"[DEBUG] Registering {typeof(TExtension).Name} as IListener");
# endif
collection.AddTransient<IListener>(provider
=> (provider.GetRequiredService<TExtension>() as IListener)!);
}
if (typeof(TExtension).IsAssignableTo(typeof(ICommand))) {
#if DEBUG
Console.WriteLine(
$"[DEBUG] Registering {typeof(TExtension).Name} as ICommand");
#endif
collection.AddTransient<ICommand>(provider
=> (provider.GetRequiredService<TExtension>() as ICommand)!);
}
collection.AddScoped<TExtension>();
@@ -87,9 +63,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 Name => GetType().Name;
string Version => GitVersionInformation.FullSemVer;
void Start();
}

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="..\Shop\Shop.csproj"/>
</ItemGroup>
<ItemGroup>
<Folder Include="RayTrace\"/>
</ItemGroup>
</Project>

View File

@@ -1,12 +1,29 @@
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game;
using TTT.Game.Roles;
using TTT.Locale;
namespace TTT.CS2;
public class CS2Body(CRagdollProp ragdoll, IPlayer player) : IBody {
public class CS2Body(IServiceProvider provider, CRagdollProp ragdoll,
IPlayer player) : IBody {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IMsgLocalizer locale =
provider.GetRequiredService<IMsgLocalizer>();
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
public CRagdollProp Ragdoll { get; } = ragdoll;
public IPlayer OfPlayer { get; } = player;
public bool IsIdentified { get; set; }
@@ -28,9 +45,4 @@ public class CS2Body(CRagdollProp ragdoll, IPlayer player) : IBody {
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

@@ -6,51 +6,74 @@ 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;
using TTT.CS2.Player;
using TTT.Game;
using TTT.Locale;
using TTT.Shop;
using TTT.Shop.Items;
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>();
// 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>();
// 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<RoleIconsHandler>();
collection.AddModBehavior<RoundEnd_GameEndHandler>();
collection.AddModBehavior<RoundStart_GameStartHandler>();
// Damage Cancelers
collection.AddModBehavior<OutOfRoundCanceler>();
collection.AddModBehavior<TaserListenCanceler>();
// Listeners
collection.AddListener<RoundTimerListener>();
collection.AddListener<BodyPickupListener>();
collection.AddListener<PlayerStatsTracker>();
collection.AddModBehavior<BodyPickupListener>();
collection.AddModBehavior<IBodyTracker, BodyTracker>();
collection.AddModBehavior<LateSpawnListener>();
collection.AddModBehavior<PlayerStatsTracker>();
collection.AddModBehavior<RoundTimerListener>();
// 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,7 +5,6 @@ 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;
@@ -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 Name => "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,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 Name => "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,41 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Player;
using TTT.CS2.Hats;
namespace TTT.CS2.Command.Test;
public class ScreenTextCommand(IServiceProvider provider) : ICommand {
public string Name => "screentext";
private readonly ITextSpawner spawner =
provider.GetRequiredService<ITextSpawner>();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public void Dispose() { }
public void Start() { }
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
var textSetting = new TextSetting { msg = "Foo" };
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
Server.NextWorldUpdate(() => {
var ents =
Utilities.FindAllEntitiesByDesignerName<CPointWorldText>(
"point_worldtext");
foreach (var ent in ents) ent.AcceptInput("Kill");
var player = converter.GetPlayer(executor);
if (player == null || !player.IsValid) return;
spawner.CreateTextScreen(textSetting, player);
info.ReplySync("Spawned screen text.");
});
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;
@@ -19,7 +18,6 @@ public class SetRoleCommand(IServiceProvider provider) : ICommand {
public void Dispose() { }
public string Name => "setrole";
public string Version => GitVersionInformation.FullSemVer;
public void Start() { }
public Task<CommandResult>

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 Name => "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,19 @@ 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 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("screentext", new ScreenTextCommand(provider));
}
public Task<CommandResult>
@@ -43,6 +38,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

@@ -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 TTT.API;
using TTT.API.Storage;
using TTT.Shop;
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,45 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using TTT.API;
using TTT.API.Storage;
using TTT.CS2.Validators;
using TTT.Shop.Items;
namespace TTT.CS2.Configs.ShopItems;
public class CS2OneShotDeagleConfig : IStorage<OneShotDeagleConfig>,
IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_onedeagle_price", "Price of the One-Shot Deagle item", 100,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<bool> CV_FRIENDLY_FIRE = new(
"css_ttt_shop_onedeagle_ff",
"Whether the One-Shot Deagle damages teammates", true);
public static readonly FakeConVar<string> CV_WEAPON = new(
"css_ttt_shop_onedeagle_weapon",
"Weapon entity name used for the One-Shot Deagle", "weapon_revolver",
ConVarFlags.FCVAR_NONE, new ItemValidator(allowMultiple: false));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(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
};
return Task.FromResult<OneShotDeagleConfig?>(cfg);
}
}

View File

@@ -1,6 +1,7 @@
using System.Drawing;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.UserMessages;
namespace TTT.CS2.Extensions;
@@ -8,7 +9,7 @@ public static class PlayerExtensions {
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
@@ -27,11 +28,38 @@ public static class PlayerExtensions {
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);
pawn.SetColor(color);
}
public enum FadeFlags {
FADE_IN, FADE_OUT, FADE_STAYOUT
}
public static void ColorScreen(this CCSPlayerController player, Color color,
float hold = 0.1f, float fade = 0.2f, FadeFlags flags = FadeFlags.FADE_IN,
bool withPurge = true) {
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;
@@ -53,8 +55,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

@@ -11,6 +11,8 @@ 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 +42,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

@@ -6,9 +6,13 @@ 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.");
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;
}
ActiveGame = new CS2Game(Provider);

View File

@@ -7,6 +7,7 @@ using CounterStrikeSharp.API.Modules.Utils;
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;
@@ -19,21 +20,22 @@ 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() { }
[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));
var ragdollBody = makeGameRagdoll(player);
var body = new CS2Body(ragdollBody, converter.GetPlayer(player));
var body = new CS2Body(provider, ragdollBody, converter.GetPlayer(player));
if (ev.Attacker != null && ev.Attacker.IsValid)
body.WithKiller(converter.GetPlayer(ev.Attacker));
@@ -43,12 +45,7 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
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;
}
@@ -63,20 +60,20 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
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

@@ -7,6 +7,7 @@ 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;
@@ -20,8 +21,8 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
public string Name => "CombatListeners";
public string Version => GitVersionInformation.FullSemVer;
private readonly IAliveSpoofer spoofer =
provider.GetRequiredService<IAliveSpoofer>();
public void Start() { }
@@ -35,7 +36,8 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
[UsedImplicitly]
[GameEventHandler(HookMode.Pre)]
public HookResult OnPlayerDeath_Pre(EventPlayerDeath ev, GameEventInfo info) {
if (!games.IsGameActive()) return HookResult.Continue;
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);
@@ -43,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;
@@ -51,42 +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 _) {
if (!games.IsGameActive()) return HookResult.Continue;
// DamageCanceler already handles this on non-Windows platforms
if (!OperatingSystem.IsWindows()) return HookResult.Continue;
var player = ev.Userid;
if (player == null) return HookResult.Continue;
@@ -94,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 Name => 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

@@ -3,6 +3,8 @@ using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.Game.Events.Player;
@@ -15,8 +17,11 @@ public class PlayerConnectionsHandler(IServiceProvider provider)
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public string Name => nameof(PlayerConnectionsHandler);
public string Version => GitVersionInformation.FullSemVer;
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
public void Start() { }
@@ -30,11 +35,10 @@ public class PlayerConnectionsHandler(IServiceProvider provider)
disconnectFromServer);
Server.NextWorldUpdate(() => {
foreach (var player in Utilities.GetPlayers()) {
var gamePlayer = converter.GetPlayer(player);
var ev = new PlayerJoinEvent(gamePlayer);
foreach (var ev in Utilities.GetPlayers()
.Select(player => converter.GetPlayer(player))
.Select(gamePlayer => new PlayerJoinEvent(gamePlayer)))
bus.Dispatch(ev);
}
});
}
@@ -42,14 +46,11 @@ public class PlayerConnectionsHandler(IServiceProvider provider)
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 +62,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,32 @@
using CounterStrikeSharp.API.Core;
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 {
private static readonly string CT_MODEL =
"characters/models/ctm_fbi/ctm_fbi_varianth.vmdl";
private static readonly string T_MODEL =
"characters/models/tm_phoenix/tm_phoenix.vmdl";
private readonly IDictionary<int, IEnumerable<CPointWorldText>>
detectiveIcons = new Dictionary<int, IEnumerable<CPointWorldText>>();
private readonly IPlayerConverter<CCSPlayerController> players =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
@@ -29,47 +39,34 @@ public class RoleIconsHandler(IServiceProvider provider)
private readonly ISet<int> traitors = new HashSet<int>();
public void Dispose() { bus.UnregisterListener(this); }
public string Name => nameof(RoleIconsHandler);
public string Version => GitVersionInformation.FullSemVer;
public void Start() { }
public void Start(BasePlugin? plugin) {
plugin
?.RegisterListener<CounterStrikeSharp.API.Core.Listeners.CheckTransmit>(
onTransmit);
bus.RegisterListener(this);
}
[UsedImplicitly]
[EventHandler(IgnoreCanceled = true)]
public void OnRoundStart(GameStateUpdateEvent ev) {
if (ev.NewState != State.IN_PROGRESS) return;
traitors.Clear();
traitorIcons.Clear();
detectiveIcons.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
removeAllIcons(player);
player.SwitchTeam(ev.Role is DetectiveRole ?
CsTeam.CounterTerrorist :
@@ -79,39 +76,59 @@ 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);
if (ev.Role is InnocentRole) return;
assignIcon(player, ev.Role);
}
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;
if (role is DetectiveRole) {
detectiveIcons[player.Slot] = roleIcon;
return;
}
traitors.Add(player.Slot);
traitorIcons[player.Slot] = roleIcon;
}
private void removeAllIcons(CCSPlayerController player) {
removeTraitorIcon(player);
removeDetectiveIcon(player);
}
private void removeTraitorIcon(CCSPlayerController player) {
removeIcons(player.Slot, traitorIcons);
}
private void removeDetectiveIcon(CCSPlayerController player) {
removeIcons(player.Slot, detectiveIcons);
}
private void removeIcons(int slot,
IDictionary<int, IEnumerable<CPointWorldText>> cache) {
cache.Remove(slot, out var icons);
if (icons == null) return;
foreach (var icon in icons) {
if (!icon.IsValid) continue;
icon.Remove();
}
}
[EventHandler(Priority = Priority.MONITOR)]
public void OnDeath(PlayerDeathEvent ev) {
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);
removeAllIcons(gamePlayer);
}
// ReSharper disable once PossiblyImpureMethodCallOnReadonlyVariable

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,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;
@@ -8,7 +9,8 @@ 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();
@@ -16,15 +18,15 @@ public class RoundStartHandler(IServiceProvider provider) : IPluginModule {
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 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,10 +37,36 @@ public class TextSpawner : ITextSpawner {
return [one, two];
}
public IEnumerable<CPointWorldText> CreateTextScreen(TextSetting setting,
CCSPlayerController player) {
var screen = spawnScreen(setting, player);
return [screen];
}
private CPointWorldText spawnScreen(TextSetting setting,
CCSPlayerController player) {
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 inFront = eyes + forward * 50;
var angle = new Angle(localAngle.X, localAngle.Y, 90);
// point angle at player
angle.Y += 180;
angle.Pitch = 90;
var ent = CreateText(setting, inFront,
new QAngle(angle.X, angle.Y, angle.Z));
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()!;
@@ -49,7 +76,7 @@ public class TextSpawner : ITextSpawner {
position.Add(GetRightVector(rotation) * -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;
}

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.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

@@ -3,6 +3,7 @@ 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;
@@ -17,13 +18,16 @@ public class PlayerStatsTracker(IServiceProvider provider) : IListener {
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
public void Dispose() { }
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);
@@ -38,7 +42,9 @@ public class PlayerStatsTracker(IServiceProvider provider) : IListener {
revealedDeaths.Add(gamePlayer.Slot);
}
[EventHandler]
// Needs to be higher so we detect the kill the game ends
// in the case that this is the last player
[EventHandler(Priority = Priority.HIGHER)]
public void OnKill(PlayerDeathEvent ev) {
var killer = ev.Killer == null ? null : converter.GetPlayer(ev.Killer);
var assister =
@@ -78,6 +84,9 @@ public class PlayerStatsTracker(IServiceProvider provider) : IListener {
.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;

View File

@@ -1,4 +1,5 @@
using System.Drawing;
using System.Reactive.Linq;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Entities.Constants;
@@ -8,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()
@@ -33,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) {
@@ -44,12 +39,8 @@ public class RoundTimerListener(IServiceProvider provider) : IListener {
.TotalSeconds);
Server.ExecuteCommand("mp_ignore_round_win_conditions 1");
foreach (var player in Utilities.GetPlayers()
.Where(p => p.LifeState != (int)LifeState_t.LIFE_ALIVE)) {
player.PawnIsAlive = true;
.Where(p => p.LifeState != (int)LifeState_t.LIFE_ALIVE))
player.Respawn();
Utilities.SetStateChanged(player, "CCSPlayerController",
"m_bPawnIsAlive");
}
foreach (var player in Utilities.GetPlayers())
player.SetColor(Color.FromArgb(254, 255, 255, 255));
@@ -71,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

@@ -38,8 +38,6 @@ public class CCPlayerConverter : IPluginModule,
}
public void Dispose() { playerCache.Clear(); }
public string Name => "PlayerConverter";
public string Version => GitVersionInformation.FullSemVer;
public void Start() { }
}

View File

@@ -0,0 +1,65 @@
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) {
player.PawnIsAlive = true;
Utilities.SetStateChanged(player, "CCSPlayerController",
"m_bPawnIsAlive");
return;
}
FakeAlivePlayers.Add(player);
Server.NextWorldUpdate(() => {
var pawn = player.Pawn.Value;
if (pawn == null || !pawn.IsValid) return;
pawn.DeathTime = 0;
Utilities.SetStateChanged(pawn, "CBasePlayerPawn", "m_flDeathTime");
Utilities.SetStateChanged(pawn, "CBasePlayerController", "m_flDeathTime");
Server.NextWorldUpdate(() => {
player.PawnIsAlive = true;
Utilities.SetStateChanged(player, "CCSPlayerController",
"m_bPawnIsAlive");
});
});
}
public void UnspoofAlive(CCSPlayerController player) {
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,3 +1,4 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using TTT.API;
using TTT.API.Player;
@@ -8,48 +9,56 @@ 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;
Server.NextWorldUpdate(() => {
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
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;
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;
});
}
public void RemoveWeapon(IOnlinePlayer player, string weaponId) {
if (!player.IsAlive) return;
Server.NextWorldUpdate(() => {
if (!player.IsAlive) return;
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
var pawn = gamePlayer.PlayerPawn.Value;
var pawn = gamePlayer.Pawn.Value;
if (pawn == null || pawn.WeaponServices == null) return;
if (pawn == null || pawn.WeaponServices == null) return;
var matchedWeapon =
pawn.WeaponServices.MyWeapons.FirstOrDefault(x
=> x.Value?.DesignerName == weaponId);
var matchedWeapon =
pawn.WeaponServices.MyWeapons.FirstOrDefault(x
=> x.Value?.DesignerName == weaponId);
if (matchedWeapon?.Value == null || !matchedWeapon.IsValid) return;
pawn.WeaponServices.ActiveWeapon.Raw = matchedWeapon.Raw;
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>();
// Make them equip the desired weapon
var activeWeaponEntity =
pawn.WeaponServices.ActiveWeapon.Value?.As<CBaseEntity>();
gamePlayer.DropActiveWeapon();
activeWeaponEntity?.AddEntityIOEvent("Kill", activeWeaponEntity);
gamePlayer.DropActiveWeapon();
activeWeaponEntity?.AddEntityIOEvent("Kill", activeWeaponEntity);
});
}
public void RemoveAllWeapons(IOnlinePlayer player) {
if (!player.IsAlive) return;
Server.NextWorldUpdate(() => {
if (!player.IsAlive) return;
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
gamePlayer.RemoveWeapons();
gamePlayer.RemoveWeapons();
});
}
}

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,26 +35,36 @@ 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;
if (value <= 0) {
Player.CommitSuicide(false, true);
return;
}
Player.Pawn.Value.Health = value;
Utilities.SetStateChanged(Player.Pawn.Value, "CBaseEntity", "m_iHealth");
}
@@ -70,21 +82,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 +104,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
}

View File

@@ -0,0 +1,70 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace TTT.CS2.RayTrace.Struct;
/// <summary>
/// Represents the results of a game trace operation, containing information about what was hit.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 0xB8)]
public unsafe struct CGameTrace {
/// <summary>
/// The surface that was hit by the trace.
/// </summary>
[FieldOffset(0x00)]
public IntPtr Surface;
/// <summary>
/// The entity that was hit by the trace.
/// </summary>
[FieldOffset(0x08)]
public IntPtr HitEntity;
/// <summary>
/// Pointer to the hitbox data if a hitbox was hit.
/// </summary>
[FieldOffset(0x10)]
public CTraceHitbox* HitboxData;
/// <summary>
/// The contents at the point of impact.
/// </summary>
[FieldOffset(0x50)]
public uint Contents;
/// <summary>
/// The starting position of the trace.
/// </summary>
[FieldOffset(0x78)]
public Vector3 StartPos;
/// <summary>
/// The end position of the trace.
/// </summary>
[FieldOffset(0x84)]
public Vector3 EndPos;
/// <summary>
/// The surface normal at the point of impact.
/// </summary>
[FieldOffset(0x90)]
public Vector3 Normal;
/// <summary>
/// The exact position where the trace hit.
/// </summary>
[FieldOffset(0x9C)]
public Vector3 Position;
/// <summary>
/// Fraction of the trace completed when the hit occurred (0.0-1.0).
/// </summary>
[FieldOffset(0xAC)]
public float Fraction;
/// <summary>
/// Whether the trace was completely inside a solid (no free space).
/// </summary>
[FieldOffset(0xB6)]
public bool AllSolid;
}

View File

@@ -0,0 +1,154 @@
using System.Runtime.InteropServices;
namespace TTT.CS2.RayTrace.Struct;
/// <summary>
/// Represents a filter used during ray tracing operations to determine which entities should be included or excluded
/// from the trace.
/// This structure closely reflects the trace filter system of the Source 2 engine.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 72)]
public unsafe struct CTraceFilter {
/// <summary>
/// Delegate pointing to the virtual destructor for the filter object.
/// </summary>
public delegate void DestructorDelegate(CTraceFilter* filter);
/// <summary>
/// Delegate used to evaluate whether a specific entity should be hit by the ray trace.
/// </summary>
public delegate bool ShouldHitEntityDelegate(CTraceFilter* filter,
IntPtr entity);
/// <summary>
/// Initializes a new instance of the <see cref="CTraceFilter" /> struct, configuring it to ignore specific entities and
/// owners.
/// </summary>
/// <param name="entityIdToIgnore">The entity ID to exclude from the trace.</param>
/// <param name="ownerId">Optional owner ID to exclude. Default is 0xFFFFFFFF (none).</param>
/// <param name="hierarchyId">Optional hierarchy ID to exclude. Default is 0xFFFF (none).</param>
public CTraceFilter(uint entityIdToIgnore, uint ownerId = 0xFFFFFFFF,
ushort hierarchyId = 0xFFFF) {
Vtable = null;
m_nInteractsWith = 0;
m_nInteractsExclude = 0x20311;
m_nInteractsAs = 0x40000;
m_nOwnerIdsToIgnore[0] = ownerId;
m_nOwnerIdsToIgnore[1] = 0xFFFFFFFF;
m_nEntityIdsToIgnore[0] = entityIdToIgnore;
m_nEntityIdsToIgnore[1] = 0xFFFFFFFF;
m_nHierarchyIds[0] = hierarchyId;
m_nHierarchyIds[1] = 0xFFFF;
m_nObjectSetMask = 7;
m_nCollisionGroup = 4;
m_nBits = 0b01000001;
m_bHitEntities = true;
m_bHitTriggers = true;
m_bTestHitboxes = true;
m_bTraceComplexEntities = false;
m_bOnlyHitIfHasPhysics = false;
m_bIterateEntities = true;
}
/// <summary>
/// Pointer to the virtual function table used internally by the engine.
/// </summary>
[FieldOffset(0x00)]
internal void* Vtable;
/// <summary>
/// Mask of interaction types to include in the trace.
/// </summary>
[FieldOffset(0x08)]
public ulong m_nInteractsWith;
/// <summary>
/// Mask of interaction types to exclude from the trace.
/// </summary>
[FieldOffset(0x10)]
public ulong m_nInteractsExclude;
/// <summary>
/// Mask of interaction types that this object interacts as.
/// </summary>
[FieldOffset(0x18)]
public ulong m_nInteractsAs;
/// <summary>
/// Array of up to two owner IDs to ignore during the trace.
/// </summary>
[FieldOffset(0x20)]
public fixed uint m_nOwnerIdsToIgnore[2];
/// <summary>
/// Array of up to two entity IDs to ignore during the trace.
/// </summary>
[FieldOffset(0x28)]
public fixed uint m_nEntityIdsToIgnore[2];
/// <summary>
/// Array of up to two hierarchy IDs to ignore during the trace.
/// </summary>
[FieldOffset(0x30)]
public fixed ushort m_nHierarchyIds[2];
/// <summary>
/// Bitmask specifying which object sets should be considered.
/// </summary>
[FieldOffset(0x34)]
public byte m_nObjectSetMask;
/// <summary>
/// Collision group the trace belongs to.
/// </summary>
[FieldOffset(0x35)]
public byte m_nCollisionGroup;
/// <summary>
/// Miscellaneous behavior flags encoded as bit flags.
/// </summary>
[FieldOffset(0x36)]
public byte m_nBits;
/// <summary>
/// Specifies whether entities should be included in the trace.
/// </summary>
[FieldOffset(0x37)]
public bool m_bHitEntities;
/// <summary>
/// Specifies whether trigger volumes should be hit.
/// </summary>
[FieldOffset(0x38)]
public bool m_bHitTriggers;
/// <summary>
/// Indicates whether hitboxes should be tested during the trace.
/// </summary>
[FieldOffset(0x39)]
public bool m_bTestHitboxes;
/// <summary>
/// Indicates whether to trace through complex entities such as physics proxies.
/// </summary>
[FieldOffset(0x3A)]
public bool m_bTraceComplexEntities;
/// <summary>
/// If set, only entities with physics will be considered in the trace.
/// </summary>
[FieldOffset(0x3B)]
public bool m_bOnlyHitIfHasPhysics;
/// <summary>
/// Indicates whether the trace system should iterate over entities to apply filtering.
/// </summary>
[FieldOffset(0x3C)]
public bool m_bIterateEntities;
}

View File

@@ -0,0 +1,31 @@
using System.Runtime.InteropServices;
namespace TTT.CS2.RayTrace.Struct;
/// <summary>
/// Represents trace hitbox information including hit group and hitbox ID.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 0x44)]
public struct CTraceHitbox {
/// <summary>
/// The hit group that was hit by the trace.
/// Common values:
/// 0 = Generic
/// 1 = Head
/// 2 = Chest
/// 3 = Stomach
/// 4 = Left Arm
/// 5 = Right Arm
/// 6 = Left Leg
/// 7 = Right Leg
/// </summary>
[FieldOffset(0x38)]
public int HitGroup;
/// <summary>
/// The specific hitbox ID that was hit by the trace.
/// Hitbox IDs are model-specific and correspond to hitboxes defined in the model.
/// </summary>
[FieldOffset(0x40)]
public int HitboxId;
}

View File

@@ -0,0 +1,26 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace TTT.CS2.RayTrace.Struct;
/// <summary>
/// Represents a 3D capsule shape defined by two centers and a radius.
/// Commonly used in collision detection and physics systems.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct Capsule {
/// <summary>
/// The center point of one end of the capsule.
/// </summary>
public Vector3 CenterA;
/// <summary>
/// The center point of the opposite end of the capsule.
/// </summary>
public Vector3 CenterB;
/// <summary>
/// The radius of the capsule.
/// </summary>
public float Radius;
}

View File

@@ -0,0 +1,21 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace TTT.CS2.RayTrace.Struct;
/// <summary>
/// Represents an axis-aligned bounding box (AABB) used for spatial queries and collision detection.
/// Defined by minimum and maximum 3D coordinates.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct Hull {
/// <summary>
/// The minimum corner of the bounding box (usually the lowest x, y, z values).
/// </summary>
public Vector3 Mins;
/// <summary>
/// The maximum corner of the bounding box (usually the highest x, y, z values).
/// </summary>
public Vector3 Maxs;
}

View File

@@ -0,0 +1,21 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace TTT.CS2.RayTrace.Struct;
/// <summary>
/// Represents a cylindrical line used in trace or collision tests.
/// Defined by a starting offset and a radius around the path.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct Line {
/// <summary>
/// The offset from the origin to the start point of the line.
/// </summary>
public Vector3 StartOffset;
/// <summary>
/// The radius of the line used to simulate thickness (e.g., swept sphere or capsule).
/// </summary>
public float Radius;
}

View File

@@ -0,0 +1,32 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace TTT.CS2.RayTrace.Struct;
/// <summary>
/// Represents a 3D mesh consisting of a set of vertices and bounding box information.
/// Often used in complex collision or trace geometry.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct Mesh {
/// <summary>
/// The minimum bounding coordinates (AABB) of the mesh.
/// </summary>
public Vector3 Mins;
/// <summary>
/// The maximum bounding coordinates (AABB) of the mesh.
/// </summary>
public Vector3 Maxs;
/// <summary>
/// Pointer to an array of vertices in memory.
/// Each vertex is typically a Vector3 or a custom vertex format.
/// </summary>
public IntPtr Vertices;
/// <summary>
/// Number of vertices in the mesh.
/// </summary>
public int NumVertices;
}

View File

@@ -0,0 +1,133 @@
using System.Numerics;
using System.Runtime.InteropServices;
using TTT.CS2.RayTrace.Enum;
namespace TTT.CS2.RayTrace.Struct;
/// <summary>
/// Represents a polymorphic ray shape used in spatial queries and collision detection.
/// This structure can represent a line, sphere, hull (AABB), capsule, or mesh—depending on the constructor used.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct Ray {
/// <summary>
/// The ray data interpreted as a line.
/// </summary>
[FieldOffset(0)]
public Line Line;
/// <summary>
/// The ray data interpreted as a sphere.
/// </summary>
[FieldOffset(0)]
public Sphere Sphere;
/// <summary>
/// The ray data interpreted as an axis-aligned bounding box (AABB).
/// </summary>
[FieldOffset(0)]
public Hull Hull;
/// <summary>
/// The ray data interpreted as a capsule.
/// </summary>
[FieldOffset(0)]
public Capsule Capsule;
/// <summary>
/// The ray data interpreted as a mesh with custom vertices.
/// </summary>
[FieldOffset(0)]
public Mesh Mesh;
/// <summary>
/// The active ray shape type, used to determine how the union should be interpreted.
/// </summary>
[FieldOffset(40)]
public RayType Type;
/// <summary>
/// Initializes a ray as a simple line with no radius.
/// </summary>
/// <param name="startOffset">The start offset of the line from the origin.</param>
public Ray(Vector3 startOffset) {
this = default;
Line = new Line { StartOffset = startOffset, Radius = 0f };
Type = RayType.Line;
}
/// <summary>
/// Initializes a ray as a sphere if the radius is positive, otherwise defaults to a line.
/// </summary>
/// <param name="center">Center of the sphere or line.</param>
/// <param name="radius">Radius of the sphere.</param>
public Ray(Vector3 center, float radius) {
this = default;
if (radius > 0f) {
Sphere = new Sphere { Center = center, Radius = radius };
Type = RayType.Sphere;
} else {
Line = new Line { StartOffset = center, Radius = 0f };
Type = RayType.Line;
}
}
/// <summary>
/// Initializes a ray as a hull (AABB) if bounds are not equal, otherwise defaults to a line.
/// </summary>
/// <param name="mins">Minimum bounding box coordinates.</param>
/// <param name="maxs">Maximum bounding box coordinates.</param>
public Ray(Vector3 mins, Vector3 maxs) {
this = default;
if (mins != maxs) {
Hull = new Hull { Mins = mins, Maxs = maxs };
Type = RayType.Hull;
} else {
Line = new Line { StartOffset = mins, Radius = 0f };
Type = RayType.Line;
}
}
/// <summary>
/// Initializes a ray as a capsule if the endpoints are distinct and the radius is positive; otherwise falls back to
/// simpler shapes.
/// </summary>
/// <param name="centerA">First endpoint of the capsule.</param>
/// <param name="centerB">Second endpoint of the capsule.</param>
/// <param name="radius">Radius of the capsule.</param>
public Ray(Vector3 centerA, Vector3 centerB, float radius) {
this = default;
if (centerA != centerB) {
if (radius > 0f) {
Capsule = new Capsule {
CenterA = centerA, CenterB = centerB, Radius = radius
};
Type = RayType.Capsule;
} else {
Line = new Line { StartOffset = centerA, Radius = 0f };
Type = RayType.Line;
}
} else { this = new Ray(centerA, radius); }
}
/// <summary>
/// Initializes a ray as a mesh with custom vertex data.
/// </summary>
/// <param name="mins">Minimum bounding box coordinates of the mesh.</param>
/// <param name="maxs">Maximum bounding box coordinates of the mesh.</param>
/// <param name="vertices">An array of 3D vertices representing the mesh.</param>
public Ray(Vector3 mins, Vector3 maxs, Vector3[] vertices) {
this = default;
unsafe {
fixed (Vector3* ptr = vertices) {
Mesh = new Mesh {
Mins = mins,
Maxs = maxs,
Vertices = (IntPtr)ptr,
NumVertices = vertices.Length
};
Type = RayType.Mesh;
}
}
}
}

View File

@@ -0,0 +1,21 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace TTT.CS2.RayTrace.Struct;
/// <summary>
/// Represents a sphere used for spatial queries or collision detection.
/// Defined by its center position and radius.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct Sphere {
/// <summary>
/// The center point of the sphere in world or local space.
/// </summary>
public Vector3 Center;
/// <summary>
/// The radius of the sphere.
/// </summary>
public float Radius;
}

View File

@@ -30,7 +30,7 @@ public static class RayTrace {
private static readonly nint GameTraceManager = NativeAPI.FindSignature(
Addresses.ServerPath,
OperatingSystem.IsLinux() ? "4C 8D 05 ? ? ? ? BB" : "48 8B 0D ? ? ? ? 0C");
OperatingSystem.IsLinux() ? "4C 8D 0D ? ? ? ? BB" : "48 8B 0D ? ? ? ? 0C");
public static Vector? TraceShape(Vector _origin, QAngle _viewangles,
bool fromPlayer = false) {

View File

@@ -2,6 +2,7 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.Utils;
namespace TTT.CS2.Utils;
@@ -11,6 +12,8 @@ public static class RoundUtil {
TerminateRoundFunc =
new(GameData.GetSignature("CCSGameRules_TerminateRound"));
private static IEnumerable<CCSTeam>? _teamManager;
public static int GetTimeElapsed() {
if (ServerUtil.GameRules == null) return 0;
var freezeTime = ServerUtil.GameRules.FreezeTime;
@@ -45,10 +48,35 @@ public static class RoundUtil {
return rules == null || rules.WarmupPeriod;
}
public static void EndRound(RoundEndReason reason, float delay = 0) {
public static void EndRound(RoundEndReason reason) {
var gameRules = ServerUtil.GameRulesProxy;
if (gameRules == null || gameRules.GameRules == null) return;
// TODO: Figure out what these params do
TerminateRoundFunc.Invoke(gameRules.GameRules.Handle, 5f, reason, 0, 0);
}
public static void SetTeamScore(CsTeam team, int score) {
_teamManager ??=
Utilities.FindAllEntitiesByDesignerName<CCSTeam>("cs_team_manager");
foreach (var entry in _teamManager) {
if (entry.TeamNum != (byte)team) continue;
entry.Score = score;
Utilities.SetStateChanged(entry, "CTeam", "m_iScore");
break;
}
}
public static void AddTeamScore(CsTeam team, int score) {
SetTeamScore(team, GetTeamScore(team) + score);
}
public static int GetTeamScore(CsTeam team) {
_teamManager ??=
Utilities.FindAllEntitiesByDesignerName<CCSTeam>("cs_team_manager");
return (from entry in _teamManager
where entry.TeamNum == (byte)team
select entry.Score).FirstOrDefault();
}
}

View File

@@ -0,0 +1,30 @@
{
"GameTraceManager": {
"signatures": {
"library": "server",
"windows": "48 8B 0D ? ? ? ? 0C",
"linux": "4C 8D 0D ? ? ? ? BB"
}
},
"TraceFunc": {
"signatures": {
"library": "server",
"windows": "4C 8B DC 49 89 5B ? 49 89 6B ? 49 89 73 ? 57 41 56 41 57 48 81 EC",
"linux": "48 B8 ? ? ? ? ? ? ? ? 55 66 0F EF C0 48 89 E5 41 57 41 56 49 89 D6"
}
},
"CTraceFilterVtable": {
"signatures": {
"library": "server",
"windows": "4C 8D 2D ? ? ? ? 24",
"linux": "48 8D 0D ? ? ? ? 66 89 95"
}
},
"TraceShape": {
"signatures": {
"library": "server",
"windows": "48 89 5C 24 ? 48 89 4C 24 ? 55 57",
"linux": "55 48 89 E5 41 57 49 89 CF 41 56 49 89 F6 41 55 4D 89 C5 41 54 49 89 D4 53 4C 89 CB"
}
}
}

View File

@@ -33,21 +33,21 @@ public class CS2Messenger(IServiceProvider provider)
public override void Debug(string msg, params object[] args) {
#if DEBUG
_ = ((IMessenger)this).BackgroundMsgAll(msg,
_ = ((IMessenger)this).BackgroundMsgAll(
$"[DEBUG] {string.Format(msg, args)}");
#endif
}
public override void DebugAnnounce(string msg, params object[] args) {
#if DEBUG
_ = ((IMessenger)this).MessageAll(msg,
_ = ((IMessenger)this).MessageAll(
$"[DEBUG ANNOUNCE] {string.Format(msg, args)}");
#endif
}
public override void DebugInform(string msg, params object[] args) {
#if DEBUG
_ = ((IMessenger)this).ScreenMsgAll(msg,
_ = ((IMessenger)this).ScreenMsgAll(
$"[DEBUG INFORM] {string.Format(msg, args)}");
#endif
}

View File

@@ -1,8 +1,17 @@
using TTT.Locale;
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game;
using TTT.Locale;
namespace TTT.CS2.lang;
public static class CS2Msgs {
public static IMsg ROLE_SPECTATOR
=> MsgFactory.Create(nameof(ROLE_SPECTATOR));
public static IMsg TASER_SCANNED(IPlayer scannedPlayer, IRole role) {
var rolePrefix = GameMsgs.GetRolePrefix(role);
return MsgFactory.Create(nameof(TASER_SCANNED),
rolePrefix + scannedPlayer.Name, role.Name);
}
}

View File

@@ -1 +1,2 @@
ROLE_SPECTATOR: "Spectator"
ROLE_SPECTATOR: "Spectator"
TASER_SCANNED: "%PREFIX%You scanned {0}{grey}, they are %an% {1}{grey}!"

View File

@@ -1,6 +1,6 @@
<Project>
<ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.332"/>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.340"/>
<PackageReference Include="System.Reactive" Version="6.0.1"/>
</ItemGroup>

View File

@@ -1,19 +1,38 @@
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game.Events.Player;
namespace TTT.Game.Actions;
public class DamagedAction(IPlayer victim, IPlayer? attacker, string? weapon,
int damage) : IAction {
public DamagedAction(PlayerDamagedEvent ev) : this(ev.Player, ev.Attacker,
ev.Weapon, ev.DmgDealt) { }
public class DamagedAction(IRoleAssigner roles, IPlayer victim,
IPlayer attacker, string? weapon, int damage) : IAction {
public DamagedAction(IRoleAssigner roles, PlayerDamagedEvent ev) : this(roles,
ev.Player,
ev.Attacker ?? throw new ArgumentNullException(nameof(ev.Attacker),
"Attacker cannot be null"), ev.Weapon, ev.DmgDealt) { }
public string? Weapon { get; } = weapon;
public int Damage { get; } = damage;
public IPlayer Player { get; } = attacker;
public IPlayer? Other { get; } = victim;
public IRole? PlayerRole { get; } = roles.GetRoles(attacker).FirstOrDefault();
public IRole? OtherRole { get; } = roles.GetRoles(victim).FirstOrDefault();
public string Id => "basegame.action.attack";
public string Verb => "damaged";
public string Details => $"for {Damage} damage with {Weapon}";
#region ConstructorAliases
public DamagedAction(IServiceProvider provider, IPlayer victim,
IPlayer attacker, string? weapon, int damage) : this(
provider.GetRequiredService<IRoleAssigner>(), victim, attacker, weapon,
damage) { }
public DamagedAction(IServiceProvider provider, PlayerDamagedEvent ev) : this(
provider.GetRequiredService<IRoleAssigner>(), ev) { }
#endregion
}

View File

@@ -1,18 +1,50 @@
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game.Events.Player;
namespace TTT.Game.Actions;
public class DeathAction(IPlayer victim, IPlayer? killer) : IAction {
public DeathAction(PlayerDeathEvent ev) : this(ev.Player, ev.Killer) {
public class DeathAction(IRoleAssigner roles, IPlayer victim, IPlayer? killer)
: IAction {
public DeathAction(IRoleAssigner roles, PlayerDeathEvent ev) : this(roles,
ev.Player, ev.Killer) {
Details = $"using {ev.Weapon}";
}
public IPlayer Player { get; } = victim;
public IPlayer? Other { get; } = killer;
public IRole? PlayerRole { get; } = roles.GetRoles(victim).FirstOrDefault();
public IRole? OtherRole { get; } = killer is not null ?
roles.GetRoles(killer).FirstOrDefault() :
null;
public string Id { get; } = "basegame.action.death";
public string Verb { get; } = killer is null ? "died" : "was killed by";
public string Verb { get; } = killer is null ? "died" : "killed";
public string Details { get; } = string.Empty;
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 ?
$"{Other}{oRole} {Verb} {Player}{pRole} {Details}" :
$"{Player}{pRole} {Verb} {Details}";
}
#region ConstructorAliases
public DeathAction(IServiceProvider provider, IPlayer victim, IPlayer? killer)
: this(provider.GetRequiredService<IRoleAssigner>(), victim, killer) { }
public DeathAction(IServiceProvider provider, PlayerDeathEvent ev) : this(
provider.GetRequiredService<IRoleAssigner>(), ev) { }
#endregion
}

View File

@@ -1,12 +1,29 @@
using TTT.API.Game;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game.Events.Body;
namespace TTT.Game.Actions;
public class IdentifyBodyAction(BodyIdentifyEvent ev) : IAction {
public class IdentifyBodyAction(IRoleAssigner roles, BodyIdentifyEvent ev)
: IAction {
#region ConstructorAliases
public IdentifyBodyAction(IServiceProvider provider, BodyIdentifyEvent ev) :
this(provider.GetRequiredService<IRoleAssigner>(), ev) { }
#endregion
public IPlayer Player { get; } = ev.Identifier;
public IPlayer? Other { get; } = ev.Body.OfPlayer;
public IRole? PlayerRole { get; } =
roles.GetRoles(ev.Identifier).FirstOrDefault();
public IRole? OtherRole { get; } =
roles.GetRoles(ev.Body.OfPlayer).FirstOrDefault();
public string Id { get; } = "basegame.action.identify_body";
public string Verb { get; } = "identified the body of";
public string Details { get; } = "";

View File

@@ -7,6 +7,8 @@ namespace TTT.Game.Actions;
public class RoleAssignedAction(IPlayer player, IRole role) : IAction {
public IPlayer Player { get; } = player;
public IPlayer? Other => null;
public IRole? PlayerRole { get; } = role;
public IRole? OtherRole { get; } = null;
public string Id => "basegame.action.roleassigned";
public string Verb => "was assigned";
public string Details { get; } = role.Name;

View File

@@ -8,7 +8,7 @@ namespace TTT.Game.Commands;
public class CommandManager(IServiceProvider provider)
: ICommandManager, ITerrorModule {
protected readonly Dictionary<string, ICommand> Commands = new();
protected readonly Dictionary<string, ICommand> cmdMap = new();
protected readonly IMsgLocalizer Localizer =
provider.GetRequiredService<IMsgLocalizer>();
@@ -19,11 +19,11 @@ public class CommandManager(IServiceProvider provider)
protected readonly IServiceProvider Provider = provider;
public virtual bool RegisterCommand(ICommand command) {
return command.Aliases.All(alias => Commands.TryAdd(alias, command));
return command.Aliases.All(alias => cmdMap.TryAdd(alias, command));
}
public bool UnregisterCommand(ICommand command) {
return command.Aliases.All(alias => Commands.Remove(alias));
return command.Aliases.All(alias => cmdMap.Remove(alias));
}
public bool CanExecute(IOnlinePlayer? executor, ICommand command) {
@@ -37,7 +37,7 @@ public class CommandManager(IServiceProvider provider)
var executor = info.CallingPlayer;
if (info.ArgCount == 0) return CommandResult.ERROR;
if (!Commands.TryGetValue(info.Args[0], out var command)) {
if (!cmdMap.TryGetValue(info.Args[0], out var command)) {
info.ReplySync(Localizer[GameMsgs.GENERIC_UNKNOWN(info.Args[0])]);
return CommandResult.UNKNOWN_COMMAND;
}
@@ -64,11 +64,14 @@ public class CommandManager(IServiceProvider provider)
return result;
}
public virtual string Name => "base.commands";
public virtual string Version => GitVersionInformation.FullSemVer;
public ISet<ICommand> Commands => cmdMap.Values.ToHashSet();
public void Dispose() { }
public virtual void Start() { RegisterCommand(new LogsCommand(Provider)); }
public virtual void Start() {
var commands = Provider.GetServices<ICommand>();
foreach (var command in commands) RegisterCommand(command);
}
private void printNoPermission(IOnlinePlayer? executor, ICommand command,
ICommandInfo info) {

View File

@@ -12,23 +12,20 @@ public class LogsCommand(IServiceProvider provider) : ICommand {
public void Dispose() { }
public string Name => "logs";
public string Version => GitVersionInformation.FullSemVer;
public void Start() { }
// TODO: Restrict and verbalize usage
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (!games.IsGameActive()) {
if (games.ActiveGame is not {
State: State.IN_PROGRESS or State.FINISHED
}) {
info.ReplySync("No active game to show logs for.");
return Task.FromResult(CommandResult.ERROR);
}
var game = games.ActiveGame;
if (game == null) {
info.ReplySync("No active game to show logs for.");
return Task.FromResult(CommandResult.ERROR);
}
game.Logger.PrintLogs(executor);
games.ActiveGame.Logger.PrintLogs(executor);
return Task.FromResult(CommandResult.SUCCESS);
}
}

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