Compare commits

...

133 Commits

Author SHA1 Message Date
Isaac
3dcc3a7de5 Item Rebalancing, Karma Updates, New Compass, Cluster Grenade | Bug Fixes (#125)
This PR implements a comprehensive set of game balancing changes, bug fixes, and new features for a Trouble in Terrorist Town (TTT) game mode in Counter-Strike 2.

Key Changes:

    Shop item pricing rebalance: Adjusted prices across multiple traitor and detective items to improve game economy balance
    New cluster grenade item: Added a new traitor shop item that splits into multiple grenades on detonation
    Compass system refactor: Split the single compass into two separate items (player compass and body compass) with a shared abstract base class
    Karma system improvements: Updated karma calculation values and added proper storage/disposal patterns
    Bug fixes: Fixed damage application, ragdoll spawning, and team change handling issues
2025-10-16 13:38:34 -07:00
MSWS
65bcafca79 Extra extra delay 2025-10-16 13:33:12 -07:00
MSWS
6cac535e94 Additional unit testing adjustments 2025-10-16 13:24:06 -07:00
Isaac
ab3dfbda45 Update TTT/CS2/Items/PoisonShots/PoisonShotsListener.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-10-16 13:22:02 -07:00
Isaac
324a19c457 Update TTT/CS2/GameHandlers/BodySpawner.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-10-16 13:21:34 -07:00
Isaac
fda4c72da5 Update TTT/CS2/Items/PoisonShots/PoisonShotsListener.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-10-16 13:21:14 -07:00
Isaac
b0a1959a2e Merge branch 'main' into dev 2025-10-16 13:19:28 -07:00
MSWS
8a18b1df9c Fix failing tests 2025-10-16 13:12:49 -07:00
MSWS
c233258efc feat: Overhaul sound events, fix duplicate body issues +semver:minor
Refactor and Enhance Gameplay Mechanics

- Remove unnecessary type checks and modify logic for handling victim HP in `OneHitKnifeListener`.
- Add configurable properties in `HealthStationConfig` for improved control over healing behavior.
- Enhance `BodySpawner` with optional parameters and improve code readability for ragdoll management.
- Simplify array initialization and item sorting logic in `BuyCommand`.
- Refactor health-related variables and update sound effects in `HealthStation` to standardize behavior.
- Introduce `DealPoisonDamage` method and utility imports in `PlayerExtensions` for improved poison damage handling.
- Implement tracking and handling of poison-related events in `PoisonShotsListener` and `PoisonSmokeListener`.
- Update `DamageStation` with tracking mechanisms and improved health adjustment logic.
- Enhance item search logic in `GiveItemCommand` and introduce `DebugMessage` management in `CS2ServiceCollection`.
- Revise communication consistency in `lang/en.yml` and add `DebugMessage` class for handling debug scenarios.
- Streamline damage handling and weapon verification in `DeagleDamageListener`.
2025-10-16 13:02:22 -07:00
MSWS
e13497af76 refactor: Update item prices, make buy menu print to chat
- Increase the price of the Poison Smoke item in `PoisonSmokeConfig.cs` from 30 to 35 and ensure a newline at the end of the file.
- Reduce the price of the gloves in `GlovesConfig.cs` from 65 to 50.
- Refactor `BuyMenuHandler.cs` to improve command management and handling robustness.
- Reduce the default price of armor in `ArmorConfig.cs` from 80 to 60.
- Adjust prices in various Traitor and Detective configurations, including `C4Config.cs`, `PoisonShotsConfig.cs`, and `StickersConfig.cs` for better game balance.
2025-10-16 11:32:57 -07:00
MSWS
e8ccd2dbf8 Update velocity configs 2025-10-16 11:19:47 -07:00
MSWS
c0e95a2254 feat: Add cluster grenade feature +semver:minor (resolves #124)
```
- Add section for `CHEGrenadeProjectile_CreateFunc` in gamedata for new grenade functionality
- Update BuyMenuHandler to recognize "weapon_hegrenade" alias for purchase process
- Enhance ClusterGrenadeListener to implement multi-projectile detonation logic and remove unused fields
- Introduce `GrenadeDataHelper` class for handling grenade projectile creation
- Extend ClusterGrenadeConfig with `UpForce` and `ThrowForce` properties for customization
```
2025-10-16 11:16:23 -07:00
MSWS
5a9fd9da1a feat: Add Cluster Grenade item and adjust item prices +semver:minor
- Increase the price of the One-Shot Deagle item in CS2OneShotDeagleConfig.cs from 100 to 110.
- Reduce item prices in CamoConfig.cs and Detective/DnaScannerConfig.cs, adjusting the game's economy and item valuation.
- Introduce the Cluster Grenade item in ClusterGrenadeItem.cs and implement its related services.
- Create ClusterGrenadeListener.cs to handle events and dependencies for cluster grenades.
- Add the Cluster Grenade item description in en.yml.
- Reduce the price of HealthStationConfig.cs to improve affordability and balance.
- Update BodyPaintConfig.cs to allow for increased usage flexibility.
- Add new configuration for the traitor shop's Cluster Grenade in Traitor/ClusterGrenadeConfig.cs.
- Add the Cluster Grenade to the shop services and adjust service ordering in ShopServiceCollection.cs.
2025-10-16 10:25:55 -07:00
MSWS
fb562563de Sort items before searching 2025-10-16 10:10:57 -07:00
MSWS
161480c1f1 Fix locale usage (resolves #124) 2025-10-16 10:08:23 -07:00
MSWS
cfdffbdb47 feat: Refactor compass items into a modular system +semver:minor
Implement New Compass Item System and Remove Legacy CompassItem

- Remove `CompassItem.cs`, eliminating the legacy radar-like compass functionality for traitors and any associated gameplay mechanics.
- Introduce `InnoCompassItem` class, providing role-specific target identification and implementing extension methods for service collection integration.
- Add `AbstractCompassItem` class as a base for compass-related items, including periodic updates, abstract methods for target logic, and a visual compass display.
- Split and rename `AddCompassServices` to `AddInnoCompassServices` and `AddBodyCompassServices` in `ShopServiceCollection.cs` for better service management.
- Update `CompassMsgs.cs` by refining message identifiers and adding new instances to support expanded in-game functionalities.
- Enhance `StationItem` object spawning logic for more accurate placement based on player orientation.
- Amend `en.yml` to include "Body Compass" item details and adjust existing labels for clarity and consistency.
- Implement `BodyCompassItem` class, extending abstract compass functionality for detective roles, with methods for ownership checks and target retrieval.
2025-10-16 10:01:40 -07:00
MSWS
70e4127ccf Merge branch 'dev' of github.com:MSWS/TTT into dev 2025-10-16 09:28:05 -07:00
MSWS
5acc57d96e Update to latest CS# 2025-10-16 08:57:26 -07:00
Isaac
a10b83ec4d Update FUNDING 2025-10-15 01:15:45 -07:00
Isaac
0a10cd22ab Update funding information for GitHub Sponsors
Signed-off-by: Isaac <git@msws.xyz>
2025-10-15 01:14:20 -07:00
MSWS
7838e335e4 fix: Avoid failing main pipeline if release already exists 2025-10-15 01:11:52 -07:00
MSWS
3a472bb0bf Reformat & Cleanup 2025-10-15 01:04:03 -07:00
MSWS
1a7943a58e Merge branch 'dev' of github.com:MSWS/TTT into dev 2025-10-15 00:58:29 -07:00
MSWS
b385daf157 Add more M4 shortcuts and AWP shortcut 2025-10-15 00:58:02 -07:00
MSWS
31a1069550 feat: Suppress game stats at all times (resolves #122)
```
- Add JetBrains.Annotations to PlayerStatsTracker and annotate methods with [UsedImplicitly] to enhance code quality and maintainability.
- Reorder logic in CombatHandler's `OnPlayerDeath_Pre` to ensure necessary operations are executed before checking game state.
- Move `info.DontBroadcast` setting in CombatHandler to occur only when game is in progress.
- Modify operation sequence in `OnPlayerDeath_Pre` to spoof player status before dispatching death event.
- Clarify comment in `OnPlayerHurt` method in CombatHandler for better understanding on non-Windows platforms.
```
2025-10-14 18:26:13 -07:00
MSWS
38ef183072 Update logs unit test 2025-10-14 16:41:43 -07:00
MSWS
2c03129e86 Update README 2025-10-14 14:05:54 -07:00
MSWS
6f169ef850 Adjust credit dispersement for good/bad kills and iding bodies 2025-10-14 12:34:58 -07:00
MSWS
6f924a82b0 Fix station spawn positioning (resolves #106) 2025-10-14 11:50:58 -07:00
MSWS
06ae0250d0 Additional tweaks to karma balance 2025-10-14 11:50:03 -07:00
MSWS
bd475edd54 Localize no logs shown msg 2025-10-14 11:43:12 -07:00
MSWS
092a676f97 feat: Implement player muting when dead +semver:minor (resolves #121)
- Introduce `PlayerMuter` class in `GameHandlers` for muting dead players and send appropriate messages
- Add `PlayerMuter` behavior to `CS2ServiceCollection` and organize mod behaviors
- Remove unnecessary debug print and simplify logic in `SilentAWPItem`'s `onWeaponSound` method
- Add reminder message in `en.yml` for dead players indicating they cannot be heard
- Add `DEAD_MUTE_REMINDER` message in `CS2Msgs.cs` to notify muted dead players
2025-10-14 11:41:41 -07:00
MSWS
cebf48a9e6 refactor: Refactor dict to use IDs, fix silent awp (#105)
- Change dictionary key types from `IOnlinePlayer` to `string` in `ListCommand` for consistency, using `executor.Id` as the key.
- Update method calls in `ListCommand` to align with new dictionary key types.
- Update `silentShots` dictionary in `SilentAWPItem` to use player IDs (`string`) instead of `IOnlinePlayer` objects.
- Modify `OnPurchase` method in `SilentAWPItem` to handle weapon management asynchronously.
- Add server logging for debug messages in `SilentAWPItem`.
2025-10-14 11:26:05 -07:00
MSWS
303b6de39c Working MAUL integration 2025-10-14 11:05:11 -07:00
MSWS
9f5e96ce33 Add MAUL compatability 2025-10-14 10:45:23 -07:00
MSWS
83e90deb44 refactor: Rename ChatHandler to TraitorChatHandler +semver:patch
- Update the traitor chat format label to pluralize "TRAITOR" to "TRAITORS" in `en.yml`
- Replace `ChatHandler` with `TraitorChatHandler` in `CS2ServiceCollection.cs` to enhance focus on traitor-specific chat functionality
- Rename `ChatHandler` to `TraitorChatHandler` and update to manage traitor roles in `ChatHandler.cs`
- Ensure message processing occurs only if the game is in progress or finished in `ChatHandler.cs`
- Modify command message handling in `ChatHandler.cs` to strip backslashes from messages
2025-10-14 10:01:25 -07:00
Isaac
658eecef02 feat: Add traitor chat (resolves #112, #114) (#120) 2025-10-14 09:04:42 -07:00
MSWS
c90af8dfcf feat: Implement traitor chat message formatting +semver:minor
- Add new message format function for traitor chat in CS2Msgs.cs
- Update ChatHandler.cs with new API modules and role-checking logic
- Modify onSay method in ChatHandler.cs to support traitor message formatting
- Add new chat format specification for traitors in en.yml
2025-10-14 09:01:03 -07:00
MSWS
6cd1788992 Start workon traitor chat 2025-10-14 08:42:25 -07:00
MSWS
1288ccbd7b Refactor & Reformat, fix Spectators preventing specific roles 2025-10-14 08:33:56 -07:00
MSWS
2596289f58 Fix unit tests 2025-10-14 07:18:37 -07:00
Isaac
59eea4bc6d Simplify perm checks, grammar update +semver:patch (#119) 2025-10-13 22:46:37 -07:00
MSWS
d13403d7a7 Simplify perm checking 2025-10-13 22:44:31 -07:00
MSWS
d4fa621a03 Fix color formatting 2025-10-13 22:16:59 -07:00
Isaac
cf6b42344f Balance Changes, Respawn On Countdown Start (#118)
- Buffed poison shots (3 -> 5 bullets)
- Nerfed healthshot price (25 -> 30 credits)
- Added role indicator in shop list
- Reduced Karma harshness
2025-10-13 21:42:52 -07:00
MSWS
969c872e48 Force logs to be run on main thread 2025-10-13 21:12:59 -07:00
MSWS
b2f19b7ac3 feat: Enhance log viewing with messaging and visibility logic +semver:minor
```
Enhance logging and visibility management in game commands and role handling

- Update `LogsCommand.cs` to integrate messaging and localization with `IMessenger` and `IMsgLocalizer`, and conditionally adjust player visibility using `IIconManager`.
- Add new localized message in `GameMsgs.cs` for player's log viewing activity, using player's name.
- Overload `SetVisiblePlayers` in `IIconManager.cs` to increase flexibility by accepting an `IOnlinePlayer` object.
- Implement `SetVisiblePlayers` in `RoleIconsHandler.cs` to improve player visibility and role management, and integrate rules for specific roles like `DetectiveRole` and `TraitorRole`.
- Modify `en.yml` to include a log message for when a player views logs alive.
```
2025-10-13 21:11:50 -07:00
MSWS
742e407bcf Add shop list footer 2025-10-13 20:55:17 -07:00
MSWS
f8c8e528e2 Buff poison shot amo (resolves #115) 2025-10-13 20:16:21 -07:00
MSWS
24bd3d5f40 Clear recipients of awp sounds 2025-10-13 19:51:49 -07:00
MSWS
f1ff53893f Add karma grants per round 2025-10-13 19:47:15 -07:00
MSWS
54c89e96c0 fix: Respawn players when game actually starts
```
- Improve player respawn logic in RoundTimerListener for better team handling and secure timer disposal.
- Enhance initial setup and end game logic in RoundBasedGame with robust role assignment, game state checks, and localization support.
- Increase default Healthshot item price in HealthshotConfig.
```
2025-10-13 19:19:42 -07:00
MSWS
46514a6016 Change comparator 2025-10-12 19:05:13 -07:00
MSWS
73a8f6a9f5 Restrict logs command behind permission 2025-10-12 18:16:52 -07:00
MSWS
f0c239f08e build: Change DI lifetimes for TExtension and ITerrorModule
- Change dependency injection lifetime for services implementing `TExtension` from scoped to singleton in `ServiceCollectionExtensions.cs`
- Register `ITerrorModule` as a transient service in `ServiceCollectionExtensions.cs`
2025-10-12 18:08:25 -07:00
MSWS
bafad884d9 Dont modify karma if killer is the victim 2025-10-12 17:49:30 -07:00
MSWS
1d7e2f7466 Update compass more frequently 2025-10-12 17:42:15 -07:00
MSWS
f9e3d2d324 perf: Refactor KarmaStorage for thread-safety and reliability
- Replace `Dictionary<IPlayer, int>` with `ConcurrentDictionary<string, int>` in `KarmaStorage.cs` for thread-safe operations.
- Introduce caching logic with semaphore for serialized writes in `KarmaStorage.cs`.
- Add periodic data flushing mechanism to the database with improved error handling in `KarmaStorage.cs`.
- Seal `KarmaStorage` class to prevent inheritance and ensure configuration loading precedes database operations.
- Improve error handling with logging and ensure reliable database connections in `KarmaStorage.cs`.
- Enhance thread safety and performance in `FlushAsync` and `Write` methods, improving idempotency and preventing race conditions.
- Increase robustness with null checks, additional validations, and modular code separation in `KarmaStorage.cs`.
2025-10-12 17:24:19 -07:00
Isaac
9ce90cccaa Additional Shop Items, Synchronous Events (#111) 2025-10-12 14:05:14 -07:00
Isaac
551f6a09ef Update TTT/CS2/Command/PlayerPingShopAlias.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-10-12 13:59:45 -07:00
MSWS
41db8f9444 Additional awaits 2025-10-12 13:54:56 -07:00
MSWS
361bbb0a49 Wait for async event completion 2025-10-12 13:52:17 -07:00
MSWS
228ea40cec feat: Add item sorting and player ping features +semver:minor
```
- Introduce `IItemSorter` interface across multiple components to enhance item sorting capabilities in shop commands.
- Enhance `ListCommand` with caching mechanisms, improved sorting logic, and item formatting adjustments for better performance and usability.
- Implement `PlayerPingShopAlias` for enhanced player interaction, including command listeners and shop command processing tied to player actions.
- Set default price for Silent AWP in `SilentAWPConfig` to standardize item pricing.
- Conduct significant cleanup and optimization in `PoisonShotsListener` to improve gameplay experience and reduce unnecessary debug messages.
```
2025-10-12 13:22:31 -07:00
MSWS
44f7283145 feat: Refactor PlayerDamagedEvent for enhanced accuracy
```
- Increase delay time in KarmaListenerTests to ensure proper karma update processing.
- Change `Event` class from abstract to concrete, and modify `Id` property implementation.
- Adjust CS2GameConfig timing settings for more balanced gameplay.
- Enhance PoisonShotsListener functionality with player health parameters and item removal.
- Make SimpleLogger methods virtual to improve subclass flexibility.
- Implement new logging capabilities in CS2Logger with Serilog integration.
- Enhance GiveItemCommand with new event handling for item purchases.
- Update DeagleTests for accurate simulation of weapon damage.
- Modify PlayerDamagedEvent structure for more precise damage calculations.
- Improve DamageStation logic to align with new damage handling.
- Refactor DamageCanceler for better code organization and cross-platform support.
```
2025-10-12 12:28:08 -07:00
MSWS
1a52daad7c Fix async event handler 2025-10-12 11:56:20 -07:00
MSWS
7d0d32998e feat: Refactor karma configs and add new settings
- Change database connection string constant name for consistency in `CS2KarmaConfig.cs`
- Extend description of minimum karma for clarification of its impact
- Refine descriptions of player actions related to karma for improved clarity
- Rename karma-related constants to generic terms for simplicity
- Introduce configurable warning window for low karma to prevent repeat warnings
- Add configurable karma delta values for in-game actions
- Update Load method to include new karma-related configurations
2025-10-11 20:53:49 -07:00
MSWS
3cda83932e feat: Refactor karma system for configurability
```
- Add detailed XML documentation comments to `KarmaConfig.cs` to improve code understanding and maintainability.
- Remove the default value of `MinKarma` in `KarmaConfig.cs`, making it a mandatory setting.
- Introduce new properties in `KarmaConfig.cs` for handling different karma scenarios in player interactions.
- Add a dependency on `TTT.API.Storage` in `KarmaListener.cs` for loading configurations.
- Replace hardcoded karma values in `KarmaListener.cs` with configurable options, enhancing flexibility and adaptability.
```
2025-10-11 20:50:24 -07:00
MSWS
7ea57d0a9b Reformat & Cleanup 2025-10-11 20:46:16 -07:00
MSWS
839be785f0 refactor: Refactor shop item removal to use generics
- Update `DeagleDamageListener.cs` to enhance type safety and address edge cases related to item removal and friendly fire logic.
- Improve the purchase validation logic in `Stickers.cs` using a type-specific item check.
- Refactor `Shop.cs` to use generic type parameters in item removal methods, enhancing type safety.
- Simplify `IShop.cs` by removing default implementations and focusing on type-based item checks.
- Enhance overall code clarity and maintainability with type-specific method improvements.
2025-10-11 20:42:40 -07:00
MSWS
8f0a273f79 refactor: Enhance EventBus validation and optimize role assign logic
```
- Introduce validation to `EventBus` listener methods to enforce `void` return type and enhance exception messaging for parameter constraints.
- Refactor `RoleAssignCreditor` to simplify execution path by making `OnRoleAssign` synchronous and handling asynchronous operations with `Task.Run`.
```
2025-10-11 20:23:06 -07:00
MSWS
cb6cb442b1 refactor: Refactor event dispatching to be synchronous +semver:minor
- Remove asynchronous calls and convert to synchronous dispatch in multiple files, improving performance and reducing complexity.
- Refactor `RoundBasedGame.cs` to enhance game state management, implement team victory determination, and ensure resource disposal.
- Update `IEventBus.cs` and `EventBus.cs` to change the dispatch method to synchronous operation, altering method return types.
- Modify karma-related tests and storage in `KarmaListenerTests.cs`, `KarmaStorage.cs`, and `MemoryKarmaStorage.cs` to reflect synchronization changes, ensuring correct behavior.
- Refactor `EventModifiedMessenger.cs` to improve message handling by switching to synchronous calls.
- Implement new karma penalty logic in `KarmaListener.cs` for certain actions, adjusting the handling and calculations of karma.
2025-10-11 20:01:40 -07:00
Isaac
cb2a5a8720 feat: Compass Item (resolves #80) (#108) 2025-10-09 18:55:36 -07:00
MSWS
10be465d33 Resolve merge / build issue 2025-10-09 18:17:10 -07:00
Isaac
a0720376d4 Merge branch 'dev' into feat/shop-compass
Signed-off-by: Isaac <git@msws.xyz>
2025-10-09 18:15:58 -07:00
Isaac
f5cb87d92c feat: Add Silent AWP item +semver:minor (resolves #105) (#110)
- Implement Silent AWP item functionality with
`SilentAWPServiceCollection` and `SilentAWPItem` class for `TraitorRole`
- Add Silent AWP shop item and related localized texts in `en.yml`
- Define message constants for Silent AWP in `SilentAWPMsgs.cs` for
internationalization
- Modify `GiveItemCommand.cs` to incorporate `OnPurchase` logic for item
purchases
- Manage player validity in `CS2AliveSpoofer.cs` by removing players
with null handles
- Enhance player detail replies in `IndexCommand.cs`
- Introduce messaging functionality in `BaseItem.cs` and use via
`Messenger` field
- Add Silent AWP service integration in `ShopServiceCollection.cs` and
`Traitor` config in `SilentAWPConfig.cs`
2025-10-09 18:14:57 -07:00
MSWS
bd6c15aca7 feat: Add Silent AWP item and related services +semver:minor
- Implement Silent AWP item functionality with `SilentAWPServiceCollection` and `SilentAWPItem` class for `TraitorRole`
- Add Silent AWP shop item and related localized texts in `en.yml`
- Define message constants for Silent AWP in `SilentAWPMsgs.cs` for internationalization
- Modify `GiveItemCommand.cs` to incorporate `OnPurchase` logic for item purchases
- Manage player validity in `CS2AliveSpoofer.cs` by removing players with null handles
- Enhance player detail replies in `IndexCommand.cs`
- Introduce messaging functionality in `BaseItem.cs` and use via `Messenger` field
- Add Silent AWP service integration in `ShopServiceCollection.cs` and `Traitor` config in `SilentAWPConfig.cs`
2025-10-09 18:07:05 -07:00
MSWS
7e5e34c500 Merge branch 'feat/shop-compass' of github.com:MSWS/TTT into feat/shop-compass 2025-10-09 15:04:21 -07:00
MSWS
8a886a158c Replace center with HTML specific call 2025-10-09 15:04:15 -07:00
MSWS
fc61682669 Merge branch 'dev' into feat/shop-compass 2025-10-08 21:02:54 -07:00
MSWS
d6e4655674 Update licenses 2025-10-08 21:02:40 -07:00
MSWS
c53a584113 Update licenses 2025-10-08 21:02:26 -07:00
MSWS
c56387d6e4 feat: Enhance compass configuration and logic
- Add new configuration fields in CompassConfig.cs to customize compass FOV and length
- Implement maximum range check and refactor angle calculations in CompassItem.cs
- Update distance descriptions in CompassItem.cs for thematic clarity
- Enhance code readability and maintainability in CompassItem.cs through refactoring
2025-10-08 20:53:30 -07:00
MSWS
1c8d1a5dd5 feat: Implement advanced compass and detection features +semver:minor
- Enhance `CompassItem` in `CompassItem.cs` with refined enemy detection, directional compass, and improved documentation.
- Add `TextCompass` utility class in `TextCompass.cs` featuring static method for compass line generation with direction normalization and cardinal placements.
2025-10-08 20:23:14 -07:00
MSWS
340dae1b16 Optimize role/team-based checks 2025-10-08 18:55:16 -07:00
MSWS
eff58ab2f1 Reformat & Cleanup 2025-10-07 20:54:29 -07:00
MSWS
acababeaf5 feat: Add compass itme +semver:minor (resolves #80)
```
Introduce Compass Item for Traitor Role

- Add a new configuration file `CompassConfig.cs` for the traitor-exclusive compass with default settings, including a price of 70 and a maximum range of 10,000.
- Integrate the `Compass` service into the shop by updating `ShopServiceCollection.cs` to support the new item.
- Extend `BaseItem.cs` with additional dependencies for game management, enhancing item functionality.
- Update localization in `en.yml` to include the "Player Compass" item, ensuring descriptions and structure accommodate the new addition.
- Create `CompassMsgs.cs` to manage compass-related messages with `SHOP_ITEM_COMPASS` and `SHOP_ITEM_COMPASS_DESC` for localization.
- Enhance vector handling in `VectorExtensions.cs` by adding nullability checks and support for nullable objects.
- Implement `CompassItem.cs` to support traitor-specific gameplay features like real-time radar updates and enemy tracking.
```
2025-10-07 20:46:32 -07:00
MSWS
f40b8ebef0 test: Fix BalanceClear unit tests relying on karma 2025-10-07 10:10:52 -07:00
MSWS
9a005c209a Cleanup & Reformat 2025-10-07 10:06:34 -07:00
MSWS
36914d01a5 refactor: Move BuyMenu handler into GameHandlers 2025-10-07 09:59:26 -07:00
MSWS
62e48e6c73 update(traitor): Buff hurt stations 2025-10-07 09:55:43 -07:00
MSWS
82a4c47dfb Merge branch 'main' into dev 2025-10-07 09:48:53 -07:00
MSWS
31f02ec7f0 ci: Cleanup release.yml 2025-10-07 09:48:48 -07:00
MSWS
36be427f56 Crash fixes for TeamChangeHandler 2025-10-05 17:17:53 -07:00
MSWS
8709d633b1 Fix one shot revolver improperly handling detective case 2025-10-05 00:36:33 -07:00
MSWS
1c63b3fbcc feat: Implement karma, team changes, and taser rewards
```
- Update PlayerKillListener to modify balance adjustments and simplify kill log messages.
- Add TeamChangeHandler to CS2ServiceCollection for managing team changes.
- Introduce TaseRewarder to ShopServiceCollection, enabling taser damage event handling.
- Make RoleAssignCreditor asynchronous and integrate karma-based credit adjustments.
- Update SpectatorRole assignment logic with new IRoleAssigner dependency.
- Make player parameter nullable in KarmaConfig's MaxKarma method.
- Refactor BuyListener to remove server chat notifications and streamline armor setting.
```
2025-10-04 22:53:11 -07:00
MSWS
f1521fc499 feat: Implement purchase handling and enhance karma update +semver:minor
- Update `KarmaStorage.cs` to increase karma update frequency and integrate Counter Strike features.
- Enhance `CS2ServiceCollection.cs` with new `BuyListener` behavior for handling purchase activities.
- Add `GetArmor` method in `PlayerExtensions.cs` to retrieve player armor and helmet status.
- Implement `BuyListener.cs` to manage player purchases, logging, and inventory adjustments.
2025-10-04 21:57:04 -07:00
MSWS
986f30c34f Remove all buyzones 2025-10-04 20:35:33 -07:00
MSWS
be6d79dcea test: Refactor damage events and enhance debug logging
- Simplify `PlayerDamagedEvent` instantiation by removing an unused parameter in `DeagleTests.cs` and `PlayerActionsTest.cs`.
- Update `HpLeft` calculation in `PlayerDamagedEvent.cs` to use a new formula for determining remaining health, improving damage calculation accuracy.
- Enhance debugging in `PoisonShotsListener.cs` by adding debug logs and null checks for more reliable damage event tracking.
- Refactor tests in `DeagleTests.cs` to verify unique item kill behavior after the first use.
2025-10-04 20:14:26 -07:00
MSWS
87d55aadd2 refactor: Refactor event handling and improve localization. +semver:minor
- Simplify `PlayerDamagedEvent` constructor and update health calculations for clarity and consistency.
- Add context to damage events in `DamageStation.cs` by including weapon property and streamline damage calculation logic.
- Enhance `PoisonShotsListener.cs` with localization support for weapon names, improve poison effect handling, and cleanup code for better maintainability.
2025-10-04 19:53:21 -07:00
MSWS
9efd37063b refactor: Refactor damage logic and event handling
- Refactor `OneHitKnifeListener` to set victim's health directly to zero for simplified one-hit kill logic.
- Add `PlayerDamagedEvent` in `DamageStation` to allow external interference in damage calculation and consider event modifications.
- Streamline `PlayerDamagedEvent` class by refactoring damage calculation logic and removing unnecessary properties and fields.
- Update `PoisonShotsListener` to include dependency on `IEventBus`, enhance poison effects, and dispatch relevant events such as `PlayerDamagedEvent` and `PlayerDeathEvent`.
- Remove `DamageEventTest` class, indicating a refactor or change in damage event management/testing strategy.
2025-10-04 19:04:24 -07:00
MSWS
3381722e7e feat: Enhance event handling and add player tracking features
```
- Update DamageStation.cs to enhance event handling and player interaction:
  - Integrate `IEventBus` for event dispatching.
  - Improve `onInterval` to provide health boosts, handle player deaths, and ensure sound commands are issued post-death event.
- Modify RoundBasedGame.cs to increase class extensibility:
  - Update variable access levels and rename to maintain consistency.
- Adjust OneHitKnife.cs to change service registration approach and extend functionality.
- Enhance DamageCanceler.cs with OS-specific cleanup logic and streamlined damage handling.
- Update OneHitKnifeListener.cs for improved damage calculation and to incorporate JetBrains annotations.
- Simplify station tracking and ownership in StationItem.cs and StationInfo.cs.
- Introduce EmitSoundCommand.cs to expand command module functionality.
```
2025-10-04 18:50:47 -07:00
Isaac
5565c72b5b Add Karma and One-Hit Knife Item (#103) 2025-10-04 09:53:53 -07:00
MSWS
36f9b7a600 Address unit test issues 2025-10-04 09:17:50 -07:00
MSWS
41c7a788d3 refactor: Refactor codebase for cleanup and async improvements
```
Refactor and optimize multiple components

- Remove obsolete attributes and unused dependencies in `IEventBus.cs`, `KarmaListener.cs`, `KarmaSyncer.cs`, and `ICommandManager.cs` to clean up code and improve maintainability.
- Enhance asynchronous event handling in `EventModifiedMessenger.cs` by integrating `await` for dispatch operations, improving consistency in message processing.
- Refine `CombatHandler.cs` logic with improved null-checking and better statistics handling, enhancing robustness during player events.
- Enable caching by default in `KarmaStorage.cs`, and update karmas asynchronously to boost performance.
- Simplify `Game/RoundBasedGame.cs` by removing unnecessary dependencies and improving state management and role assignment logic, enhancing game flow control.
```
2025-10-04 08:56:58 -07:00
MSWS
5d37e5d1ec feat: Add One-Hit Knife to Traitor shop +semver:minor (resolves #99)
- Add new shop item "One-Hit Knife" with description in `en.yml`
- Introduce `OneHitKnifeMsgs` class for handling localization messages related to "One-Hit Knife" item
- Implement `OneHitKnifeListener` class to handle game events for the "One-Hit Knife" item functionality
- Create `OneHitKnife` class, extending the item functionality for the `TraitorRole` with service integration
- Update `ShopServiceCollection.cs` to include "One-Hit Knife" in the item collection and shop services
- Add `OneHitKnifeConfig.cs` to define configuration settings including price and friendly fire options for the "One Hit Knife" feature
2025-10-04 08:40:19 -07:00
MSWS
741f2b8586 feat: Implement karma system enhancement features (resolves #47)
Implement enhanced Karma system functionalities and refactorings

- Update `KarmaConfig.cs` to make properties non-virtual and add immutability with "init" accessors, and introduce new configuration properties to support additional karma functionalities.
- Add `KarmaSyncer` behavior to `CS2ServiceCollection.cs` for managing Karma-related synchronization tasks.
- Enhance `SpectatorRole.cs` with an `OnAssign` method to facilitate role assignments and automatically convert players to spectators when needed.
- Filter player assignments in `RoundBasedGame.cs` to include only specific roles and refine non-traitor calculations to only consider assigned players.
- Introduce a comprehensive `KarmaSyncer` class in `Listeners/KarmaSyncer.cs` for managing Karma synchronization, including event handling for Karma updates and safe operations checks.
- Refactor `CS2KarmaConfig.cs` by simplifying structure, adding descriptive updates, and altering command usages related to Karma configurations.
- Remove `KarmaSyncer.cs` from `GameHandlers` to accommodate code restructuring and potential modality changes for player data management.
- Refactor and expand methods in `KarmaMsgs.cs` to adopt simpler expression-bodied syntax and create structured warning messages.
- Introduce dependency checks and reward temporal conditions in `PeriodicRewarder.cs` to ensure balanced issuance aligns with game progression.
- Enhance `KarmaBanner.cs` with new dependencies, tracking mechanisms, and advanced event handling for role assignments and karma-related notifications.
- Improve `KarmaListener.cs` by changing class inheritance, adding debugging capabilities, and switching to batch processing for Karma updates.
- Extend language file `en.yml` with additional warning messages targeting users with critically low Karma levels.
2025-10-04 08:27:21 -07:00
Isaac
e08cad21b3 Add remaining karma services / implementations (#102) 2025-10-04 07:09:34 -07:00
MSWS
ff4c8e76ff Reformat & Cleanup 2025-10-04 07:05:31 -07:00
MSWS
e8bb8564ad feat: Introduce Karma feature enhancements and .NET 8 update
```
- Update TTT/Plugin/Plugin.csproj to target .NET 8.0 and add Microsoft.Data.Sqlite reference
- Enhance TTT/CS2/CS2ServiceCollection.cs with new KarmaBanner mod behavior
- Add TTT/Karma/lang/KarmaMsgs.cs for Karma language messages handling
- Introduce configuration command in TTT/CS2/Configs/CS2KarmaConfig.cs for low karma actions
- Add old karma value property in TTT/Karma/Events/KarmaUpdateEvent.cs
- Implement KarmaBanner class in TTT/CS2/Listeners/KarmaBanner.cs for monitoring karma updates
- Develop new KarmaCommand class in TTT/Karma/KarmaCommand.cs for retrieving karma values
- Enhance TTT/Karma/KarmaStorage.cs with cache support and better SQL handling
- Introduce NameDisplayer OS check in TTT/CS2/GameHandlers/NameDisplayer.cs
- Add CommandUponLowKarma configuration in TTT/Karma/KarmaConfig.cs
- Register KarmaCommand in TTT/Karma/KarmaServiceCollection.cs
- Create English language file TTT/Karma/lang/en.yml for Karma messages
```
2025-10-04 07:00:08 -07:00
MSWS
58cb208c1d feat: Add Karma configuration and async event handling +semver:minor
```
Implement Karma Configuration and Improve Event Handling

- Add `IStorage<KarmaConfig>` service registration in `CS2ServiceCollection`, enhancing support for karma configurations.
- Make `KarmaUpdateEvent` dispatch asynchronous in `KarmaStorage`, and improve error checking for database connections.
- Introduce `CS2KarmaConfig`, a new configuration class with settings for managing karma, including connection strings and value limits.
- Convert karma properties and methods to virtual in `KarmaConfig`, allowing for potential overrides in derived classes.
```
2025-10-04 06:03:34 -07:00
MSWS
a546a8b22e feat: Migrate to SQLite and cleanup KarmaSyncer logic +semver:minor
- Update `Karma.csproj` to include Microsoft.Data.Sqlite and SQLite packages.
- Refactor `KarmaSyncer.cs` by removing the unused `IPlayerFinder` initialization, adding the `UsedImplicitly` attribute, and streamlining player validation logic.
- Change database backend in `KarmaStorage.cs` from MySQL to SQLite.
- Define a default value for the `DbString` property in `KarmaConfig.cs`.
- Adjust service configuration in `KarmaServiceCollection.cs` by modifying `IKarmaService` registration and introducing `KarmaListener`.
2025-10-04 05:58:49 -07:00
Isaac
05daee24a4 Shop and gameplay expansion: new items, localization, periodic rewards, command refactor (#94)
**Summary**

This PR delivers a substantial TTT update that expands the shop, refines
gameplay flow, and improves player feedback. It adds multiple new items
with CS2-specific behavior, introduces a periodic reward system,
implements configurable sounds and full localization, and cleans up
internal command and event handling.

**Scope**

* 56 commits
* 117 files changed
* +2,518 −301

**Why**

* Broaden traitor and detective toolkits to increase strategic variety.
* Make messaging consistent and translatable.
* Reduce friction in command handling and event wiring.
* Close a set of outstanding issues.

**Highlights**

New items and features

* C4 item with CS2 configuration (resolves #79).
* M4A1 item and storage support (resolves #71).
* Traitor gloves item (resolves #81).
* Camouflage item with async fix.
* Poison shots with enhanced effects and feedback (resolves #75).
* Poison smoke (resolves #74).
* Armor item (resolves #67).
* Taser item (resolves #69).
* Healthshot and role reveal (resolves #98).
* Body Paint item and service registration with default config.
* Periodic reward system for players (resolves #97).
* Credits granted for kills and identifications with tuned penalties.

Player experience

* Configurable sound support.
* Full localization for commands and messages.
* Increased HUD font size and adjusted text positioning.
* Shop prefix and HUD tweaks. Skull hidden in HUD.

Core refactors

* Command execution support on main thread. Command structure
consolidated and clarified.
* Round timer and C4 event handling refactored. Old C4 listener logic
removed.
* Event namespaces and item handling reorganized.
* Name formatting utilities updated.

Stability and fixes

* Shop events now reliably fire.
* Role assignment logging corrected.
* Credits no longer overridden by balance clearer.
* Various cleanup, reformatting, and unused line removals.

Administrative

* Licenses updated for new dependencies and content.
* Multiple merges and resyncs with main to keep branch current.

**Configuration changes**

* New item configs: C4, M4A1, Gloves, Camouflage, Poison Shots, Poison
Smoke, Armor, Taser, Healthshot, Body Paint.
* Added localization keys for new messages, commands, and out-of-ammo
lines.
* Sound configuration is now user configurable.
* Station role behavior updated to support Poison Smoke changes.

**API and plugin touchpoints**

* Storage and plugin modules extended for M4A1Config.
* PlayerKillListener added and registered.
* BodyPaint service registered and applied by default where configured.

**Testing**

* Unit and runtime checks for:

  * Shop purchase flows for each new item.
  * Command execution on main thread and permission checks.
  * Credits earn and clear at round transitions.
  * Poison effects timing, damage application, particles, and feedback.
  * HUD text size and positions across common resolutions.
  * Localization fallback behavior for missing keys.
* Manual CS2 tests for C4 behavior and item equips.
* Verified shop events fire for buy, equip, and use.
2025-10-04 05:50:51 -07:00
MSWS
84230fd231 Improve prop drag line appeareance (resolves #96) 2025-10-04 05:40:10 -07:00
MSWS
fdfc0cc3bd Update phrasing in role reveals 2025-10-04 05:31:01 -07:00
MSWS
7fc0f21fa4 fix: Poison shots carrying over rounds (resolves #101)
```
Enhance poison effect management and resource disposal

- Improve resource management in `PoisonSmokeListener.cs` by ensuring proper disposal of poison smoke timers, clearing the timer list upon disposal, and ensuring termination of damage effects according to configuration settings. Remove unnecessary debug output for cleaner logs.
- Refactor `PoisonShotsListener.cs` to reset poison shot data and improve disposal logic. Enhance poison shot usage management and streamline game mechanics related to poison damage and effects, ensuring a more efficient game experience.
```
2025-10-04 05:28:56 -07:00
MSWS
ed7ad35b85 Address concurrent modification issue 2025-10-04 05:25:55 -07:00
MSWS
e6009dd75a Price tweaks 2025-10-04 05:22:10 -07:00
MSWS
b295fc45a2 Suppress bomb planting notification 2025-10-04 05:02:38 -07:00
MSWS
385f87ad12 Reformat & Cleanup 2025-10-04 03:47:57 -07:00
MSWS
6aedbeb3fb Adjust shop prefix 2025-10-04 03:38:40 -07:00
MSWS
2d078e4dfa Adjust periodic reward 2025-10-04 03:36:54 -07:00
MSWS
ff3dd9563e feat: Add periodic reward system +semver:minor (resolves #97)
```
- Modify `DamageStation.cs` to check `_Config.TotalHealthGiven` before health comparison and improve `onInterval` method for better clarity and robustness.
- Enhance `ShopServiceCollection.cs` by adding `PeriodicRewarder`, introducing periodic rewards functionality.
- Update `StationConfig.cs` by setting `TotalHealthGiven` to 0 and increasing `StationHealth` from 100 to 1000, enhancing station durability.
- Implement `PeriodicRewarder.cs`, a new class for issuing periodic rewards to players using dependency injection and a timed approach.
- Introduce new properties `CreditRewardInterval` and `IntervalRewardAmount` in `ShopConfig.cs` to define frequency and amount of periodic rewards, supporting a new credit accumulation strategy.
```
2025-10-04 03:36:11 -07:00
MSWS
8126dfea21 Hide skull in HUD 2025-10-04 03:20:56 -07:00
MSWS
e158bbbd77 feat: Add Healthshot and role-reveal +semver:minor (resolves #98)
Enhance Game Messaging and Item Functionality

- Update `GameMsgs.cs` to add new messages for role reveal on death and traitor reveal, ensuring consistency with message factory use.
- Add "Health Shot" item to `en.yml`, enhancing shop content with healing capabilities.
- Refine `RoundBasedGame.cs` to improve game state transitions, event handling, and winning team determination.
- Create `HealthshotConfig.cs` for configuring "Health Shot" item specifics, including pricing and weapon identifiers.
- Modify `BuyCommand.cs` to implement main thread execution and enhance item search logic for partial matches.
- Introduce `TraitorBuddyInformer` class to inform traitors of their allies during rounds.
- Expand `OutOfRoundCanceler.cs` logic to allow damage events in finished game states.
- Add `PlayerDeathInformer` class for revealing player killers, enhancing game event transparency.
- Introduce and register `HealthshotItem` within the shop, defining purchase logic and localized messaging.
- Remove redundant debug information from `PoisonSmokeListener.cs`, optimizing poison effect logic.
2025-10-04 02:55:08 -07:00
MSWS
e382302911 Reformat & Cleanup 2025-10-04 00:45:57 -07:00
MSWS
75690ee64b feat: Add Poison Smoke feature and refactor station roles (resolves #74)
Introduce Poison Smoke Item and Enhance Role Configurations

- Create `CS2PoisonSmokeConfig.cs` to configure the new Poison Smoke item, defining parameters like price, damage, and sound effect.
- Update `CS2ServiceCollection.cs` to handle poison smoke configuration using new implementations.
- Add `PoisonSmokeMsgs.cs` to define messages for the Poison Smoke item, facilitating localization.
- Enhance `DamageStation.cs` with role management by excluding `TraitorRole` from damage and adding sound feedback.
- Implement `PoisonSmokeListener.cs` to manage poison smoke events with dependencies and scheduled effects.
- Refactor `StationItem.cs` to introduce a generic role parameter, increasing flexibility.
- Update `HealthStation.cs` to specify the detective role with the generic `StationItem`.
- Introduce new configuration files for the traitor's poison smoke in `Traitor/PoisonSmokeConfig.cs`, detailing item properties.
- Simplify `ListCommand.cs` by adjusting item formatting logic for consistency.
- Update `PoisonShotsListener.cs` to handle refined poison configurations.
- Create `PoisonSmokeItem.cs` to define and manage the Poison Smoke item with dependency injection and role restrictions.
2025-10-04 00:43:53 -07:00
MSWS
dadd7b31a1 feat: Increase font size and adjust text positioning
- Increase default font size in TextSetting.cs for improved visibility
- Adjust world units per pixel in TextSetting.cs for better text scaling
- Modify text hat positioning in TextSpawner.cs relative to player’s rotation
2025-10-03 23:50:17 -07:00
MSWS
697c7f5d6b refactor: Refactor commands and enhance item handling +semver:minor
- Enhance `BuyCommand` with usage guidance and unified query logic
- Improve `ListCommand` with async task handling, dependency injections, and enhanced filtering and sorting based on player roles and game state
- Introduce a shorthand alias for `ShopCommand` balance and add usage property for better command guidance
- Upgrade `RoleIconsHandler` event handling, visibility management, and hot-reload logic
- Implement standard pricing and new properties in Traitor and Detective configuration files; adjust color mapping for health display
- Improve player equality handling by implementing `IEquatable` in `CS2Player` and `IPlayer` classes
2025-10-03 23:38:21 -07:00
MSWS
559718621f feat: Implement main thread command execution support +semver:minor
- Integrate `CounterStrikeSharp.API` in `BuyCommand.cs` for enhanced shop functionality.
- Refactor health color calculation in `HealthStationConfig.cs` to increase intensity as health increases.
- Revamp `CS2CommandManager.cs` with improved synchronous command processing and robust exception handling.
- Add `MustBeOnMainThread` property in `ICommand.cs` to manage command execution context.
- Update `ShopCommand.cs` to refine execution flow and description formatting.
- Revise logical comment and color calculation in `DamageStationConfig.cs` for correctness.
- Enhance clarity in `PlayerKillListener.cs` by refining event handling messages.
2025-10-03 22:59:25 -07:00
MSWS
d84e581392 feat: Localize balance command messages +semver:minor
```
- Update `BalanceCommand.cs` to include message localization and update reply format
- Refactor `Shop.cs` logging methods and simplify item handling
- Enhance `ShopMsgs.cs` with message prefix logic and credit prefix determination
- Update `en.yml` with consistent shop prefix and add new balance display message
```
2025-10-03 22:29:49 -07:00
MSWS
640924d2a2 fix: Fixed credits being overriden by balance clearer
```
- Modify RoundShopClearer to reset balances and items only when the game state is "FINISHED"
- Add debug logging and correct logic in PlayerKillListener to improve event handling
- Comment out obsolete Roles property in TestPlayer
- Enhance debug logging and initialize item lists correctly in Shop
- Update BalanceClearTest with role-related imports and new test for role assignments
```
2025-10-03 22:09:56 -07:00
MSWS
d6da16e537 Register PlayerKillListener 2025-10-03 21:50:24 -07:00
MSWS
c223f3994b feat: Add Taser item +semver:minor (resolves #69)
```
- Integrate Taser functionality into the Shop module by updating the `ShopServiceCollection.cs` to include the Taser item in the shop services.
- Add a new `TaserConfig` file to define default configurations for the taser item in `TTT/ShopAPI/Configs`.
- Update `en.yml` to include localization for the new Taser item with its description.
- Introduce `CS2TaserConfig` for advanced configuration management of taser items within the CS2 module, and integrate it into `CS2ServiceCollection.cs`.
- Implement the `TaserItem` class in `TTT/Shop/Items/Taser` along with the supporting `TaserMsgs` class for handling messages related to the taser in the shop.
```
2025-10-03 21:47:59 -07:00
185 changed files with 3588 additions and 691 deletions

15
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: [msws] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: msws # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -13,10 +13,8 @@ jobs:
auto-release:
runs-on: ubuntu-latest
env:
# Tweak these if you want a different model or style
OPENAI_MODEL: gpt-4o-mini
OPENAI_TEMPERATURE: "0.2"
# Safety: cap how many characters we feed to the model
OPENAI_TEMPERATURE: "0.3"
MAX_CHANGELOG_CHARS: "50000"
steps:
@@ -35,7 +33,31 @@ jobs:
id: gitversion
uses: gittools/actions/gitversion/execute@v4
# Early exit guard: if tag already exists, mark and skip all following steps
- name: Check if tag exists
id: tag_exists
run: |
set -euo pipefail
git fetch --tags --force
TAG="${{ steps.gitversion.outputs.fullSemVer }}"
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Tag ${TAG} already exists locally."
elif git ls-remote --tags origin "refs/tags/${TAG}" | grep -q "refs/tags/${TAG}$"; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Tag ${TAG} already exists on origin."
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "Tag ${TAG} does not exist. Continuing."
fi
# Short-circuit info step for logs
- name: Tag exists, nothing to do
if: steps.tag_exists.outputs.exists == 'true'
run: echo "Release already exists for tag ${{ steps.gitversion.outputs.fullSemVer }}. Exiting successfully."
- name: Build Locale
if: steps.tag_exists.outputs.exists != 'true'
run: |
mkdir -p build/TTT/lang
dotnet restore Locale/Locale.csproj
@@ -43,22 +65,26 @@ jobs:
cp lang/*.json build/TTT/lang
- name: Copy Gamedata
if: steps.tag_exists.outputs.exists != 'true'
run: |
mkdir -p build/TTT/gamedata
cp -r TTT/CS2/gamedata/* build/TTT/gamedata
- name: Publish Plugin
if: steps.tag_exists.outputs.exists != 'true'
run: |
dotnet restore TTT/Plugin/Plugin.csproj
dotnet publish TTT/Plugin/Plugin.csproj --no-restore -c Release -o build/TTT
- name: Zip Artifacts
if: steps.tag_exists.outputs.exists != 'true'
run: |
cd build/TTT
zip -r TTT-${{ steps.gitversion.outputs.fullSemVer }}.zip *
# 2. Get latest tag
- name: Get latest tag
if: steps.tag_exists.outputs.exists != 'true'
id: latest_tag
run: |
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
@@ -67,62 +93,60 @@ jobs:
echo "tag=0.0.0" >> $GITHUB_OUTPUT
fi
# 3. Tag if new version
- name: Create and push new tag
if: steps.gitversion.outputs.fullSemVer != steps.latest_tag.outputs.tag
if: steps.tag_exists.outputs.exists != 'true' && steps.gitversion.outputs.fullSemVer != steps.latest_tag.outputs.tag
run: |
set -euo pipefail
TAG="${{ steps.gitversion.outputs.fullSemVer }}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag ${{ steps.gitversion.outputs.fullSemVer }}
git push origin ${{ steps.gitversion.outputs.fullSemVer }}
if ! git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
git tag "${TAG}"
fi
if git ls-remote --tags origin "refs/tags/${TAG}" | grep -q "refs/tags/${TAG}$"; then
echo "Tag ${TAG} already on origin. Skipping push."
else
git push origin "${TAG}"
fi
# 4. Determine previous relevant tag (lineage-aware)
- name: Determine previous relevant tag
if: steps.tag_exists.outputs.exists != 'true'
id: prev_tag
run: |
set -euo pipefail
branch="${GITHUB_REF_NAME}"
# Use HEAD^ to skip the tag we just created. If no parent, fall back to HEAD.
if git rev-parse --verify -q HEAD^ >/dev/null; then
base_rev="HEAD^"
else
base_rev="HEAD"
fi
# Match stable tags on main and prerelease tags on non-main
if [[ "$branch" == "main" ]]; then
pattern='[0-9]*.[0-9]*.[0-9]*'
else
pattern='[0-9]*.[0-9]*.[0-9]*-*'
fi
# Nearest tag reachable on this lineage, not just "second most recent by date"
prev=$(git describe --tags --abbrev=0 --match "$pattern" --tags "$base_rev" 2>/dev/null || true)
echo "tag=${prev:-0.0.0}" >> "$GITHUB_OUTPUT"
# 5. Generate changelog using local git (no compare API)
- name: Generate changelog
if: steps.tag_exists.outputs.exists != 'true'
run: |
set -euo pipefail
prev="${{ steps.prev_tag.outputs.tag }}"
curr="${{ steps.gitversion.outputs.fullSemVer }}"
# Choose what you want in the raw feed: %s = subject only, %B = full message
GIT_LOG_FORMAT='%B'
if [[ "$prev" == "0.0.0" ]]; then
# First release: whole history to this tag, first-parent to reflect mains narrative
git log --no-merges --format="${GIT_LOG_FORMAT}" --reverse "$curr" > CHANGELOG.md
else
# Strict range between the previous reachable tag and the new tag on this lineage
git log --no-merges --format="${GIT_LOG_FORMAT}" --reverse "$prev..$curr" > CHANGELOG.md
fi
# Fallback in case nothing was captured
if [[ ! -s CHANGELOG.md ]]; then
echo "No commits found between $prev and $curr on first-parent. Using full messages without first-parent filter." >&2
if [[ "$prev" == "0.0.0" ]]; then
@@ -134,10 +158,9 @@ jobs:
cat CHANGELOG.md
# 5b. Rewrite changelog with OpenAI
- name: Rewrite changelog with OpenAI
id: ai_changelog
if: success()
if: steps.tag_exists.outputs.exists != 'true'
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_MODEL: ${{ env.OPENAI_MODEL }}
@@ -146,25 +169,19 @@ jobs:
run: |
set -euo pipefail
# Ensure we have a changelog to work with
if [[ ! -s CHANGELOG.md ]]; then
echo "CHANGELOG.md is empty. Skipping AI rewrite."
echo "skipped=true" >> $GITHUB_OUTPUT
exit 0
fi
# Trim the input to a safe size for token limits
head -c "${MAX_CHANGELOG_CHARS}" CHANGELOG.md > CHANGELOG_RAW.md
# Build the JSON body. We feed system guidance and the raw changelog
# See OpenAI Responses API docs for the schema and output_text helper. :contentReference[oaicite:0]{index=0}
jq -Rs --arg sys "You are an expert release-notes writer. Given a list of changes in various formats (e.g: commits, merges, etc.), write release notes intended for reading by the public, grouping by features, features, and other pertinent groups where appropriate. Do not include a group if it is not necessary / populated. Remove internal ticket IDs and commit hashes unless essential. Merge duplicates. Use imperative, past tense voice voice. Output valid Markdown only." \
jq -Rs --arg sys "You are an expert release-notes writer. Given a list of changes in various formats (e.g: commits, merges, etc.), write release notes intended for reading by the public, grouping by features, features, and other pertinent groups where appropriate. Do not include a group if it is unnecessary. Remove internal ticket IDs and commit hashes unless essential. Merge duplicates. Use imperative, past tense voice with proper prose. Output valid Markdown only." \
--arg temp "${OPENAI_TEMPERATURE}" \
--arg model "${OPENAI_MODEL}" \
'{model:$model, temperature: ($temp|tonumber), input:[{role:"system", content:$sys},{role:"user", content:.}]}' CHANGELOG_RAW.md > request.json
# Call the API
# Basic retry on transient failures
for i in 1 2 3; do
HTTP_CODE=$(curl -sS -w "%{http_code}" -o ai_response.json \
https://api.openai.com/v1/responses \
@@ -181,14 +198,12 @@ jobs:
exit 0
fi
# Prefer output_text if present. Fallback to first text item. :contentReference[oaicite:1]{index=1}
if jq -e '.output_text' ai_response.json >/dev/null; then
jq -r '.output_text' ai_response.json > CHANGELOG.md
else
jq -r '.output[0].content[] | select(.type=="output_text") | .text' ai_response.json | sed '/^[[:space:]]*$/d' > CHANGELOG.md
fi
# If the rewrite somehow produced an empty file, keep the raw one
if [[ ! -s CHANGELOG.md ]]; then
echo "AI returned empty content. Restoring raw changelog."
mv CHANGELOG_RAW.md CHANGELOG.md
@@ -200,8 +215,8 @@ jobs:
echo "Rewritten changelog:"
cat CHANGELOG.md
# 6. Create release using the (possibly rewritten) changelog
- name: Create GitHub release
if: steps.tag_exists.outputs.exists != 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.gitversion.outputs.fullSemVer }}
@@ -211,9 +226,8 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 7. Cleanup old pre-releases
- name: Delete old pre-releases
if: github.ref_name != 'main'
if: steps.tag_exists.outputs.exists != 'true' && github.ref_name != 'main'
run: |
gh release list --limit 100 --json name,isPrerelease \
--jq '.[] | select(.isPrerelease) | .name' | tail -n +11 | \

View File

@@ -4,12 +4,14 @@
| CounterStrikeSharp.API | 1.0.340 | Expression | GPL-3.0-only | https://licenses.nuget.org/GPL-3.0-only | | Roflmuffin | http://docs.cssharp.dev/ |
| Dapper | 2.1.66 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | 2019 Stack Exchange, Inc. | Sam Saffron,Marc Gravell,Nick Craver | https://github.com/DapperLib/Dapper |
| JetBrains.Annotations | 2025.2.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) 2016-2025 JetBrains s.r.o. | JetBrains | https://www.jetbrains.com/help/resharper/Code_Analysis__Code_Annotations.html |
| Microsoft.Data.Sqlite | 9.0.9 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://docs.microsoft.com/dotnet/standard/data/sqlite/ |
| Microsoft.Extensions.DependencyInjection.Abstractions | 9.0.7 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://dot.net/ |
| Microsoft.Extensions.Localization.Abstractions | 8.0.3 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://asp.net/ |
| Microsoft.NET.Test.Sdk | 17.14.1 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://github.com/microsoft/vstest |
| Microsoft.Reactive.Testing | 6.0.1 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) .NET Foundation and Contributors. | .NET Foundation and Contributors | https://github.com/dotnet/reactive |
| Microsoft.Testing.Extensions.CodeCoverage | 17.14.2 | Unknown | | https://aka.ms/deprecateLicenseUrl | © Microsoft Corporation. All rights reserved. | Microsoft | https://github.com/microsoft/codecoverage |
| MySqlConnector | 2.4.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright 20162024 Bradley Grainger | Bradley Grainger | https://mysqlconnector.net/ |
| SQLite | 3.13.0 | Unknown | | | Public Domain | SQLite Development Team | |
| System.Reactive | 6.0.1 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) .NET Foundation and Contributors. | .NET Foundation and Contributors | https://github.com/dotnet/reactive |
| System.Text.Json | 8.0.5 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://dot.net/ |
| Xunit.DependencyInjection | 10.6.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright © 2019 | Wei Peng | https://github.com/pengweiqhca/Xunit.DependencyInjection/tree/main/src/Xunit.DependencyInjection |

View File

@@ -43,7 +43,7 @@ public partial class StringLocalizer : IMsgLocalizer {
private LocalizedString getString(string name, params object[] arguments) {
// Get the localized value
string value;
try { value = localizer[name].Value; } catch (NullReferenceException e) {
try { value = localizer[name].Value; } catch (NullReferenceException) {
return new LocalizedString(name, name, true);
}

View File

@@ -14,8 +14,8 @@ survive while eliminating the traitors among them.
- [X] Traitors
- [X] Detectives
- [X] Innocents
- [ ] Shop
- [ ] Karma
- [X] Shop
- [X] Karma
- [ ] Statistics
## Versioning

View File

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

View File

@@ -12,7 +12,6 @@ public interface ICommandManager {
/// 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,5 +1,5 @@
namespace TTT.API.Events;
public abstract class Event {
public abstract string Id { get; }
public class Event {
public virtual string Id => GetType().Name.ToLowerInvariant();
}

View File

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

View File

@@ -24,7 +24,7 @@ public static class ServiceCollectionExtensions {
collection.AddTransient<ICommand>(provider
=> (provider.GetRequiredService<TExtension>() as ICommand)!);
collection.AddScoped<TExtension>();
collection.AddSingleton<TExtension>();
collection.AddTransient<ITerrorModule, TExtension>(provider
=> provider.GetRequiredService<TExtension>());

View File

@@ -33,9 +33,6 @@ public interface IGame : IDisposable {
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() {
return Players.OfType<IOnlinePlayer>().Where(p => p.IsAlive).ToHashSet();
}

View File

@@ -9,9 +9,4 @@ 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

@@ -11,6 +11,7 @@ public interface IIconManager {
void RevealToAll(int client);
void AddVisiblePlayer(int client, int player);
void RemoveVisiblePlayer(int client, int player);
void SetVisiblePlayers(IOnlinePlayer online, ulong playersBitmask);
void ClearAllVisibility();
}

View File

@@ -1,6 +1,6 @@
namespace TTT.API.Player;
public interface IPlayer {
public interface IPlayer : IEquatable<IPlayer> {
/// <summary>
/// The unique identifier for the player, should
/// be unique across all players at all times.
@@ -8,4 +8,9 @@ public interface IPlayer {
string Id { get; }
string Name { get; }
bool IEquatable<IPlayer>.Equals(IPlayer? other) {
if (other is null) return false;
return Id == other.Id;
}
}

View File

@@ -15,6 +15,12 @@
<ProjectReference Include="..\ShopAPI\ShopAPI.csproj"/>
</ItemGroup>
<ItemGroup>
<Reference Include="MAULActainShared.dll">
<HintPath>./ThirdParties/Binaries/MAULActainShared.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Folder Include="RayTrace\"/>
</ItemGroup>

View File

@@ -1,4 +1,5 @@
using CounterStrikeSharp.API;
using TTT.API.Game;
using TTT.API.Player;
using TTT.Game.Loggers;
@@ -12,4 +13,8 @@ public class CS2Logger(IServiceProvider provider) : SimpleLogger(provider) {
public override void PrintLogs(IOnlinePlayer? player) {
Server.NextWorldUpdate(() => base.PrintLogs(player));
}
public override void LogAction(IAction action) {
Server.NextWorldUpdate(() => base.LogAction(action));
}
}

View File

@@ -21,6 +21,7 @@ using TTT.CS2.lang;
using TTT.CS2.Listeners;
using TTT.CS2.Player;
using TTT.Game;
using TTT.Karma;
using TTT.Locale;
namespace TTT.CS2;
@@ -34,6 +35,8 @@ public static class CS2ServiceCollection {
collection.AddModBehavior<ICommandManager, CS2CommandManager>();
collection.AddModBehavior<IAliveSpoofer, CS2AliveSpoofer>();
collection.AddModBehavior<IIconManager, RoleIconsHandler>();
collection.AddModBehavior<NameDisplayer>();
collection.AddModBehavior<PlayerPingShopAlias>();
// Configs
collection.AddModBehavior<IStorage<TTTConfig>, CS2GameConfig>();
@@ -42,6 +45,10 @@ public static class CS2ServiceCollection {
.AddModBehavior<IStorage<OneShotDeagleConfig>, CS2OneShotDeagleConfig>();
collection.AddModBehavior<IStorage<C4Config>, CS2C4Config>();
collection.AddModBehavior<IStorage<M4A1Config>, CS2M4A1Config>();
collection.AddModBehavior<IStorage<TaserConfig>, CS2TaserConfig>();
collection
.AddModBehavior<IStorage<PoisonSmokeConfig>, CS2PoisonSmokeConfig>();
collection.AddModBehavior<IStorage<KarmaConfig>, CS2KarmaConfig>();
// TTT - CS2 Specific optionals
collection.AddScoped<ITextSpawner, TextSpawner>();
@@ -52,8 +59,13 @@ public static class CS2ServiceCollection {
collection.AddModBehavior<DamageCanceler>();
collection.AddModBehavior<PlayerConnectionsHandler>();
collection.AddModBehavior<PropMover>();
// collection.AddModBehavior<RoundEnd_GameEndHandler>();
collection.AddModBehavior<RoundStart_GameStartHandler>();
collection.AddModBehavior<BombPlantSuppressor>();
collection.AddModBehavior<MapZoneRemover>();
collection.AddModBehavior<BuyMenuHandler>();
collection.AddModBehavior<TeamChangeHandler>();
collection.AddModBehavior<TraitorChatHandler>();
collection.AddModBehavior<PlayerMuter>();
// Damage Cancelers
collection.AddModBehavior<OutOfRoundCanceler>();
@@ -66,10 +78,13 @@ public static class CS2ServiceCollection {
collection.AddModBehavior<PlayerStatsTracker>();
collection.AddModBehavior<RoundTimerListener>();
collection.AddModBehavior<ScreenColorApplier>();
collection.AddModBehavior<KarmaBanner>();
collection.AddModBehavior<KarmaSyncer>();
// Commands
#if DEBUG
collection.AddModBehavior<TestCommand>();
collection.AddModBehavior<DebugMessage>();
#endif
collection.AddScoped<IGameManager, CS2GameManager>();

View File

@@ -4,9 +4,10 @@ using CounterStrikeSharp.API.Modules.Commands;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Command;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.Game;
using TTT.Game.Commands;
using TTT.Game.lang;
namespace TTT.CS2.Command;
@@ -17,6 +18,9 @@ public class CS2CommandManager(IServiceProvider provider)
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IMessenger messenger = provider
.GetRequiredService<IMessenger>();
private BasePlugin? plugin;
public void Start(BasePlugin? basePlugin, bool hotReload) {
@@ -40,20 +44,46 @@ public class CS2CommandManager(IServiceProvider provider)
var wrapper = executor == null ?
null :
converter.GetPlayer(executor) as IOnlinePlayer;
Task.Run(async () => {
try {
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(() => {
messenger.Debug($"Received command: {cs2Info.Args[0]} from {wrapper?.Id}");
if (cmdMap.TryGetValue(cs2Info.Args[0], out var command))
if (command.MustBeOnMainThread) {
processCommandSync(cs2Info, wrapper);
return;
}
Task.Run(async () => await processCommandAsync(cs2Info, wrapper));
}
private async Task<CommandResult> processCommandAsync(CS2CommandInfo cs2Info,
IOnlinePlayer? wrapper) {
try {
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.CommandString}\" by {wrapper?.Id}");
Console.WriteLine(e);
});
return CommandResult.ERROR;
}
}
private void processCommandSync(CS2CommandInfo cs2Info,
IOnlinePlayer? wrapper) {
try { _ = ProcessCommand(cs2Info); } catch (Exception e) {
var msg = e.Message;
cs2Info.ReplySync(Localizer[GameMsgs.GENERIC_ERROR(msg)]);
Server.NextWorldUpdateAsync(() => {
Console.WriteLine(
$"Encountered an error when processing command: \"{cs2Info.CommandString}\" by {wrapper?.Id}");
Console.WriteLine(e);
});
return CommandResult.ERROR;
}
});
})
.Wait();
}
}
}

View File

@@ -0,0 +1,55 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Commands;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API;
using TTT.API.Command;
using TTT.API.Player;
namespace TTT.CS2.Command;
public class PlayerPingShopAlias(IServiceProvider provider) : IPluginModule {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IItemSorter itemSorter =
provider.GetRequiredService<IItemSorter>();
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
plugin?.AddCommandListener("player_ping", onPlayerPing, HookMode.Post);
for (var i = 0; i < 10; i++) {
var index = i; // Capture variable
plugin?.AddCommand($"css_{index}", "",
(player, _) => { onButton(player, index); });
}
}
private HookResult onPlayerPing(CCSPlayerController? player,
CommandInfo commandInfo) {
if (player == null) return HookResult.Continue;
var gamePlayer = converter.GetPlayer(player) as IOnlinePlayer;
var cmdInfo =
new CS2CommandInfo(provider, gamePlayer, 0, "css_shop", "list");
cmdInfo.CallingContext = CommandCallingContext.Chat;
provider.GetRequiredService<ICommandManager>().ProcessCommand(cmdInfo);
return HookResult.Continue;
}
private void onButton(CCSPlayerController? player, int index) {
if (player == null) return;
if (converter.GetPlayer(player) is not IOnlinePlayer gamePlayer) return;
var lastUpdated = itemSorter.GetLastUpdate(gamePlayer);
if (lastUpdated == null
|| DateTime.Now - lastUpdated > TimeSpan.FromSeconds(20))
return;
var cmdInfo = new CS2CommandInfo(provider, gamePlayer, 0, "css_shop", "buy",
(index - 1).ToString());
cmdInfo.CallingContext = CommandCallingContext.Chat;
provider.GetRequiredService<ICommandManager>().ProcessCommand(cmdInfo);
}
}

View File

@@ -0,0 +1,21 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Command;
using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class CreditsCommand(IServiceProvider provider) : ICommand {
private readonly IShop shop = provider.GetRequiredService<IShop>();
public void Dispose() { }
public void Start() { }
public string Id => "credits";
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
shop.AddBalance(executor, 1000);
info.ReplySync("You have been given 1000 credits!");
return Task.FromResult(CommandResult.SUCCESS);
}
}

View File

@@ -0,0 +1,33 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class EmitSoundCommand(IServiceProvider provider) : ICommand {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public string Id => "emitsound";
public void Dispose() { }
public void Start() { }
public string[] Usage => ["[sound]"];
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
var gamePlayer = converter.GetPlayer(executor);
if (gamePlayer == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
if (info.Args.Length < 2) return Task.FromResult(CommandResult.PRINT_USAGE);
Server.NextWorldUpdate(() => {
gamePlayer.EmitSound(info.Args[1]);
info.ReplySync("Emitted sound " + info.Args[1]);
});
return Task.FromResult(CommandResult.SUCCESS);
}
}

View File

@@ -1,17 +1,19 @@
using CounterStrikeSharp.API;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Events;
using TTT.API.Command;
using TTT.API.Events;
using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class GiveItemCommand(IServiceProvider provider) : ICommand {
private readonly IShop shop = provider.GetRequiredService<IShop>();
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
private readonly IShop shop = provider.GetRequiredService<IShop>();
public void Dispose() { }
public void Start() { }
@@ -44,6 +46,10 @@ public class GiveItemCommand(IServiceProvider provider) : ICommand {
target = result;
}
var purchaseEv = new PlayerPurchaseItemEvent(target, item);
provider.GetRequiredService<IEventBus>().Dispatch(purchaseEv);
if (purchaseEv.IsCanceled) return;
shop.GiveItem(target, item);
info.ReplySync($"Gave item '{item.Name}' to {target.Name}.");
});
@@ -52,7 +58,8 @@ public class GiveItemCommand(IServiceProvider provider) : ICommand {
private IShopItem? searchItem(string query) {
var item = shop.Items.FirstOrDefault(it
=> it.Name.Equals(query, StringComparison.OrdinalIgnoreCase));
=> it.Name.Replace(" ", "")
.Equals(query, StringComparison.OrdinalIgnoreCase));
if (item != null) return item;

View File

@@ -1,15 +1,10 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class IndexCommand(IServiceProvider provider) : ICommand {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public class IndexCommand : ICommand {
public string Id => "index";
public void Dispose() { }
@@ -21,7 +16,8 @@ public class IndexCommand(IServiceProvider provider) : ICommand {
Server.NextWorldUpdate(() => {
foreach (var player in Utilities.GetPlayers())
info.ReplySync($"{player.PlayerName} - {player.Slot}");
info.ReplySync(
$"{player.PlayerName} - {player.Slot} {player.Index} {player.DraftIndex} {player.PawnCharacterDefIndex}");
});
return Task.FromResult(CommandResult.SUCCESS);

View File

@@ -21,9 +21,11 @@ public class TestCommand(IServiceProvider provider) : ICommand, IPluginModule {
subCommands.Add("state", new StateCommand(provider));
subCommands.Add("screencolor", new ScreenColorCommand(provider));
subCommands.Add("giveitem", new GiveItemCommand(provider));
subCommands.Add("index", new IndexCommand(provider));
subCommands.Add("index", new IndexCommand());
subCommands.Add("showicons", new ShowIconsCommand(provider));
subCommands.Add("sethealth", new SetHealthCommand());
subCommands.Add("emitsound", new EmitSoundCommand(provider));
subCommands.Add("credits", new CreditsCommand(provider));
}
public Task<CommandResult>

View File

@@ -11,7 +11,7 @@ namespace TTT.CS2.Configs;
public class CS2GameConfig : IStorage<TTTConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_ROUND_COUNTDOWN = new(
"css_ttt_round_countdown", "Time to wait before starting a round", 10,
"css_ttt_round_countdown", "Time to wait before starting a round", 15,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 60));
public static readonly FakeConVar<int> CV_MINIMUM_PLAYERS = new(
@@ -80,7 +80,7 @@ public class CS2GameConfig : IStorage<TTTConfig>, IPluginModule {
new ItemValidator(allowMultiple: true));
public static readonly FakeConVar<int> CV_TIME_BETWEEN_ROUNDS = new(
"css_ttt_time_between_rounds", "Time to wait between rounds in seconds", 5,
"css_ttt_time_between_rounds", "Time to wait between rounds in seconds", 1,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 60));
public void Dispose() { }

View File

@@ -0,0 +1,115 @@
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.Karma;
namespace TTT.CS2.Configs;
public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
public static readonly FakeConVar<string> CV_DB_STRING = new(
"css_ttt_karma_db_string", "Database connection string for Karma storage",
"Data Source=karma.db");
public static readonly FakeConVar<int> CV_MIN_KARMA = new("css_ttt_karma_min",
"Minimum possible Karma value; falling below executes the low-karma command",
0, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 1000));
public static readonly FakeConVar<int> CV_DEFAULT_KARMA = new(
"css_ttt_karma_default", "Default Karma assigned to new or reset players",
50, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 1000));
public static readonly FakeConVar<string> CV_LOW_KARMA_COMMAND = new(
"css_ttt_karma_low_command",
"Command executed when a player's karma falls below the minimum (use {0} for player slot)",
"css_ban #{0} 2880 Low Karma");
public static readonly FakeConVar<int> CV_TIMEOUT_THRESHOLD = new(
"css_ttt_karma_timeout_threshold",
"Minimum Karma before timing a player out for KarmaRoundTimeout rounds", 20,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 1000));
public static readonly FakeConVar<int> CV_ROUND_TIMEOUT = new(
"css_ttt_karma_round_timeout",
"Number of rounds a player is timed out for after falling below threshold",
4, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 100));
public static readonly FakeConVar<int> CV_WARNING_WINDOW_HOURS = new(
"css_ttt_karma_warning_window_hours",
"Time window (in hours) preventing repeat warnings for low karma", 24,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 168));
// Karma deltas
public static readonly FakeConVar<int> CV_INNO_ON_TRAITOR = new(
"css_ttt_karma_inno_on_traitor",
"Karma gained when Innocent kills a Traitor", 4, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(-50, 50));
public static readonly FakeConVar<int> CV_TRAITOR_ON_DETECTIVE = new(
"css_ttt_karma_traitor_on_detective",
"Karma gained when Traitor kills a Detective", 1, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(-50, 50));
public static readonly FakeConVar<int> CV_INNO_ON_INNO_VICTIM = new(
"css_ttt_karma_inno_on_inno_victim",
"Karma gained or lost when Innocent kills another Innocent who was a victim",
-1, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-50, 50));
public static readonly FakeConVar<int> CV_INNO_ON_INNO = new(
"css_ttt_karma_inno_on_inno",
"Karma lost when Innocent kills another Innocent", -5,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-50, 50));
public static readonly FakeConVar<int> CV_TRAITOR_ON_TRAITOR = new(
"css_ttt_karma_traitor_on_traitor",
"Karma lost when Traitor kills another Traitor", -6, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(-50, 50));
public static readonly FakeConVar<int> CV_INNO_ON_DETECTIVE = new(
"css_ttt_karma_inno_on_detective",
"Karma lost when Innocent kills a Detective", -8, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(-50, 50));
public static readonly FakeConVar<int> CV_KARMA_PER_ROUND = new(
"css_ttt_karma_per_round",
"Amount of karma a player will gain at the end of each round", 2,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 50));
public static readonly FakeConVar<int> CV_KARMA_PER_ROUND_WIN = new(
"css_ttt_karma_per_round_win",
"Amount of karma a player will gain at the end of each round if their team won",
4, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 50));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<KarmaConfig?> Load() {
var cfg = new KarmaConfig {
DbString = CV_DB_STRING.Value,
MinKarma = CV_MIN_KARMA.Value,
DefaultKarma = CV_DEFAULT_KARMA.Value,
CommandUponLowKarma = CV_LOW_KARMA_COMMAND.Value,
KarmaTimeoutThreshold = CV_TIMEOUT_THRESHOLD.Value,
KarmaRoundTimeout = CV_ROUND_TIMEOUT.Value,
KarmaWarningWindow = TimeSpan.FromHours(CV_WARNING_WINDOW_HOURS.Value),
KarmaPerRound = CV_KARMA_PER_ROUND.Value,
KarmaPerRoundWin = CV_KARMA_PER_ROUND_WIN.Value,
INNO_ON_TRAITOR = CV_INNO_ON_TRAITOR.Value,
TRAITOR_ON_DETECTIVE = CV_TRAITOR_ON_DETECTIVE.Value,
INNO_ON_INNO_VICTIM = CV_INNO_ON_INNO_VICTIM.Value,
INNO_ON_INNO = CV_INNO_ON_INNO.Value,
TRAITOR_ON_TRAITOR = CV_TRAITOR_ON_TRAITOR.Value,
INNO_ON_DETECTIVE = CV_INNO_ON_DETECTIVE.Value
};
return Task.FromResult<KarmaConfig?>(cfg);
}
}

View File

@@ -0,0 +1,38 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs;
using TTT.API;
using TTT.API.Storage;
using TTT.CS2.Validators;
namespace TTT.CS2.Configs;
public class CS2TaserConfig : IStorage<TaserConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_taser_price", "Price of the Taser item", 100,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<string> CV_WEAPON = new(
"css_ttt_shop_taser_weapon", "Weapon entity name used for the Taser",
"weapon_taser", 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<TaserConfig?> Load() {
var cfg = new TaserConfig {
Price = CV_PRICE.Value, Weapon = CV_WEAPON.Value
};
return Task.FromResult<TaserConfig?>(cfg);
}
}

View File

@@ -11,7 +11,7 @@ namespace TTT.CS2.Configs.ShopItems;
public class CS2C4Config : IStorage<C4Config>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new("css_ttt_shop_c4_price",
"Price of the C4 item", 140, ConVarFlags.FCVAR_NONE,
"Price of the C4 item", 130, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<string> CV_WEAPON = new(

View File

@@ -11,7 +11,7 @@ namespace TTT.CS2.Configs.ShopItems;
public class CS2M4A1Config : IStorage<M4A1Config>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_m4a1_price", "Price of the M4A1 item", 90,
"css_ttt_shop_m4a1_price", "Price of the M4A1 item", 75,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<string> CV_CLEAR_SLOTS = new(

View File

@@ -12,7 +12,7 @@ namespace TTT.CS2.Configs.ShopItems;
public class CS2OneShotDeagleConfig : IStorage<OneShotDeagleConfig>,
IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_onedeagle_price", "Price of the One-Shot Deagle item", 120,
"css_ttt_shop_onedeagle_price", "Price of the One-Shot Deagle item", 110,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<bool> CV_FRIENDLY_FIRE = new(

View File

@@ -0,0 +1,67 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs.Traitor;
using TTT.API;
using TTT.API.Storage;
using TTT.CS2.Validators;
namespace TTT.CS2.Configs.ShopItems;
public class CS2PoisonSmokeConfig : IStorage<PoisonSmokeConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_poisonsmoke_price", "Price of the Poison Smoke item", 45,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<string> CV_WEAPON = new(
"css_ttt_shop_poisonsmoke_weapon",
"Weapon entity name used for the Poison Smoke item", "weapon_smokegrenade",
ConVarFlags.FCVAR_NONE, new ItemValidator(allowMultiple: false));
// Poison effect sub-config
public static readonly FakeConVar<int> CV_POISON_TICK_DAMAGE = new(
"css_ttt_shop_poisonsmoke_poison_damage_per_tick",
"Damage dealt per poison tick", 15, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(1, 100));
public static readonly FakeConVar<int> CV_POISON_TOTAL_DAMAGE = new(
"css_ttt_shop_poisonsmoke_poison_total_damage",
"Total damage dealt over the poison duration", 500, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(1, 1000));
public static readonly FakeConVar<int> CV_POISON_TICK_INTERVAL = new(
"css_ttt_shop_poisonsmoke_poison_tick_interval",
"Milliseconds between each poison damage tick", 500, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(100, 10000));
public static readonly FakeConVar<string> CV_POISON_SOUND = new(
"css_ttt_shop_poisonsmoke_poison_sound",
"Sound played when poison deals damage",
"sounds/player/player_damagebody_03");
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<PoisonSmokeConfig?> Load() {
var poison = new PoisonConfig {
TimeBetweenDamage =
TimeSpan.FromMilliseconds(CV_POISON_TICK_INTERVAL.Value),
DamagePerTick = CV_POISON_TICK_DAMAGE.Value,
TotalDamage = CV_POISON_TOTAL_DAMAGE.Value,
PoisonSound = CV_POISON_SOUND.Value
};
var cfg = new PoisonSmokeConfig {
Price = CV_PRICE.Value, Weapon = CV_WEAPON.Value, PoisonConfig = poison
};
return Task.FromResult<PoisonSmokeConfig?>(cfg);
}
}

View File

@@ -2,6 +2,7 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.UserMessages;
using CounterStrikeSharp.API.Modules.Utils;
namespace TTT.CS2.Extensions;
@@ -67,14 +68,24 @@ public static class PlayerExtensions {
var pawn = player.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid) return;
pawn.ArmorValue = armor;
if (withHelmet) {
if (withHelmet)
if (pawn.ItemServices != null)
new CCSPlayer_ItemServices(pawn.ItemServices.Handle).HasHelmet = true;
}
Utilities.SetStateChanged(pawn, "CCSPlayerPawn", "m_ArmorValue");
}
public static (int, bool) GetArmor(this CCSPlayerController player) {
if (!player.IsValid) return (0, false);
var pawn = player.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid) return (0, false);
var hasHelmet = false;
if (pawn.ItemServices != null)
hasHelmet = new CCSPlayer_ItemServices(pawn.ItemServices.Handle)
.HasHelmet;
return (pawn.ArmorValue, hasHelmet);
}
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) {
@@ -97,4 +108,19 @@ public static class PlayerExtensions {
color.R | color.G << 8 | color.B << 16 | color.A << 24);
fadeMsg.Send(player);
}
public static void DealPoisonDamage(this CCSPlayerController player,
int damage) {
if (player.Pawn.Value == null) return;
player.AddHealth(-damage);
player.PlayerPawn.Value?.EmitSound("Player.DamageBody.Onlooker",
OTHERS(player.Slot), 0.2f, 1);
player.PlayerPawn.Value?.EmitSound("Player.DamageBody.Victim",
SELF(player.Slot), 0.2f, 1);
}
private static RecipientFilter SELF(int slot) => new(slot);
private static RecipientFilter OTHERS(int slot)
=> new(ulong.MaxValue & ~(1ul << slot));
}

View File

@@ -9,7 +9,7 @@ public static class VectorExtensions {
return MathF.Sqrt(vec.DistanceSquared(other));
}
public static float DistanceSquared(this Vector vec, Vector other) {
public static float DistanceSquared(this Vector? vec, Vector other) {
return (vec.X - other.X) * (vec.X - other.X)
+ (vec.Y - other.Y) * (vec.Y - other.Y)
+ (vec.Z - other.Z) * (vec.Z - other.Z);

View File

@@ -1,17 +1,34 @@
using System.Reactive.Linq;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.CS2.Roles;
using TTT.CS2.Utils;
using TTT.Game;
using TTT.Game.lang;
using TTT.Game.Events.Game;
using TTT.Game.Roles;
namespace TTT.CS2.Game;
public class CS2Game(IServiceProvider provider) : RoundBasedGame(provider) {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public override State State {
set {
var ev = new GameStateUpdateEvent(this, value);
Bus.Dispatch(ev);
if (ev.IsCanceled) return;
state = value;
}
get => state;
}
public override IActionLogger Logger { get; } = new CS2Logger(provider);
public override IList<IRole> Roles { get; } = [
@@ -59,4 +76,13 @@ public class CS2Game(IServiceProvider provider) : RoundBasedGame(provider) {
return timer;
}
override protected ISet<IOnlinePlayer> GetParticipants() {
var players = Utilities.GetPlayers()
.Where(p => p is { Team: CsTeam.Terrorist or CsTeam.CounterTerrorist });
return players.Select(p => converter.GetPlayer(p))
.OfType<IOnlinePlayer>()
.ToHashSet();
}
}

View File

@@ -6,17 +6,18 @@ using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.CS2.Extensions;
using TTT.Game.Events.Body;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
using TTT.Game.Roles;
namespace TTT.CS2.GameHandlers;
public class BodySpawner(IServiceProvider provider) : IPluginModule {
public class BodySpawner(IServiceProvider provider) : BaseListener(provider) {
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly IPlayerConverter<CCSPlayerController> converter =
@@ -25,31 +26,39 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
public void Dispose() { }
public void Start() { }
[UsedImplicitly]
[EventHandler]
public void OnLeave(PlayerLeaveEvent ev) {
if (games.ActiveGame is not { State: State.IN_PROGRESS }) return;
spawnRagdoll(ev.Player);
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnDeath(EventPlayerDeath ev, GameEventInfo _) {
if (games.ActiveGame is not { State: State.IN_PROGRESS })
return HookResult.Continue;
var player = ev.Userid;
if (player == null || !player.IsValid) return HookResult.Continue;
[EventHandler]
public void OnDeath(PlayerDeathEvent ev) {
if (games.ActiveGame is not { State: State.IN_PROGRESS }) return;
spawnRagdoll(ev.Player, ev.Killer, ev.Weapon);
}
private void spawnRagdoll(IPlayer apiPlayer, IOnlinePlayer? killer = null,
string? weapon = null) {
var player = converter.GetPlayer(apiPlayer);
if (player == null || !player.IsValid) return;
player.SetColor(Color.FromArgb(0, 255, 255, 255));
var ragdollBody = makeGameRagdoll(player);
var body = new CS2Body(ragdollBody, converter.GetPlayer(player));
if (ev.Attacker != null && ev.Attacker.IsValid)
body.WithKiller(converter.GetPlayer(ev.Attacker));
if (killer != null) body.WithKiller(killer);
body.WithWeapon(new BaseWeapon(ev.Weapon));
body.WithWeapon(new BaseWeapon(weapon ?? "unknown"));
var bodyCreatedEvent = new BodyCreateEvent(body);
bus.Dispatch(bodyCreatedEvent);
if (bodyCreatedEvent.IsCanceled) ragdollBody.AcceptInput("Kill");
return HookResult.Continue;
if (bodyCreatedEvent.IsCanceled) {
ragdollBody.AcceptInput("Kill");
}
}
[UsedImplicitly]

View File

@@ -0,0 +1,17 @@
using CounterStrikeSharp.API.Core;
using TTT.API;
namespace TTT.CS2.GameHandlers;
public class BombPlantSuppressor : IPluginModule {
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
plugin?.HookUserMessage(322, um => {
um.Recipients.Clear();
return HookResult.Handled;
});
}
}

View File

@@ -0,0 +1,65 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Commands;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Command;
using TTT.API.Player;
using TTT.CS2.Command;
using TTT.CS2.Extensions;
using TTT.Game.Roles;
namespace TTT.CS2.GameHandlers;
public class BuyMenuHandler(IServiceProvider provider) : IPluginModule {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IInventoryManager inventory =
provider.GetRequiredService<IInventoryManager>();
private readonly Dictionary<string, string> shopAliases = new() {
{ "item_assaultsuit", "Armor" },
{ "item_kevlar", "Armor" },
{ "weapon_taser", "Taser" },
{ "weapon_deagle", "Revolver" },
{ "weapon_smokegrenade", "Poison Smoke" },
{ "weapon_m4a1_silencer", "M4A1" },
{ "weapon_usp_silencer", "M4A1" },
{ "weapon_sg556", "M4A1" },
{ "weapon_mp5sd", "M4A1" },
{ "weapon_decoy", "healthshot" },
{ "weapon_awp", "AWP" },
{ "weapon_hegrenade", "Cluster" }
};
public void Dispose() { }
public void Start() { }
[UsedImplicitly]
[GameEventHandler(HookMode.Pre)]
public HookResult OnPurchase(EventItemPurchase ev, GameEventInfo info) {
if (ev.Userid == null) return HookResult.Continue;
if (converter.GetPlayer(ev.Userid) is not IOnlinePlayer player)
return HookResult.Continue;
if (ev.Weapon is "item_assaultsuit" or "item_kevlar") {
var user = ev.Userid;
user.SetArmor(0);
}
inventory.RemoveWeapon(player, new BaseWeapon(ev.Weapon));
if (!shopAliases.TryGetValue(ev.Weapon, out var alias))
return HookResult.Continue;
var commandManager = provider.GetRequiredService<ICommandManager>();
var newInfo = new CS2CommandInfo(provider, player, 0, "css_shop", "buy",
alias);
newInfo.CallingContext = CommandCallingContext.Chat;
commandManager.ProcessCommand(newInfo);
return HookResult.Handled;
}
}

View File

@@ -36,19 +36,18 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
[UsedImplicitly]
[GameEventHandler(HookMode.Pre)]
public HookResult OnPlayerDeath_Pre(EventPlayerDeath ev, GameEventInfo info) {
if (games.ActiveGame is not { State: State.IN_PROGRESS })
return HookResult.Continue;
var player = ev.Userid;
if (player == null) return HookResult.Continue;
var deathEvent = new PlayerDeathEvent(converter, ev);
Server.NextWorldUpdateAsync(() => bus.Dispatch(deathEvent));
info.DontBroadcast = true;
hideAndTrackStats(ev, player);
if (games.ActiveGame is not { State: State.IN_PROGRESS })
return HookResult.Continue;
info.DontBroadcast = true;
spoofer.SpoofAlive(player);
Server.NextWorldUpdateAsync(() => bus.Dispatch(deathEvent));
return HookResult.Continue;
}
@@ -62,11 +61,16 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
}
var killerStats = ev.Attacker?.ActionTrackingServices?.MatchStats;
if (killerStats == null) return;
killerStats.Kills -= 1;
killerStats.Damage -= ev.DmgHealth;
if (ev.Attacker != null) {
Utilities.SetStateChanged(ev.Attacker,
"CCSPlayerController_ActionTrackingServices",
"m_pActionTrackingServices");
if (killerStats == null) return;
killerStats.Kills -= 1;
killerStats.Damage -= ev.DmgHealth;
if (ev.Attacker.ActionTrackingServices != null)
ev.Attacker.ActionTrackingServices.NumRoundKills--;
Utilities.SetStateChanged(ev.Attacker, "CCSPlayerController",
"m_pActionTrackingServices");
ev.FireEventToClient(ev.Attacker);

View File

@@ -15,7 +15,11 @@ public class DamageCanceler(IServiceProvider provider) : IPluginModule {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public void Dispose() { }
public void Dispose() {
if (OperatingSystem.IsWindows()) return;
VirtualFunctions.CBaseEntity_TakeDamageOldFunc.Unhook(onTakeDamage,
HookMode.Pre);
}
public void Start() { }
@@ -29,14 +33,10 @@ public class DamageCanceler(IServiceProvider provider) : IPluginModule {
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;
var info = hook.GetParam<CTakeDamageInfo>(1);
info.Damage = damagedEvent.DmgDealt;
return HookResult.Continue;
}
}

View File

@@ -1,4 +1,5 @@
using TTT.API.Events;
using JetBrains.Annotations;
using TTT.API.Events;
using TTT.API.Game;
using TTT.CS2.Utils;
using TTT.Game.Events.Player;
@@ -8,10 +9,11 @@ namespace TTT.CS2.GameHandlers.DamageCancelers;
public class OutOfRoundCanceler(IServiceProvider provider)
: BaseListener(provider) {
[UsedImplicitly]
[EventHandler]
public void OnHurt(PlayerDamagedEvent ev) {
if (RoundUtil.IsWarmup()) return;
if (Games.ActiveGame is not { State: State.IN_PROGRESS })
if (Games.ActiveGame is not { State: State.IN_PROGRESS or State.FINISHED })
ev.IsCanceled = true;
}
}

View File

@@ -0,0 +1,25 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.UserMessages;
using TTT.API;
namespace TTT.CS2.GameHandlers;
public class DebugMessage : IPluginModule {
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
for (var i = 0; i < 10000; i++) {
if (i is 325 or 124) continue;
var j = i;
plugin.HookUserMessage(i, msg => debug(msg, j));
}
}
private HookResult debug(UserMessage native, int id) {
Server.PrintToConsole(id + "");
Server.PrintToConsole(native.DebugString);
return HookResult.Continue;
}
}

View File

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

View File

@@ -0,0 +1,40 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using JetBrains.Annotations;
using TTT.API;
namespace TTT.CS2.GameHandlers;
public class MapZoneRemover : IPluginModule {
private BasePlugin? plugin;
private bool zonesRemoved;
public void Dispose() {
plugin?.RemoveListener<CounterStrikeSharp.API.Core.Listeners.OnMapStart>(
onMapStart);
}
public void Start() { }
public void Start(BasePlugin? pluginParent) {
if (pluginParent != null) plugin = pluginParent;
plugin?.RegisterListener<CounterStrikeSharp.API.Core.Listeners.OnMapStart>(
onMapStart);
}
private void onMapStart(string mapName) { zonesRemoved = false; }
[UsedImplicitly]
[GameEventHandler]
public HookResult OnRoundStart(EventRoundStart _, GameEventInfo _2) {
if (zonesRemoved) return HookResult.Continue;
var buyzones =
Utilities.FindAllEntitiesByDesignerName<CBuyZone>("func_buyzone");
foreach (var zone in buyzones) zone.Remove();
zonesRemoved = true;
return HookResult.Continue;
}
}

View File

@@ -0,0 +1,35 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Timers;
using TTT.API;
using TTT.CS2.Extensions;
using TTT.CS2.RayTrace.Class;
using TTT.CS2.RayTrace.Enum;
namespace TTT.CS2.GameHandlers;
public class NameDisplayer : IPluginModule {
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
if (OperatingSystem.IsWindows()) return;
plugin?.AddTimer(0.25f, showNames, TimerFlags.REPEAT);
}
private void showNames() {
foreach (var player in Utilities.GetPlayers()) {
if (player.GetHealth() <= 0) continue;
var target = player.GetGameTraceByEyePosition(TraceMask.MaskSolid,
Contents.NoDraw, player);
if (target == null) continue;
if (!target.Value.HitPlayer(out var hit)) continue;
if (hit == null) continue;
player.PrintToCenterAlert($"{hit.PlayerName}");
}
}
}

View File

@@ -0,0 +1,55 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.CS2.lang;
using TTT.Locale;
namespace TTT.CS2.GameHandlers;
public class PlayerMuter(IServiceProvider provider) : IPluginModule {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IMsgLocalizer locale =
provider.GetRequiredService<IMsgLocalizer>();
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
plugin
?.RegisterListener<CounterStrikeSharp.API.Core.Listeners.OnClientVoice>(
onVoice);
}
private void onVoice(int playerSlot) {
var player = Utilities.GetPlayerFromSlot(playerSlot);
if (player == null) return;
if (player.Pawn.Value is { Health: > 0 }) return;
if ((player.VoiceFlags & VoiceFlags.Muted) != VoiceFlags.Muted) {
var apiPlayer = converter.GetPlayer(player);
messenger.Message(apiPlayer, locale[CS2Msgs.DEAD_MUTE_REMINDER]);
}
player.VoiceFlags |= VoiceFlags.Muted;
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnSpawn(EventPlayerSpawn ev, GameEventInfo _) {
var player = ev.Userid;
if (player == null) return HookResult.Continue;
player.VoiceFlags &= ~VoiceFlags.Muted;
return HookResult.Continue;
}
}

View File

@@ -187,12 +187,11 @@ public class PropMover(IServiceProvider provider) : IPluginModule {
targetVector.Z = Math.Max(targetVector.Z, playerOrigin.Z - 48);
if (ent.AbsOrigin == null) return;
var lerpedVector = ent.AbsOrigin.Lerp(targetVector, 0.3f);
if (info.Beam != null && info.Beam.IsValid) {
info.Beam.AcceptInput("Kill");
info.Beam = createBeam(playerOrigin.With(z: playerOrigin.Z - 16),
lerpedVector);
ent.AbsOrigin);
}
playersPressingE[player] = info;
@@ -201,9 +200,9 @@ public class PropMover(IServiceProvider provider) : IPluginModule {
private CEnvBeam? createBeam(Vector start, Vector end) {
var beam = Utilities.CreateEntityByName<CEnvBeam>("env_beam");
if (beam == null) return null;
beam.RenderMode = RenderMode_t.kRenderTransColor;
beam.Width = 0.5f;
beam.Render = Color.White;
beam.RenderMode = RenderMode_t.kRenderTransAlpha;
beam.Width = 2.0f;
beam.Render = Color.FromArgb(32, Color.White);
beam.EndPos.X = end.X;
beam.EndPos.Y = end.Y;
beam.EndPos.Z = end.Z;

View File

@@ -69,6 +69,12 @@ public class RoleIconsHandler(IServiceProvider provider)
visibilities[client] &= ~(1UL << player);
}
public void SetVisiblePlayers(IOnlinePlayer online, ulong playersBitmask) {
var gamePlayer = players.GetPlayer(online);
if (gamePlayer == null || !gamePlayer.IsValid) return;
SetVisiblePlayers(gamePlayer.Slot, playersBitmask);
}
public void ClearAllVisibility() {
Array.Clear(visibilities, 0, visibilities.Length);
}
@@ -77,6 +83,7 @@ public class RoleIconsHandler(IServiceProvider provider)
plugin
?.RegisterListener<CounterStrikeSharp.API.Core.Listeners.CheckTransmit>(
onTransmit);
if (hotReload) OnRoundEnd(null!, null!);
}
[UsedImplicitly]

View File

@@ -39,4 +39,18 @@ public class RoundStart_GameStartHandler(IServiceProvider provider)
game?.Start(config.RoundCfg.CountDownDuration);
return HookResult.Continue;
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnWarmupEnd(EventWarmupEnd ev, GameEventInfo _1) {
if (games.ActiveGame is { State: State.IN_PROGRESS or State.COUNTDOWN })
return HookResult.Continue;
var count = finder.GetOnline().Count;
if (count < config.RoundCfg.MinimumPlayers) return HookResult.Continue;
var game = games.CreateGame();
game?.Start(config.RoundCfg.CountDownDuration);
return HookResult.Continue;
}
}

View File

@@ -0,0 +1,74 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Commands;
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.Game.Events.Player;
namespace TTT.CS2.GameHandlers;
public class TeamChangeHandler(IServiceProvider provider) : IPluginModule {
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
plugin?.AddCommandListener("jointeam", onJoinTeam);
}
private HookResult onJoinTeam(CCSPlayerController? player,
CommandInfo commandInfo) {
CsTeam requestedTeam;
if (int.TryParse(commandInfo.GetArg(1), out var teamIndex))
requestedTeam = (CsTeam)teamIndex;
else
requestedTeam = commandInfo.GetArg(1).ToLower() switch {
"ct" or "counterterrorist" or "counter" => CsTeam.CounterTerrorist,
"t" or "terrorist" => CsTeam.Terrorist,
"s" or "spec" or "spectator" or "spectators" => CsTeam.Spectator,
_ => CsTeam.None
};
if (games.ActiveGame is not { State: State.IN_PROGRESS }) {
if (player != null && player.LifeState != (int)LifeState_t.LIFE_ALIVE)
Server.NextWorldUpdate(player.Respawn);
return HookResult.Continue;
}
if (requestedTeam is CsTeam.CounterTerrorist or CsTeam.Terrorist)
if (player != null && player.Team is CsTeam.Spectator or CsTeam.None)
return HookResult.Continue;
return HookResult.Handled;
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnChangeTeam(EventPlayerTeam ev, GameEventInfo _) {
if (ev.Userid == null) return HookResult.Continue;
var team = (CsTeam)ev.Team;
if (team is not (CsTeam.Spectator or CsTeam.None))
return HookResult.Continue;
var apiPlayer = converter.GetPlayer(ev.Userid);
Server.NextWorldUpdate(() => {
var playerDeath = new PlayerDeathEvent(apiPlayer);
bus.Dispatch(playerDeath);
});
return HookResult.Continue;
}
}

View File

@@ -0,0 +1,85 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Commands;
using MAULActainShared.plugin;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Game;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.API.Role;
using TTT.CS2.lang;
using TTT.CS2.ThirdParties.eGO;
using TTT.Game.Roles;
using TTT.Locale;
namespace TTT.CS2.GameHandlers;
public class TraitorChatHandler(IServiceProvider provider) : IPluginModule {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IGameManager game =
provider.GetRequiredService<IGameManager>();
private readonly IMsgLocalizer locale =
provider.GetRequiredService<IMsgLocalizer>();
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
private IActain? maulService;
public void Start(BasePlugin? plugin) {
try {
maulService ??= EgoApi.MAUL.Get();
if (maulService != null) {
maulService.getChatShareService().OnChatShare += OnOnChatShare;
return;
}
plugin?.AddCommandListener("say_team", onSay);
} catch (KeyNotFoundException) {
plugin?.AddCommandListener("say_team", onSay);
}
}
public void Dispose() {
if (maulService != null)
maulService.getChatShareService().OnChatShare -= OnOnChatShare;
}
public void Start() { }
private void OnOnChatShare(CCSPlayerController? player, CommandInfo info,
ref bool canceled) {
if (!info.GetArg(0).Equals("say_team", StringComparison.OrdinalIgnoreCase))
return;
var result = onSay(player, info);
if (result == HookResult.Handled) canceled = true;
}
private HookResult onSay(CCSPlayerController? player,
CommandInfo commandInfo) {
if (player == null
|| game.ActiveGame is not { State: State.IN_PROGRESS or State.FINISHED }
|| converter.GetPlayer(player) is not IOnlinePlayer apiPlayer
|| !roles.GetRoles(apiPlayer).Any(r => r is TraitorRole))
return HookResult.Continue;
var teammates = game.ActiveGame?.Players.Where(p
=> roles.GetRoles(p).Any(r => r is TraitorRole))
.ToList();
if (teammates == null) return HookResult.Continue;
var msg = commandInfo.ArgString;
if (msg.StartsWith('"') && msg.EndsWith('"') && msg.Length >= 2)
msg = msg[1..^1];
var formatted = locale[CS2Msgs.TRAITOR_CHAT_FORMAT(apiPlayer, msg)];
foreach (var mate in teammates) messenger.Message(mate, formatted);
return HookResult.Handled;
}
}

View File

@@ -8,7 +8,7 @@ public class TextSetting {
public float depthOffset = 0.0f;
public bool enabled = true;
public string fontName = "Arial";
public float fontSize = 50;
public float fontSize = 64;
public bool fullbright = true;
public PointWorldTextJustifyHorizontal_t horizontal =
@@ -23,5 +23,5 @@ public class TextSetting {
public PointWorldTextJustifyVertical_t vertical =
PointWorldTextJustifyVertical_t.POINT_WORLD_TEXT_JUSTIFY_VERTICAL_CENTER;
public float worldUnitsPerPx = 0.4f;
public float worldUnitsPerPx = 0.5f;
}

View File

@@ -72,7 +72,7 @@ public class TextSpawner : ITextSpawner {
position.Add(new Vector(0, 0, 72));
rotation = new QAngle(rotation.X, rotation.Y + yRot, rotation.Z + 90);
position.Add(rotation.ToRight() * -10);
position.Add(rotation.ToRight() * 5);
var ent = CreateText(setting, position, rotation);
ent.AcceptInput("SetParent", player.Pawn.Value, null, "!activator");

View File

@@ -22,13 +22,13 @@ public class ArmorItem(IServiceProvider provider) : BaseItem(provider) {
.GetAwaiter()
.GetResult() ?? new ArmorConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public override string Name => Locale[ArmorMsgs.SHOP_ITEM_ARMOR];
public override string Description => Locale[ArmorMsgs.SHOP_ITEM_ARMOR_DESC];
public override ShopItemConfig Config => config;
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public override void OnPurchase(IOnlinePlayer player) {
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;

View File

@@ -5,7 +5,6 @@ using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game.Roles;
using TTT.Shop.Items.Traitor.BodyPaint;
namespace TTT.CS2.Items.BodyPaint;

View File

@@ -2,7 +2,6 @@ using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Events;
using TTT.API.Events;
using TTT.API.Player;
using TTT.API.Storage;
@@ -10,23 +9,23 @@ using TTT.CS2.API;
using TTT.CS2.Extensions;
using TTT.Game.Events.Body;
using TTT.Game.Listeners;
using TTT.Shop.Items.Traitor.BodyPaint;
namespace TTT.CS2.Items.BodyPaint;
public class BodyPaintListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly IBodyTracker bodies =
provider.GetRequiredService<IBodyTracker>();
private readonly BodyPaintConfig config =
provider.GetService<IStorage<BodyPaintConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new BodyPaintConfig();
private readonly Dictionary<IPlayer, int> uses = new();
private readonly IShop shop = provider.GetRequiredService<IShop>();
private readonly IBodyTracker bodies =
provider.GetRequiredService<IBodyTracker>();
private readonly Dictionary<IPlayer, int> uses = new();
[UsedImplicitly]
[EventHandler(Priority = Priority.HIGH)]

View File

@@ -1,6 +1,6 @@
using TTT.Locale;
namespace TTT.Shop.Items.Traitor.BodyPaint;
namespace TTT.CS2.Items.BodyPaint;
public class BodyPaintMsgs {
public static IMsg SHOP_ITEM_BODY_PAINT
@@ -8,7 +8,7 @@ public class BodyPaintMsgs {
public static IMsg SHOP_ITEM_BODY_PAINT_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_BODY_PAINT_DESC));
public static IMsg SHOP_ITEM_BODY_PAINT_OUT
=> MsgFactory.Create(nameof(SHOP_ITEM_BODY_PAINT_OUT));
}

View File

@@ -0,0 +1,38 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game.Roles;
namespace TTT.CS2.Items.ClusterGrenade;
public static class ClusterGrenadeServiceCollection {
public static void AddClusterGrenade(this IServiceCollection services) {
services.AddModBehavior<ClusterGrenadeItem>();
services.AddModBehavior<ClusterGrenadeListener>();
}
}
public class ClusterGrenadeItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider) {
private readonly ClusterGrenadeConfig config = provider
.GetService<IStorage<ClusterGrenadeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new ClusterGrenadeConfig();
public override string Name
=> Locale[ClusterGrenadeMsgs.SHOP_ITEM_CLUSTER_GRENADE];
public override string Description
=> Locale[ClusterGrenadeMsgs.SHOP_ITEM_CLUSTER_GRENADE_DESC];
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) {
Inventory.GiveWeapon(player, config);
}
}

View File

@@ -0,0 +1,65 @@
using System.Reactive.Concurrency;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs.Traitor;
using TTT.API;
using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
namespace TTT.CS2.Items.ClusterGrenade;
public class ClusterGrenadeListener(IServiceProvider provider) : IPluginModule {
private readonly ClusterGrenadeConfig config =
provider.GetService<IStorage<ClusterGrenadeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new ClusterGrenadeConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IShop shop = provider.GetRequiredService<IShop>();
[UsedImplicitly]
[GameEventHandler]
public HookResult OnHeGrenade(EventHegrenadeDetonate ev, GameEventInfo _) {
if (ev.Userid == null) return HookResult.Continue;
var player = converter.GetPlayer(ev.Userid) as IOnlinePlayer;
if (player == null) return HookResult.Continue;
if (!shop.HasItem<ClusterGrenadeItem>(player)) return HookResult.Continue;
shop.RemoveItem<ClusterGrenadeItem>(player);
for (var i = 0; i < config.GrenadeCount; i++) {
var entity =
Utilities.GetEntityFromIndex<CHEGrenadeProjectile>(ev.Entityid);
if (entity == null || entity.AbsOrigin == null) continue;
// Throw grenade in circular pattern
var angle = new Vector(
(float)(Math.Cos(2 * Math.PI / config.GrenadeCount * i)
* config.ThrowForce),
(float)(Math.Sin(2 * Math.PI / config.GrenadeCount * i)
* config.ThrowForce), config.UpForce);
if (ev.Userid.Pawn.Value == null) continue;
GrenadeDataHelper.CreateGrenade(entity.AbsOrigin, QAngle.Zero, angle,
Vector.Zero, ev.Userid.Pawn.Value.Handle, ev.Userid.Team);
}
return HookResult.Continue;
}
public void Dispose() { }
public void Start() { }
}

View File

@@ -0,0 +1,11 @@
using TTT.Locale;
namespace TTT.CS2.Items.ClusterGrenade;
public class ClusterGrenadeMsgs {
public static IMsg SHOP_ITEM_CLUSTER_GRENADE
=> MsgFactory.Create(nameof(SHOP_ITEM_CLUSTER_GRENADE));
public static IMsg SHOP_ITEM_CLUSTER_GRENADE_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_CLUSTER_GRENADE_DESC));
}

View File

@@ -0,0 +1,32 @@
using System.Runtime.InteropServices;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
using TTT.CS2.RayTrace.Class;
using Address = TTT.CS2.Utils.Address;
namespace TTT.CS2.Items.ClusterGrenade;
public class GrenadeDataHelper {
private static readonly CHEGrenadeProjectile_CreateDelegate
CHEGrenadeProjectile_CreateFunc;
static GrenadeDataHelper() {
var heGrenadeSignature = NativeAPI.FindSignature(Addresses.ServerPath,
GameData.GetSignature("CHEGrenadeProjectile_CreateFunc"));
CHEGrenadeProjectile_CreateFunc =
Marshal
.GetDelegateForFunctionPointer<CHEGrenadeProjectile_CreateDelegate>(
heGrenadeSignature);
}
private delegate int CHEGrenadeProjectile_CreateDelegate(IntPtr position,
IntPtr angle, IntPtr velocity, IntPtr velocityAngle, IntPtr thrower,
int weaponId, byte team);
public static int CreateGrenade(Vector position, QAngle angle,
Vector velocity, Vector velocityAngle, IntPtr thrower, CsTeam team) {
return CHEGrenadeProjectile_CreateFunc(position.Handle, angle.Handle,
velocity.Handle, velocityAngle.Handle, thrower, 44, (byte)team);
}
}

View File

@@ -0,0 +1,152 @@
using System.Linq;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Timers;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
using TTT.API;
using TTT.API.Events;
using TTT.API.Extensions;
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.Events.Game;
using TTT.Game.Roles;
namespace TTT.CS2.Items.Compass;
/// <summary>
/// Base compass that renders a heading toward the nearest target returned by GetTargets.
/// Child classes decide which targets to expose and who owns the item.
/// </summary>
public abstract class AbstractCompassItem<TRole> : RoleRestrictedItem<TRole>,
IListener, IPluginModule where TRole : class, IRole {
protected readonly CompassConfig config;
protected readonly IPlayerConverter<CCSPlayerController> Converter;
protected readonly ISet<IPlayer> Owners = new HashSet<IPlayer>();
protected AbstractCompassItem(IServiceProvider provider) : base(provider) {
config = provider.GetService<IStorage<CompassConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new CompassConfig();
Converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
}
public override ShopItemConfig Config => config;
public void Start(BasePlugin? plugin) {
base.Start();
plugin?.AddTimer(0.1f, Tick, TimerFlags.REPEAT);
}
[UsedImplicitly]
[EventHandler]
public void OnRoundEnd(GameStateUpdateEvent ev) {
if (ev.NewState == State.FINISHED) Owners.Clear();
}
/// <summary>
/// Return world positions to point at for this player.
/// </summary>
protected abstract IList<Vector> GetTargets(IOnlinePlayer requester);
/// <summary>
/// Whether this player currently owns/has this compass effect.
/// </summary>
protected abstract bool OwnsItem(IOnlinePlayer player);
public override void OnPurchase(IOnlinePlayer player) { Owners.Add(player); }
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
return OwnsItem(player) ?
PurchaseResult.ALREADY_OWNED :
base.CanPurchase(player);
}
private void Tick() {
if (Games.ActiveGame is not { State: State.IN_PROGRESS or State.FINISHED })
return;
foreach (var player in Owners.OfType<IOnlinePlayer>()) {
var gamePlayer = Converter.GetPlayer(player);
if (gamePlayer == null) continue;
ShowCompass(gamePlayer, player);
}
}
private void ShowCompass(CCSPlayerController viewer, IOnlinePlayer online) {
if (Games.ActiveGame?.Players == null) return;
if (viewer.PlayerPawn.Value == null) return;
var src = viewer.Pawn.Value?.AbsOrigin.Clone();
if (src == null) return;
var targets = GetTargets(online).ToList();
if (targets.Count == 0) return;
var (nearest, distance) = GetNearestVector(src, targets);
if (nearest == null || distance > config.MaxRange) return;
var normalizedYaw = AdjustGameAngle(viewer.PlayerPawn.Value.EyeAngles.Y);
var diff = (nearest - src).Normalized();
var targetYaw = MathF.Atan2(diff.Y, diff.X) * 180f / MathF.PI;
targetYaw = AdjustGameAngle(targetYaw);
var compass = GenerateCompass(normalizedYaw, targetYaw);
compass = "<font color=\"#777777\">" + compass;
foreach (var c in "NESW".ToCharArray())
compass = compass.Replace(c.ToString(),
$"</font><font color=\"#FFFF00\">{c}</font><font color=\"#777777\">");
compass = compass.Replace("X",
"</font><font color=\"#FF0000\">X</font><font color=\"#777777\">");
compass += "</font>";
viewer.PrintToCenterHtml($"{compass} {GetDistanceDescription(distance)}");
}
private static float AdjustGameAngle(float angle) {
return 360 - (angle + 360) % 360 + 90;
}
private string GenerateCompass(float pointing, float target) {
return TextCompass.GenerateCompass(config.CompassFOV, config.CompassLength,
pointing, targetDir: target);
}
private static string GetDistanceDescription(float distance) {
return distance switch {
> 2000 => "AWP Distance",
> 1500 => "Scout Distance",
> 1000 => "Rifle Distance",
> 500 => "Pistol",
> 250 => "Nearby",
_ => "Knife Range"
};
}
private static (Vector?, float) GetNearestVector(in Vector src,
IList<Vector> targets) {
var minDistSq = float.MaxValue;
Vector? nearest = null;
foreach (var v in targets) {
var d2 = v.Clone().DistanceSquared(src);
if (d2 >= minDistSq) continue;
minDistSq = d2;
nearest = v;
}
return (nearest, MathF.Sqrt(minDistSq));
}
}

View File

@@ -0,0 +1,53 @@
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Extensions;
using TTT.API.Game;
using TTT.API.Player;
using TTT.CS2.API;
using TTT.CS2.Extensions;
using TTT.Game.Roles;
namespace TTT.CS2.Items.Compass;
public static class BodyCompassItemExtensions {
public static void
AddBodyCompassServices(this IServiceCollection collection) {
collection.AddModBehavior<BodyCompassItem>();
}
}
public class BodyCompassItem(IServiceProvider provider)
: AbstractCompassItem<DetectiveRole>(provider) {
private readonly IBodyTracker bodies =
provider.GetRequiredService<IBodyTracker>();
public override string Name => Locale[CompassMsgs.SHOP_ITEM_COMPASS_BODY];
public override string Description
=> Locale[CompassMsgs.SHOP_ITEM_COMPASS_BODY_DESC];
/// <summary>
/// For innocents: point to nearest traitor.
/// For traitors: point to nearest non-traitor (ally list in original code).
/// Returns target world positions as vectors.
/// </summary>
protected override IList<Vector> GetTargets(IOnlinePlayer requester) {
if (Games.ActiveGame is not { State: State.IN_PROGRESS or State.FINISHED })
return Array.Empty<Vector>();
List<Vector> vectors = [];
foreach (var (apiBody, body) in bodies.Bodies) {
if (apiBody.IsIdentified) continue;
var origin = body.AbsOrigin.Clone();
if (origin == null) continue;
vectors.Add(origin);
}
return vectors;
}
override protected bool OwnsItem(IOnlinePlayer player) {
return Shop.HasItem<BodyCompassItem>(player);
}
}

View File

@@ -0,0 +1,17 @@
using TTT.Locale;
namespace TTT.CS2.Items.Compass;
public class CompassMsgs {
public static IMsg SHOP_ITEM_COMPASS_PLAYER
=> MsgFactory.Create(nameof(SHOP_ITEM_COMPASS_PLAYER));
public static IMsg SHOP_ITEM_COMPASS_PLAYER_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_COMPASS_PLAYER_DESC));
public static IMsg SHOP_ITEM_COMPASS_BODY
=> MsgFactory.Create(nameof(SHOP_ITEM_COMPASS_BODY));
public static IMsg SHOP_ITEM_COMPASS_BODY_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_COMPASS_BODY_DESC));
}

View File

@@ -0,0 +1,62 @@
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Extensions;
using TTT.API.Game;
using TTT.API.Player;
using TTT.CS2.Extensions;
using TTT.Game.Roles;
namespace TTT.CS2.Items.Compass;
public static class InnoCompassItemExtensions {
public static void
AddInnoCompassServices(this IServiceCollection collection) {
collection.AddModBehavior<InnoCompassItem>();
}
}
public class InnoCompassItem(IServiceProvider provider)
: AbstractCompassItem<TraitorRole>(provider) {
public override string Name => Locale[CompassMsgs.SHOP_ITEM_COMPASS_PLAYER];
public override string Description
=> Locale[CompassMsgs.SHOP_ITEM_COMPASS_PLAYER_DESC];
/// <summary>
/// For innocents: point to nearest traitor.
/// For traitors: point to nearest non-traitor (ally list in original code).
/// Returns target world positions as vectors.
/// </summary>
protected override IList<Vector> GetTargets(IOnlinePlayer requester) {
if (Games.ActiveGame is not { State: State.IN_PROGRESS or State.FINISHED })
return Array.Empty<Vector>();
var all = Games.ActiveGame.Players.OfType<IOnlinePlayer>()
.Where(p => p.IsAlive)
.ToList();
// Split by traitor role
var traitors = all.Where(p => Roles.GetRoles(p).Any(r => r is TraitorRole))
.ToList();
var allies = all.Where(p => !Roles.GetRoles(p).Any(r => r is TraitorRole))
.ToList();
var enemies = Roles.GetRoles(requester).Any(r => r is TraitorRole) ?
allies :
traitors;
// Convert to game controllers then to positions
var vectors = new List<Vector>(enemies.Count);
foreach (var enemy in enemies) {
var controller = Converter.GetPlayer(enemy);
var pos = controller?.Pawn.Value?.AbsOrigin.Clone();
if (pos != null) vectors.Add(pos);
}
return vectors;
}
override protected bool OwnsItem(IOnlinePlayer player) {
return Shop.HasItem<InnoCompassItem>(player);
}
}

View File

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

View File

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

View File

@@ -31,7 +31,7 @@ public class DnaScanner(IServiceProvider provider)
public override void OnPurchase(IOnlinePlayer player) { }
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
if (Shop.HasItem(player, this)) return PurchaseResult.ALREADY_OWNED;
if (Shop.HasItem<DnaScanner>(player)) return PurchaseResult.ALREADY_OWNED;
return base.CanPurchase(player);
}
}

View File

@@ -0,0 +1,41 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game.Roles;
namespace TTT.CS2.Items.OneHitKnife;
public static class OneHitKnifeServiceCollection {
public static void AddOneHitKnifeService(this IServiceCollection services) {
services.AddModBehavior<OneHitKnife>();
services.AddModBehavior<OneHitKnifeListener>();
}
}
public class OneHitKnife(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider) {
private readonly OneHitKnifeConfig config = provider
.GetService<IStorage<OneHitKnifeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new OneHitKnifeConfig();
public override string Name
=> Locale[OneHitKnifeMsgs.SHOP_ITEM_ONE_HIT_KNIFE];
public override string Description
=> Locale[OneHitKnifeMsgs.SHOP_ITEM_ONE_HIT_KNIFE_DESC];
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) { }
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
if (Shop.HasItem<OneHitKnife>(player)) return PurchaseResult.ALREADY_OWNED;
return base.CanPurchase(player);
}
}

View File

@@ -0,0 +1,43 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs.Traitor;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
namespace TTT.CS2.Items.OneHitKnife;
public class OneHitKnifeListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly OneHitKnifeConfig config =
provider.GetService<IStorage<OneHitKnifeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new OneHitKnifeConfig();
private readonly IShop shop = provider.GetRequiredService<IShop>();
[UsedImplicitly]
[EventHandler]
public void OnDamage(PlayerDamagedEvent ev) {
if (Games.ActiveGame is not { State: State.IN_PROGRESS }) return;
if (ev.Weapon == null || !Tag.KNIVES.Contains(ev.Weapon)) return;
var attacker = ev.Attacker;
var victim = ev.Player;
if (attacker == null) return;
if (!shop.HasItem<OneHitKnife>(attacker)) return;
var friendly = Roles.GetRoles(attacker)
.Any(r => Roles.GetRoles(victim).Contains(r));
if (friendly && !config.FriendlyFire) return;
shop.RemoveItem<OneHitKnife>(attacker);
ev.HpLeft = -100;
}
}

View File

@@ -0,0 +1,11 @@
using TTT.Locale;
namespace TTT.CS2.Items.OneHitKnife;
public class OneHitKnifeMsgs {
public static IMsg SHOP_ITEM_ONE_HIT_KNIFE
=> MsgFactory.Create(nameof(SHOP_ITEM_ONE_HIT_KNIFE));
public static IMsg SHOP_ITEM_ONE_HIT_KNIFE_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_ONE_HIT_KNIFE_DESC));
}

View File

@@ -10,9 +10,10 @@ public class PoisonShotMsgs {
public static IMsg SHOP_ITEM_POISON_SHOTS_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_POISON_SHOTS_DESC));
public static IMsg SHOP_ITEM_POISON_HIT(IPlayer player)
=> MsgFactory.Create(nameof(SHOP_ITEM_POISON_HIT), player.Name);
public static IMsg SHOP_ITEM_POISON_OUT
=> MsgFactory.Create(nameof(SHOP_ITEM_POISON_OUT));
public static IMsg SHOP_ITEM_POISON_HIT(IPlayer player) {
return MsgFactory.Create(nameof(SHOP_ITEM_POISON_HIT), player.Name);
}
}

View File

@@ -1,20 +1,19 @@
using System.Drawing;
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Concurrency;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs.Traitor;
using ShopAPI.Events;
using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.CS2.Extensions;
using TTT.Game.Events.Body;
using TTT.Game.Events.Game;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
@@ -23,7 +22,7 @@ namespace TTT.CS2.Items.PoisonShots;
public class PoisonShotsListener(IServiceProvider provider)
: BaseListener(provider), IPluginModule {
private readonly Dictionary<IPlayer, int> poisonShots = new();
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly PoisonShotsConfig config =
provider.GetService<IStorage<PoisonShotsConfig>>()
@@ -34,13 +33,22 @@ public class PoisonShotsListener(IServiceProvider provider)
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IScheduler scheduler =
provider.GetRequiredService<IScheduler>();
private readonly Dictionary<IPlayer, int> poisonShots = new();
private readonly List<IDisposable> poisonTimers = [];
private readonly IScheduler scheduler =
provider.GetRequiredService<IScheduler>();
private readonly IShop shop = provider.GetRequiredService<IShop>();
private readonly ISet<string> killedWithPoison = new HashSet<string>();
public override void Dispose() {
base.Dispose();
foreach (var timer in poisonTimers) timer.Dispose();
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnFire(EventWeaponFire ev, GameEventInfo _) {
@@ -61,9 +69,10 @@ public class PoisonShotsListener(IServiceProvider provider)
if (ev.Attacker == null) return;
if (!poisonShots.TryGetValue(ev.Attacker, out var shot) || shot <= 0)
return;
if (ev.Weapon == null || !Tag.GUNS.Contains(ev.Weapon)) return;
Messenger.Message(ev.Attacker,
Locale[PoisonShotMsgs.SHOP_ITEM_POISON_HIT(ev.Player)]);
addPoisonEffect(ev.Player);
addPoisonEffect(ev.Player, ev.Attacker);
}
[UsedImplicitly]
@@ -73,16 +82,21 @@ public class PoisonShotsListener(IServiceProvider provider)
foreach (var timer in poisonTimers) timer.Dispose();
poisonTimers.Clear();
poisonShots.Clear();
killedWithPoison.Clear();
}
private void addPoisonEffect(IPlayer player) {
[SuppressMessage("ReSharper", "AccessToModifiedClosure")]
private void addPoisonEffect(IPlayer player, IPlayer shooter) {
IDisposable? timer = null;
var effect = new PoisonEffect(player);
timer = scheduler.SchedulePeriodic(config.TimeBetweenDamage, () => {
// ReSharper disable once AccessToModifiedClosure
var effect = new PoisonEffect(player, shooter);
timer = scheduler.SchedulePeriodic(config.PoisonConfig.TimeBetweenDamage, ()
=> {
Server.NextWorldUpdate(() => {
if (!tickPoison(effect)) timer?.Dispose();
if (tickPoison(effect) || timer == null) return;
timer.Dispose();
poisonTimers.Remove(timer);
});
});
@@ -92,31 +106,39 @@ public class PoisonShotsListener(IServiceProvider provider)
private bool tickPoison(PoisonEffect effect) {
if (effect.Player is not IOnlinePlayer online) return false;
if (!online.IsAlive) return false;
online.Health -= config.DamagePerTick;
var dmgEvent = new PlayerDamagedEvent(online,
effect.Shooter as IOnlinePlayer, online.Health,
online.Health - config.PoisonConfig.DamagePerTick) {
Weapon = $"[{Locale[PoisonShotMsgs.SHOP_ITEM_POISON_SHOTS]}]"
};
bus.Dispatch(dmgEvent);
if (dmgEvent.IsCanceled) return true;
if (online.Health - config.PoisonConfig.DamagePerTick <= 0) {
killedWithPoison.Add(online.Id);
var deathEvent = new PlayerDeathEvent(online)
.WithKiller(effect.Shooter as IOnlinePlayer)
.WithWeapon($"[{Locale[PoisonShotMsgs.SHOP_ITEM_POISON_SHOTS]}]");
bus.Dispatch(deathEvent);
}
effect.Ticks++;
effect.DamageGiven += config.DamagePerTick;
effect.DamageGiven += config.PoisonConfig.DamagePerTick;
var gamePlayer = converter.GetPlayer(online);
gamePlayer?.ColorScreen(config.PoisonColor, 0.2f, 0.3f);
gamePlayer?.ExecuteClientCommand("play " + config.PoisonSound);
if (gamePlayer != null)
gamePlayer.DealPoisonDamage(config.PoisonConfig.DamagePerTick);
return effect.DamageGiven < config.TotalDamage;
}
public override void Dispose() {
base.Dispose();
foreach (var timer in poisonTimers) timer.Dispose();
}
private class PoisonEffect(IPlayer player) {
public IPlayer Player { get; init; } = player;
public int Ticks { get; set; }
public int DamageGiven { get; set; }
return effect.DamageGiven < config.PoisonConfig.TotalDamage;
}
/// <summary>
/// Uses a poison shot for the player. Returns the remaining shots, -1 if none
/// are available.
/// Uses a poison shot for the player. Returns the remaining shots, -1 if none
/// are available.
/// </summary>
/// <param name="player"></param>
/// <returns></returns>
@@ -133,4 +155,20 @@ public class PoisonShotsListener(IServiceProvider provider)
shop.RemoveItem<PoisonShotsItem>(player);
return 0;
}
private class PoisonEffect(IPlayer player, IPlayer shooter) {
public IPlayer Player { get; } = player;
public IPlayer Shooter { get; } = shooter;
public int Ticks { get; set; }
public int DamageGiven { get; set; }
}
[UsedImplicitly]
[EventHandler]
public void OnRagdollSpawn(BodyCreateEvent ev) {
if (!killedWithPoison.Contains(ev.Body.OfPlayer.Id)) return;
if (ev.Body.Killer == null || ev.Body.Killer.Id == ev.Body.OfPlayer.Id)
ev.IsCanceled = true;
}
}

View File

@@ -0,0 +1,43 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game.Roles;
namespace TTT.CS2.Items.PoisonSmoke;
public static class PoisonSmokeServiceCollection {
public static void AddPoisonSmoke(this IServiceCollection services) {
services.AddModBehavior<PoisonSmokeItem>();
services.AddModBehavior<PoisonSmokeListener>();
}
}
public class PoisonSmokeItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider) {
private readonly PoisonSmokeConfig config =
provider.GetService<IStorage<PoisonSmokeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new PoisonSmokeConfig();
public override string Name => Locale[PoisonSmokeMsgs.SHOP_ITEM_POISON_SMOKE];
public override string Description
=> Locale[PoisonSmokeMsgs.SHOP_ITEM_POISON_SMOKE_DESC];
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) {
Inventory.GiveWeapon(player, new BaseWeapon(config.Weapon));
}
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
return Shop.HasItem<PoisonSmokeItem>(player) ?
PurchaseResult.ALREADY_OWNED :
base.CanPurchase(player);
}
}

View File

@@ -0,0 +1,156 @@
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Concurrency;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs.Traitor;
using TTT.API;
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.Game.Events.Body;
using TTT.Game.Events.Game;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
using TTT.Game.Roles;
namespace TTT.CS2.Items.PoisonSmoke;
public class PoisonSmokeListener(IServiceProvider provider)
: BaseListener(provider), IPluginModule {
private readonly PoisonSmokeConfig config =
provider.GetService<IStorage<PoisonSmokeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new PoisonSmokeConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly List<IDisposable> poisonSmokes = [];
private readonly IShop shop = provider.GetRequiredService<IShop>();
private readonly ISet<string> killedWithPoison = new HashSet<string>();
public override void Dispose() {
base.Dispose();
foreach (var timer in poisonSmokes) timer.Dispose();
poisonSmokes.Clear();
killedWithPoison.Clear();
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnSmokeGrenade(EventSmokegrenadeDetonate ev,
GameEventInfo _) {
if (ev.Userid == null) return HookResult.Continue;
var player = converter.GetPlayer(ev.Userid) as IOnlinePlayer;
if (player == null) return HookResult.Continue;
if (!shop.HasItem<PoisonSmokeItem>(player)) return HookResult.Continue;
shop.RemoveItem<PoisonSmokeItem>(player);
var projectile =
Utilities.GetEntityFromIndex<CSmokeGrenadeProjectile>(ev.Entityid);
if (projectile == null || !projectile.IsValid) return HookResult.Continue;
startPoisonEffect(projectile, player);
return HookResult.Continue;
}
[SuppressMessage("ReSharper", "AccessToModifiedClosure")]
private void startPoisonEffect(CSmokeGrenadeProjectile projectile,
IOnlinePlayer thrower) {
IDisposable? timer = null;
var effect = new PoisonEffect(projectile, thrower);
timer = Scheduler.SchedulePeriodic(config.PoisonConfig.TimeBetweenDamage, ()
=> {
Server.NextWorldUpdate(() => {
if (tickPoisonEffect(effect) || timer == null) return;
timer.Dispose();
poisonSmokes.Remove(timer);
});
});
poisonSmokes.Add(timer);
}
private bool tickPoisonEffect(PoisonEffect effect) {
if (!effect.Projectile.IsValid) return false;
effect.Ticks++;
var players = Finder.GetOnline()
.Where(player => player.IsAlive && Roles.GetRoles(player)
.Any(role => role is InnocentRole or DetectiveRole));
var gamePlayers = players.Select(p => (p, converter.GetPlayer(p)))
.Where(p => p.Item2 != null && p.Item2.Pawn.Value != null
&& p.Item2.Pawn.Value.IsValid)
.Select(p => (p!, p.Item2?.Pawn.Value?.AbsOrigin.Clone()!));
gamePlayers = gamePlayers.Where(t
=> t.Item2.Distance(effect.Origin) <= config.SmokeRadius);
foreach (var (apiPlayer, gamePlayer) in gamePlayers.Select(p => p.Item1)) {
if (effect.DamageGiven >= config.PoisonConfig.TotalDamage) continue;
if (gamePlayer.GetHealth() - config.PoisonConfig.DamagePerTick <= 0) {
killedWithPoison.Add(apiPlayer.Id);
var playerDeathEvent = new PlayerDeathEvent(apiPlayer)
.WithKiller(effect.Attacker as IOnlinePlayer)
.WithWeapon("[Poison Smoke]");
Bus.Dispatch(playerDeathEvent);
gamePlayer.SetHealth(0);
continue;
}
var dmgEvent = new PlayerDamagedEvent(apiPlayer,
effect.Attacker as IOnlinePlayer, config.PoisonConfig.DamagePerTick) {
Weapon = "[Poison Smoke]"
};
Bus.Dispatch(dmgEvent);
gamePlayer.DealPoisonDamage(config.PoisonConfig.DamagePerTick);
effect.DamageGiven += config.PoisonConfig.DamagePerTick;
}
return effect.DamageGiven < config.PoisonConfig.TotalDamage;
}
private class PoisonEffect(CSmokeGrenadeProjectile projectile,
IOnlinePlayer attacker) {
public int Ticks { get; set; }
public int DamageGiven { get; set; }
public Vector Origin { get; } = projectile.AbsOrigin.Clone()!;
public CSmokeGrenadeProjectile Projectile { get; } = projectile;
public IPlayer Attacker { get; } = attacker;
}
[UsedImplicitly]
[EventHandler]
public void OnGameEnd(GameStateUpdateEvent ev) {
if (ev.NewState != State.FINISHED) return;
killedWithPoison.Clear();
}
[UsedImplicitly]
[EventHandler]
public void OnRagdollSpawn(BodyCreateEvent ev) {
if (!killedWithPoison.Contains(ev.Body.OfPlayer.Id)) return;
if (ev.Body.Killer == null || ev.Body.Killer.Id == ev.Body.OfPlayer.Id)
ev.IsCanceled = true;
}
}

View File

@@ -0,0 +1,11 @@
using TTT.Locale;
namespace TTT.CS2.Items.PoisonSmoke;
public class PoisonSmokeMsgs {
public static IMsg SHOP_ITEM_POISON_SMOKE
=> MsgFactory.Create(nameof(SHOP_ITEM_POISON_SMOKE));
public static IMsg SHOP_ITEM_POISON_SMOKE_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_POISON_SMOKE_DESC));
}

View File

@@ -0,0 +1,104 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.UserMessages;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
using TTT.API;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.CS2.Extensions;
using TTT.CS2.RayTrace.Class;
using TTT.Game.Roles;
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
namespace TTT.CS2.Items.SilentAWP;
public static class SilentAWPServiceCollection {
public static void AddSilentAWPServices(this IServiceCollection services) {
services.AddModBehavior<SilentAWPItem>();
}
}
public class SilentAWPItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider), IPluginModule {
private readonly SilentAWPConfig config =
provider.GetService<IStorage<SilentAWPConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new SilentAWPConfig();
private readonly IPlayerConverter<CCSPlayerController> playerConverter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IDictionary<string, int> silentShots =
new Dictionary<string, int>();
public override string Name => Locale[SilentAWPMsgs.SHOP_ITEM_SILENT_AWP];
public override string Description
=> Locale[SilentAWPMsgs.SHOP_ITEM_SILENT_AWP_DESC];
public override ShopItemConfig Config => config;
public void Start(BasePlugin? plugin) {
base.Start();
plugin?.HookUserMessage(452, onWeaponSound);
}
public override void OnPurchase(IOnlinePlayer player) {
silentShots[player.Id] = config.CurrentAmmo ?? 0 + config.ReserveAmmo ?? 0;
Task.Run(async () => {
await Inventory.RemoveWeaponInSlot(player, 0);
await Inventory.GiveWeapon(player, config);
});
}
private HookResult onWeaponSound(UserMessage msg) {
var defIndex = msg.ReadUInt("item_def_index");
if (config.WeaponIndex != defIndex) return HookResult.Continue;
var splits = msg.DebugString.Split("\n");
if (splits.Length < 5) return HookResult.Continue;
var angleLines = msg.DebugString.Split("\n")[1..4]
.Select(s => s.Trim())
.ToList();
if (!angleLines[0].Contains('x') || !angleLines[1].Contains('y')
|| !angleLines[2].Contains('z'))
return HookResult.Continue;
var x = float.Parse(angleLines[0].Split(' ')[1]);
var y = float.Parse(angleLines[1].Split(' ')[1]);
var z = float.Parse(angleLines[2].Split(' ')[1]);
var vec = new Vector(x, y, z);
var player = findPlayerByCoord(vec);
if (player == null) return HookResult.Continue;
if (playerConverter.GetPlayer(player) is not IOnlinePlayer apiPlayer)
return HookResult.Continue;
if (!silentShots.TryGetValue(apiPlayer.Id, out var shots) || shots <= 0)
return HookResult.Continue;
silentShots[apiPlayer.Id] = shots - 1;
if (silentShots[apiPlayer.Id] == 0) {
silentShots.Remove(apiPlayer.Id);
Shop.RemoveItem<SilentAWPItem>(apiPlayer);
}
msg.Recipients.Clear();
return HookResult.Handled;
}
private CCSPlayerController? findPlayerByCoord(Vector vec) {
foreach (var pl in Utilities.GetPlayers()) {
var origin = pl.GetEyePosition();
if (origin == null) continue;
var dist = vec.DistanceSquared(origin);
if (dist < 1) return pl;
}
return null;
}
}

View File

@@ -0,0 +1,11 @@
using TTT.Locale;
namespace TTT.CS2.Items.SilentAWP;
public class SilentAWPMsgs {
public static IMsg SHOP_ITEM_SILENT_AWP
=> MsgFactory.Create(nameof(SHOP_ITEM_SILENT_AWP));
public static IMsg SHOP_ITEM_SILENT_AWP_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_SILENT_AWP_DESC));
}

View File

@@ -1,12 +1,18 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
using TTT.API.Events;
using TTT.API.Extensions;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
using TTT.CS2.Extensions;
using TTT.Game.Events.Body;
using TTT.Game.Events.Game;
using TTT.Game.Events.Player;
using TTT.Game.Roles;
namespace TTT.CS2.Items.Station;
@@ -17,11 +23,14 @@ public static class DamageStationCollection {
}
}
public class DamageStation(IServiceProvider provider) : StationItem(provider,
provider.GetService<IStorage<DamageStationConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new DamageStationConfig()) {
public class DamageStation(IServiceProvider provider)
: StationItem<TraitorRole>(provider,
provider.GetService<IStorage<DamageStationConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new DamageStationConfig()), IListener {
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
@@ -36,11 +45,15 @@ public class DamageStation(IServiceProvider provider) : StationItem(provider,
public override string Description
=> Locale[StationMsgs.SHOP_ITEM_STATION_HURT_DESC];
private readonly ISet<string> killedWithStation = new HashSet<string>();
override protected void onInterval() {
var players = finder.GetOnline();
var players = finder.GetOnline();
var toRemove = new List<CPhysicsPropMultiplayer>();
foreach (var (prop, info) in props) {
if (Math.Abs(info.HealthGiven) > Math.Abs(_Config.TotalHealthGiven)) {
props.Remove(prop);
if (_Config.TotalHealthGiven != 0 && Math.Abs(info.HealthGiven)
> Math.Abs(_Config.TotalHealthGiven)) {
toRemove.Add(prop);
continue;
}
@@ -65,11 +78,45 @@ public class DamageStation(IServiceProvider provider) : StationItem(provider,
var healthScale = 1.0 - dist / _Config.MaxRange;
var damageAmount =
(int)Math.Floor(_Config.HealthIncrements * healthScale);
var dmgEvent = new PlayerDamagedEvent(player,
info.Owner as IOnlinePlayer, damageAmount) { Weapon = $"[{Name}]" };
bus.Dispatch(dmgEvent);
damageAmount = -dmgEvent.DmgDealt;
if (player.Health + damageAmount <= 0) {
killedWithStation.Add(player.Id);
var playerDeath = new PlayerDeathEvent(player)
.WithKiller(info.Owner as IOnlinePlayer)
.WithWeapon($"[{Name}]");
bus.Dispatch(playerDeath);
}
player.Health += damageAmount;
info.HealthGiven += damageAmount;
gamePlayer.ExecuteClientCommand("play " + _Config.UseSound);
gamePlayer.EmitSound("Player.DamageFall", null, 0.2f);
}
}
foreach (var prop in toRemove) props.Remove(prop);
}
[UsedImplicitly]
[EventHandler]
public void OnGameEnd(GameStateUpdateEvent ev) {
if (ev.NewState != State.FINISHED) return;
killedWithStation.Clear();
}
[UsedImplicitly]
[EventHandler]
public void OnRagdollSpawn(BodyCreateEvent ev) {
if (!killedWithStation.Contains(ev.Body.OfPlayer.Id)) return;
if (ev.Body.Killer == null || ev.Body.Killer.Id == ev.Body.OfPlayer.Id)
ev.IsCanceled = true;
}
}

View File

@@ -1,10 +1,11 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs;
using ShopAPI.Configs.Detective;
using TTT.API.Extensions;
using TTT.API.Storage;
using TTT.CS2.Extensions;
using TTT.Game.Roles;
namespace TTT.CS2.Items.Station;
@@ -14,21 +15,24 @@ public static class HealthStationCollection {
}
}
public class HealthStation(IServiceProvider provider) : StationItem(provider,
provider.GetService<IStorage<HealthStationConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new HealthStationConfig()) {
public class HealthStation(IServiceProvider provider)
: StationItem<DetectiveRole>(provider,
provider.GetService<IStorage<HealthStationConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new HealthStationConfig()) {
public override string Name => Locale[StationMsgs.SHOP_ITEM_STATION_HEALTH];
public override string Description
=> Locale[StationMsgs.SHOP_ITEM_STATION_HEALTH_DESC];
override protected void onInterval() {
var players = Utilities.GetPlayers();
var players = Utilities.GetPlayers();
var toRemove = new List<CPhysicsPropMultiplayer>();
foreach (var (prop, info) in props) {
if (Math.Abs(info.HealthGiven) > _Config.TotalHealthGiven) {
props.Remove(prop);
if (_Config.TotalHealthGiven != 0
&& Math.Abs(info.HealthGiven) > _Config.TotalHealthGiven) {
toRemove.Add(prop);
continue;
}
@@ -45,14 +49,17 @@ public class HealthStation(IServiceProvider provider) : StationItem(provider,
foreach (var (player, dist) in playerDists) {
var maxHp = player.Pawn.Value?.MaxHealth ?? 100;
var healthScale = 1.0 - dist / _Config.MaxRange;
var healAmount =
var maxHealAmo =
(int)Math.Ceiling(_Config.HealthIncrements * healthScale);
var newHealth = Math.Min(player.GetHealth() + healAmount, maxHp);
var newHealth = Math.Min(player.GetHealth() + maxHealAmo, maxHp);
var healthGiven = newHealth - player.GetHealth();
player.SetHealth(newHealth);
info.HealthGiven += healAmount;
info.HealthGiven += healthGiven;
player.ExecuteClientCommand("play " + _Config.UseSound);
if (healthGiven > 0) player.EmitSound("HealthShot.Pickup", null, 0.1f);
}
}
foreach (var prop in toRemove) props.Remove(prop);
}
}

View File

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

View File

@@ -9,20 +9,22 @@ using ShopAPI;
using ShopAPI.Configs;
using TTT.API;
using TTT.API.Player;
using TTT.API.Role;
using TTT.CS2.Extensions;
using TTT.Game.Roles;
using TTT.CS2.RayTrace.Class;
namespace TTT.CS2.Items.Station;
public abstract class StationItem(IServiceProvider provider,
public abstract class StationItem<T>(IServiceProvider provider,
StationConfig config)
: RoleRestrictedItem<DetectiveRole>(provider), IPluginModule {
private static readonly long PROP_SIZE_SQUARED = 500;
: RoleRestrictedItem<T>(provider), IPluginModule where T : IRole {
protected readonly StationConfig _Config = config;
protected readonly IPlayerConverter<CCSPlayerController> Converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly long PROP_SIZE_SQUARED = 500;
protected readonly Dictionary<CPhysicsPropMultiplayer, StationInfo> props =
new();
@@ -117,7 +119,7 @@ public abstract class StationItem(IServiceProvider provider,
if (prop == null) return;
props[prop] = new StationInfo(prop, _Config.StationHealth);
props[prop] = new StationInfo(prop, _Config.StationHealth, player);
prop.SetModel("models/props/cs_office/microwave.vmdl");
prop.DispatchSpawn();
@@ -126,12 +128,15 @@ public abstract class StationItem(IServiceProvider provider,
if (gamePlayer == null || !gamePlayer.Pawn.IsValid
|| gamePlayer.Pawn.Value == null)
return;
var spawnPos = gamePlayer.Pawn.Value.AbsOrigin.Clone();
if (spawnPos != null && gamePlayer.PlayerPawn.Value != null) {
var forward = gamePlayer.PlayerPawn.Value.EyeAngles.ToForward();
forward.Z = 0;
spawnPos += forward.Normalized() * 8;
}
var spawnPos = gamePlayer.GetEyePosition();
var forward = gamePlayer.Pawn.Value.AbsRotation;
if (spawnPos == null) return;
if (forward == null) forward = new QAngle(0, 0, 0);
spawnPos += forward.ToForward() * 50;
prop.Teleport(spawnPos);
});

View File

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

View File

@@ -0,0 +1,75 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.CS2.Roles;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
using TTT.Karma;
using TTT.Karma.Events;
using TTT.Karma.lang;
namespace TTT.CS2.Listeners;
public class KarmaBanner(IServiceProvider provider) : BaseListener(provider) {
private readonly KarmaConfig config =
provider.GetService<IStorage<KarmaConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new KarmaConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly Dictionary<IPlayer, int> cooldownRounds = new();
private readonly IKarmaService karma =
provider.GetRequiredService<IKarmaService>();
private readonly Dictionary<IPlayer, DateTime> lastWarned = new();
[UsedImplicitly]
[EventHandler(Priority = Priority.MONITOR, IgnoreCanceled = true)]
public void OnKarmaUpdate(KarmaUpdateEvent ev) {
if (ev.Karma < config.MinKarma) {
issueKarmaBan(ev.Player);
return;
}
if (ev.Karma >= config.KarmaTimeoutThreshold) return;
var timeSinceLastWarn = DateTime.UtcNow
- lastWarned.GetValueOrDefault(ev.Player, DateTime.MinValue);
if (timeSinceLastWarn <= config.KarmaWarningWindow) return;
issueKarmaWarning(ev.Player);
}
[UsedImplicitly]
[EventHandler(Priority = Priority.HIGH)]
public void OnRoleAssign(PlayerRoleAssignEvent ev) {
if (!cooldownRounds.TryGetValue(ev.Player, out var rounds) || rounds <= 0)
return;
Messenger.Message(ev.Player, Locale[KarmaMsgs.KARMA_WARNING(rounds)]);
cooldownRounds[ev.Player]--;
if (cooldownRounds[ev.Player] <= 0) cooldownRounds.Remove(ev.Player);
ev.Role = new SpectatorRole(Provider);
}
private void issueKarmaBan(IPlayer player) {
Server.NextWorldUpdate(() => {
var userId = converter.GetPlayer(player);
if (userId == null) return;
Server.ExecuteCommand(string.Format(config.CommandUponLowKarma,
userId.UserId));
Task.Run(async () => await karma.Write(player, config.DefaultKarma));
});
}
private void issueKarmaWarning(IPlayer player) {
cooldownRounds[player] = config.KarmaRoundTimeout;
lastWarned[player] = DateTime.UtcNow;
}
}

View File

@@ -0,0 +1,54 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Events;
using TTT.API.Player;
using TTT.Game.Listeners;
using TTT.Karma;
using TTT.Karma.Events;
namespace TTT.CS2.Listeners;
public class KarmaSyncer(IServiceProvider provider)
: BaseListener(provider), IPluginModule {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IKarmaService? karma = provider.GetService<IKarmaService>();
[UsedImplicitly]
[EventHandler]
public void OnKarmaUpdate(KarmaUpdateEvent ev) {
if (karma == null) return;
Server.NextWorldUpdate(() => {
var player = converter.GetPlayer(ev.Player);
if (player == null) return;
player.Score = ev.Karma;
Utilities.SetStateChanged(player, "CCSPlayerController", "m_iScore");
});
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnJoin(EventPlayerConnectFull ev, GameEventInfo _) {
if (ev.Userid == null || karma == null) return HookResult.Continue;
var player = converter.GetPlayer(ev.Userid);
var user = ev.Userid;
Task.Run(async () => {
var karmaValue = await karma.Load(player);
await Server.NextWorldUpdateAsync(() => {
if (!user.IsValid) return;
user.Score = karmaValue;
Utilities.SetStateChanged(user, "CCSPlayerController", "m_iScore");
});
});
return HookResult.Continue;
}
}

View File

@@ -1,9 +1,9 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using JetBrains.Annotations;
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;
@@ -18,9 +18,6 @@ public class PlayerStatsTracker(IServiceProvider provider) : IListener {
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
private readonly ISet<int> revealedDeaths = new HashSet<int>();
private readonly IDictionary<int, (int, int)> roundKillsAndAssists =
@@ -28,6 +25,7 @@ public class PlayerStatsTracker(IServiceProvider provider) : IListener {
public void Dispose() { }
[UsedImplicitly]
[EventHandler(Priority = Priority.MONITOR)]
public void OnIdentify(BodyIdentifyEvent ev) {
var gamePlayer = converter.GetPlayer(ev.Body.OfPlayer);
@@ -44,6 +42,7 @@ public class PlayerStatsTracker(IServiceProvider provider) : IListener {
// Needs to be higher so we detect the kill before the game ends
// in the case that this is the last player
[UsedImplicitly]
[EventHandler(Priority = Priority.HIGH)]
public void OnKill(PlayerDeathEvent ev) {
var killer = ev.Killer == null ? null : converter.GetPlayer(ev.Killer);
@@ -63,6 +62,7 @@ public class PlayerStatsTracker(IServiceProvider provider) : IListener {
}
}
[UsedImplicitly]
[EventHandler]
public void OnRoundEnd(GameStateUpdateEvent ev) {
if (ev.NewState == State.IN_PROGRESS) {

View File

@@ -45,7 +45,9 @@ public class RoundTimerListener(IServiceProvider provider)
.TotalSeconds);
Server.ExecuteCommand("mp_ignore_round_win_conditions 1");
foreach (var player in Utilities.GetPlayers()
.Where(p => p.LifeState != (int)LifeState_t.LIFE_ALIVE))
.Where(p => p.LifeState != (int)LifeState_t.LIFE_ALIVE && p is {
Team: CsTeam.CounterTerrorist or CsTeam.Terrorist
}))
player.Respawn();
foreach (var player in Utilities.GetPlayers())
@@ -55,6 +57,15 @@ public class RoundTimerListener(IServiceProvider provider)
return;
}
if (ev.NewState == State.IN_PROGRESS)
Server.NextWorldUpdate(() => {
foreach (var player in Utilities.GetPlayers()
.Where(p => p.LifeState != (int)LifeState_t.LIFE_ALIVE && p is {
Team: CsTeam.CounterTerrorist or CsTeam.Terrorist
}))
player.Respawn();
});
if (ev.NewState == State.FINISHED) endTimer?.Dispose();
if (ev.NewState != State.IN_PROGRESS) return;
var duration = config.RoundCfg.RoundDuration(ev.Game.Players.Count);

View File

@@ -53,7 +53,7 @@ public class CS2AliveSpoofer : IAliveSpoofer, IPluginModule {
}
private void onTick() {
_fakeAlivePlayers.RemoveWhere(p => !p.IsValid);
_fakeAlivePlayers.RemoveWhere(p => !p.IsValid || p.Handle == IntPtr.Zero);
foreach (var player in _fakeAlivePlayers) {
player.PawnIsAlive = true;
Utilities.SetStateChanged(player, "CCSPlayerController",

View File

@@ -1,7 +1,5 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
using TTT.API.Player;
namespace TTT.CS2.Player;
@@ -10,18 +8,13 @@ public class CS2PermManager(IPlayerConverter<CCSPlayerController> converter)
: IPermissionManager {
public bool HasFlags(IPlayer player, params string[] flags) {
if (flags.Length == 0) return true;
Console.WriteLine("Checking flags for player: " + player.Id);
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return false;
ulong.TryParse(player.Id, out var steamId);
return AdminManager.PlayerHasPermissions(new SteamID(steamId), flags);
return AdminManager.PlayerHasPermissions(gamePlayer, flags);
}
public bool InGroups(IPlayer player, params string[] groups) {
if (groups.Length == 0) return true;
Server.PrintToChatAll("Checking groups for player: " + player.Id);
Console.WriteLine("Checking groups for player: " + player.Id);
var gamePlayer = converter.GetPlayer(player);
var adminData = AdminManager.GetPlayerAdminData(gamePlayer);

View File

@@ -10,7 +10,7 @@ namespace TTT.CS2.Player;
/// Non-human Players (bots) will be tracked by their entity index.
/// Note that slot numbers are not guaranteed to be stable across server restarts.
/// </summary>
public class CS2Player : IOnlinePlayer {
public class CS2Player : IOnlinePlayer, IEquatable<CS2Player> {
private CCSPlayerController? cachePlayer;
protected CS2Player(string id, string name) {
@@ -51,6 +51,11 @@ public class CS2Player : IOnlinePlayer {
=> Math.Min(Utilities.GetPlayers().Select(p => p.PlayerName.Length).Max(),
24);
public bool Equals(CS2Player? other) {
if (other is null) return false;
return Id == other.Id;
}
public string Id { get; }
public string Name { get; }
@@ -107,6 +112,8 @@ public class CS2Player : IOnlinePlayer {
return player.SteamID.ToString();
}
public override int GetHashCode() { return Id.GetHashCode(); }
public override string ToString() { return createPaddedName(); }
// Goal: Pad the name to a fixed width for better alignment in logs

View File

@@ -13,6 +13,9 @@ public class SpectatorRole(IServiceProvider provider) : IRole {
private readonly IPlayerConverter<CCSPlayerController> playerConverter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
public string Id => "basegame.role.spectator";
public string Name
@@ -22,6 +25,14 @@ public class SpectatorRole(IServiceProvider provider) : IRole {
public IOnlinePlayer? FindPlayerToAssign(ISet<IOnlinePlayer> players) {
return players.FirstOrDefault(p
=> playerConverter.GetPlayer(p) is { Team: CsTeam.Spectator });
=> roles.GetRoles(p).Count == 0
&& playerConverter.GetPlayer(p) is { Team: CsTeam.Spectator });
}
public void OnAssign(IOnlinePlayer player) {
var csPlayer = playerConverter.GetPlayer(player);
if (csPlayer is null) return;
csPlayer.CommitSuicide(false, true);
csPlayer.ChangeTeam(CsTeam.Spectator);
}
}

Binary file not shown.

View File

@@ -0,0 +1,9 @@
using CounterStrikeSharp.API.Core.Capabilities;
using MAULActainShared.plugin;
namespace TTT.CS2.ThirdParties.eGO;
public class EgoApi {
public static PluginCapability<IActain> MAUL { get; } =
new("maulactain:core");
}

View File

@@ -0,0 +1,81 @@
namespace TTT.CS2.Utils;
public static class TextCompass {
/// <summary>
/// Builds a compass line with at most one character for each of N, E, S, W.
/// 0° = North, 90° = East, angles increase clockwise.
/// </summary>
/// <param name="fov">Field of view in degrees [0..360].</param>
/// <param name="width">Output width in characters.</param>
/// <param name="direction">Facing direction in degrees.</param>
/// <param name="filler">Filler character for empty slots.</param>
/// <param name="targetDir"></param>
public static string GenerateCompass(float fov, int width, float direction,
char filler = '·', float? targetDir = null) {
if (width <= 0) return string.Empty;
fov = Math.Clamp(fov, 0.001f, 360f);
direction = Normalize(direction);
var buf = new char[width];
for (var i = 0; i < width; i++) buf[i] = filler;
var start = direction - fov / 2f; // left edge of view
var degPerChar = fov / width;
PlaceIfVisible('N', 0f);
PlaceIfVisible('E', 90f);
PlaceIfVisible('S', 180f);
PlaceIfVisible('W', 270f);
if (targetDir.HasValue) PlaceIfVisible('X', targetDir.Value);
return new string(buf);
void PlaceIfVisible(char c, float cardinalAngle) {
var delta = ForwardDelta(start, cardinalAngle); // [0..360)
if (delta < 0f || delta >= fov) return; // outside view
// Map degrees to nearest character cell
var idx = (int)MathF.Round(delta / degPerChar);
if (idx < 0) idx = 0;
if (idx >= width) idx = width - 1;
// Nudge left/right to avoid collisions when possible
if (buf[idx] == filler) {
buf[idx] = c;
return;
}
var maxRadius = Math.Max(idx, width - 1 - idx);
for (var r = 1; r <= maxRadius; r++) {
var left = idx - r;
if (left >= 0 && buf[left] == filler) {
buf[left] = c;
return;
}
var right = idx + r;
if (right < width && buf[right] == filler) {
buf[right] = c;
return;
}
}
// If no space, overwrite the original cell as a last resort
buf[idx] = c;
}
}
private static float Normalize(float angle) {
angle %= 360f;
return angle < 0 ? angle + 360f : angle;
}
// Delta moving forward from start to target, wrapped to [0..360)
private static float ForwardDelta(float start, float target) {
var s = Normalize(start);
var t = Normalize(target);
var d = t - s;
return d < 0 ? d + 360f : d;
}
}

View File

@@ -26,5 +26,12 @@
"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"
}
},
"CHEGrenadeProjectile_CreateFunc": {
"signatures": {
"library": "server",
"windows": "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 40 48 8B 6C 24 70",
"linux": "55 4C 89 C1 48 89 E5 41 57 49 89 D7"
}
}
}

View File

@@ -1,6 +1,6 @@
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game.lang;
using TTT.Game;
using TTT.Locale;
namespace TTT.CS2.lang;
@@ -9,9 +9,16 @@ public static class CS2Msgs {
public static IMsg ROLE_SPECTATOR
=> MsgFactory.Create(nameof(ROLE_SPECTATOR));
public static IMsg DEAD_MUTE_REMINDER
=> MsgFactory.Create(nameof(DEAD_MUTE_REMINDER));
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);
}
public static IMsg TRAITOR_CHAT_FORMAT(IOnlinePlayer player, string msg) {
return MsgFactory.Create(nameof(TRAITOR_CHAT_FORMAT), player.Name, msg);
}
}

View File

@@ -1,7 +1,10 @@
ROLE_SPECTATOR: "Spectator"
ROLE_SPECTATOR: "Spectator"
TRAITOR_CHAT_FORMAT: "{darkred}[TRAITORS] {red}{0}: {default}{1}"
TASER_SCANNED: "%PREFIX%You scanned {0}{grey}, they are %an% {1}{grey}!"
DNA_PREFIX: "{darkblue}D{blue}N{lightblue}A{grey} | {grey}"
DEAD_MUTE_REMINDER: "%PREFIX%You are dead and cannot be heard."
SHOP_ITEM_DNA: "DNA Scanner"
SHOP_ITEM_DNA_DESC: "Scan bodies to reveal the person who killed them."
SHOP_ITEM_DNA_SCANNED: "%DNA_PREFIX%You scanned {0}{1}'%s% {grey}body, their killer was {red}{2}{grey}."
@@ -25,5 +28,23 @@ SHOP_ITEM_POISON_SHOTS_DESC: "Your bullets are coated in a mildly poisonous subs
SHOP_ITEM_POISON_HIT: "%PREFIX% You hit {green}{0}{grey} with a {lightpurple}poison shot{grey}."
SHOP_ITEM_POISON_OUT: "%PREFIX% You are out of poison shots."
SHOP_ITEM_POISON_SMOKE: "Poison Smoke"
SHOP_ITEM_POISON_SMOKE_DESC: "Throw a grenade that releases poisonous gas."
SHOP_ITEM_ARMOR: "Armor with Helmet"
SHOP_ITEM_ARMOR_DESC: "Wear armor that reduces incoming damage."
SHOP_ITEM_ARMOR_DESC: "Wear armor that reduces incoming damage."
SHOP_ITEM_ONE_HIT_KNIFE: "One-Hit Knife"
SHOP_ITEM_ONE_HIT_KNIFE_DESC: "Your next knife hit will be a guaranteed kill."
SHOP_ITEM_COMPASS_PLAYER: "Player Compass"
SHOP_ITEM_COMPASS_PLAYER_DESC: "Reveals the direction that the nearest non-Traitor is in."
SHOP_ITEM_COMPASS_BODY: "Body Compass"
SHOP_ITEM_COMPASS_BODY_DESC: "Reveals the direction that the nearest unidentified body is in."
SHOP_ITEM_SILENT_AWP: "Silent AWP"
SHOP_ITEM_SILENT_AWP_DESC: "Receive a silenced AWP with limited ammo."
SHOP_ITEM_CLUSTER_GRENADE: "Cluster Grenade"
SHOP_ITEM_CLUSTER_GRENADE_DESC: "A grenade that splits into multiple smaller grenades."

View File

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

View File

@@ -15,7 +15,9 @@ public class IdentifyBodyAction(IRoleAssigner roles, BodyIdentifyEvent ev)
#endregion
public IPlayer Player { get; } = ev.Identifier;
public IPlayer Player { get; } =
ev.Identifier ?? throw new InvalidOperationException();
public IPlayer? Other { get; } = ev.Body.OfPlayer;
public IRole? PlayerRole { get; } =

View File

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

View File

@@ -1,7 +1,9 @@
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Game;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.Locale;
namespace TTT.Game.Commands;
@@ -9,22 +11,38 @@ public class LogsCommand(IServiceProvider provider) : ICommand {
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
private readonly IIconManager? icons = provider.GetService<IIconManager>();
private readonly IMsgLocalizer localizer =
provider.GetRequiredService<IMsgLocalizer>();
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
public void Dispose() { }
public string[] RequiredFlags => ["@ttt/admin"];
public bool MustBeOnMainThread => true;
public string Id => "logs";
public void Start() { }
// TODO: Restrict and verbalize usage
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (games.ActiveGame is not {
State: State.IN_PROGRESS or State.FINISHED
}) {
info.ReplySync("No active game to show logs for.");
messenger.Message(executor, localizer[GameMsgs.GAME_LOGS_NONE]);
return Task.FromResult(CommandResult.ERROR);
}
if (executor is { IsAlive: true }) {
messenger.MessageAll(localizer[GameMsgs.LOGS_VIEWED_ALIVE(executor)]);
} else if (icons != null && executor != null) {
icons.SetVisiblePlayers(executor, ulong.MaxValue);
messenger.Message(executor, localizer[GameMsgs.LOGS_VIEWED_INFO]);
}
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