├── .github ├── CONTRIBUTING.md └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── Resources ├── ASITE.png ├── BSITE.png ├── RetakesAllocator_gamedata.json ├── left.gif ├── right.gif └── tab.gif ├── RetakesAllocator ├── CustomGameData.cs ├── Helpers.cs ├── Managers │ ├── AbstractVoteManager.cs │ └── NextRoundVoteManager.cs ├── Menus │ ├── AdvancedGunMenu.cs │ ├── GunsMenu.cs │ ├── Interfaces │ │ └── AbstractBaseMenu.cs │ ├── MenuManager.cs │ └── VoteMenu.cs ├── RetakesAllocator.cs ├── RetakesAllocator.csproj ├── lang │ ├── en.json │ ├── pt-BR.json │ ├── pt-PT.json │ └── zh-Hans.json └── release.bash ├── RetakesAllocatorCore ├── Config │ ├── Configs.cs │ └── ZeusPreference.cs ├── Db │ ├── Db.cs │ ├── Queries.cs │ └── UserSetting.cs ├── Log.cs ├── Managers │ └── RoundTypeManager.cs ├── Migrations │ ├── 20240105045524_InitialCreate.Designer.cs │ ├── 20240105045524_InitialCreate.cs │ ├── 20240105050248_DontAutoIncrement.Designer.cs │ ├── 20240105050248_DontAutoIncrement.cs │ ├── 20240116025022_BigIntTime.Designer.cs │ ├── 20240116025022_BigIntTime.cs │ └── DbModelSnapshot.cs ├── NadeHelpers.cs ├── OnRoundPostStartHelper.cs ├── OnWeaponCommandHelper.cs ├── PluginInfo.cs ├── RetakesAllocatorCore.csproj ├── RoundType.cs ├── Translator.cs ├── Utils.cs └── WeaponHelpers.cs ├── RetakesAllocatorTest ├── ConfigTests.cs ├── DbTests.cs ├── GlobalSetup.cs ├── GlobalUsings.cs ├── NadeAllocationTests.cs ├── RetakesAllocatorTest.csproj ├── RoundStartTests.cs ├── RoundTypeTests.cs ├── TestConstants.cs ├── WeaponHelpersTests.cs └── WeaponSelectionTests.cs └── cs2-retakes-allocator.sln /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Hey there, thanks for wanting to contribute to this project! Im accepting new issues and PRs, but before you make a large change, please reach out to me either via an issue on this repo, or on [discord in the CounterStrikeSharp plugin thread](https://canary.discord.com/channels/1160907911501991946/1193311939862986792) before getting started to make sure theres no wasted work. Thanks! 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build RetakesAllocator.zip 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | release: 9 | types: 10 | - created 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Setup .NET 18 | uses: actions/setup-dotnet@v4 19 | with: 20 | dotnet-version: 8.0.x 21 | - name: Restore Dependencies 22 | run: dotnet restore 23 | - name: Test 24 | run: dotnet test 25 | - name: Build 26 | shell: bash 27 | run: dotnet build --no-restore -c Release 28 | - name: Package 29 | working-directory: ${{github.workspace}}/RetakesAllocator/ 30 | run: chmod +x release.bash; ./release.bash 31 | - name: Publish Archive for Branch 32 | uses: actions/upload-artifact@v4 33 | if: "!startsWith(github.ref, 'refs/tags/')" 34 | with: 35 | name: RetakesAllocator-${{github.sha}} 36 | path: ${{github.workspace}}/RetakesAllocator/bin/Release/RetakesAllocator 37 | - name: Publish Archive for Release 38 | uses: actions/upload-artifact@v4 39 | if: startsWith(github.ref, 'refs/tags/') 40 | with: 41 | name: RetakesAllocator-${{github.ref_name}} 42 | path: ${{github.workspace}}/RetakesAllocator/bin/Release/RetakesAllocator 43 | release: 44 | needs: build 45 | permissions: write-all 46 | runs-on: ubuntu-latest 47 | if: github.event_name == 'release' 48 | steps: 49 | - uses: actions/download-artifact@v4 50 | name: Fetch Artifact 51 | with: 52 | name: RetakesAllocator-${{github.ref_name}} 53 | path: ${{github.workspace}}/RetakesAllocator/ 54 | - name: Create Archive 55 | run: zip -r RetakesAllocator-${{github.ref_name}}.zip RetakesAllocator 56 | - name: Get Release Info 57 | run: | 58 | RELEASE_INFO=$(curl -sH 'Accept: application/vnd.github.v3+json' https://api.github.com/repos/${{ github.repository }}/releases) 59 | export UPLOAD_URL=$(echo $RELEASE_INFO | jq -r ".[] | select(.tag_name == \"${{ github.event.release.tag_name }}\").upload_url") 60 | echo "UPLOAD_URL=$UPLOAD_URL" >> $GITHUB_ENV 61 | - name: Upload Release Asset 62 | uses: actions/upload-release-asset@v1 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | with: 66 | upload_url: ${{ env.UPLOAD_URL }} 67 | asset_path: ./RetakesAllocator-${{github.ref_name}}.zip 68 | asset_name: "cs2-retakes-allocator-${{ github.event.release.tag_name }}.zip" 69 | asset_content_type: application/zip 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | /packages/ 4 | riderModule.iml 5 | /_ReSharper.Caches/ 6 | .idea 7 | *.DotSettings.user 8 | .vs/ 9 | .vs-code/ 10 | data.db 11 | *.sln -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CS2 Retakes Allocator 2 | 3 | [![Build RetakesAllocator.zip](https://github.com/yonilerner/cs2-retakes-allocator/actions/workflows/build.yml/badge.svg)](https://github.com/yonilerner/cs2-retakes-allocator/actions/workflows/build.yml) 4 | 5 | ## Retakes 6 | 7 | This plugin is made to run alongside B3none's retakes implementation: https://github.com/b3none/cs2-retakes 8 | 9 | ## Installation 10 | 11 | - Ensure you have https://github.com/b3none/cs2-retakes installed already 12 | - Update the `RetakesPlugin` config to have `EnableFallbackAllocation` disabled 13 | - Download a release from https://github.com/yonilerner/cs2-retakes-allocator/releases 14 | - Extract the zip archive and upload the `RetakesAllocator` plugin to your CounterStrikeSharp plugins folder on your 15 | server 16 | - Each build comes with two necessary runtimes for sqlite3, one for linux64 and one for win64. If you need a 17 | different runtime, please submit an issue and I can provide more runtimes 18 | - If you're wondering why so many DLLs in the build: They are necessary for the Entity Framework that enables modern 19 | interfaces for databases 20 | - [Buy Menu - Optional] If you want buy menu weapon selection to work, ensure the following convars are set at the 21 | bottom of `game/csgo/cfg/cs2-retakes/retakes.cfg`: 22 | - `mp_buy_anywhere 1` 23 | - `mp_buytime 60000` 24 | - `mp_maxmoney 65535` 25 | - `mp_startmoney 65535` 26 | - `mp_afterroundmoney 65535` 27 | - More info about this in the "Buy Menu" section below 28 | 29 | ## Game Data/Signatures (Optional Information) 30 | This section is optional. If you dont care, you can skip it. The defaults are fine. 31 | 32 | This plugin relies on some function signatures that: 33 | - Regularly break with game updates: 34 | - `GetCSWeaponDataFromKey` 35 | - `CCSPlayer_ItemServices_CanAcquire` 36 | - Are not included in the default CS# signatures: 37 | - `GiveNamedItem2` 38 | 39 | Custom game data signatures are maintained in https://github.com/yonilerner/cs2-retakes-allocator/blob/main/Resources/RetakesAllocator_gamedata.json. There are a few ways to keep these up to date on your server: 40 | - If you want the plugin to automatically download the signatures, you can do so by running the plugin with the `AutoUpdateSignatures` config set to `true`. **This is the recommended approach**. See more below in the "Configuration" section. 41 | - If you want to manually download the signatures, you can do so by downloading the `RetakesAllocator_gamedata.json` file from Github and placing it in the `RetakesAllocator/gamedata` folder in the plugin. You may have to create that folder if it does not exist. 42 | 43 | If you do not want to use any custom game data/signatures, you can disable `AutoUpdateSignatures` and `CapabilityWeaponPaints`. If you do this (and if you previously had downloaded custom game data, make sure to delete the `RetakesAllocator/gamedata/RetakesAllocator_gamedata.json` file), the plugin will fallback to using the default CS# signatures. See more below in the "Configuration" section. 44 | 45 | ## Usage 46 | 47 | ### Round Types 48 | 49 | This plugin implements 3 different round types: 50 | 51 | - Pistol 52 | - Weapons: Only pistols 53 | - Armor: Kevlar and no helmet 54 | - Util: Flash or smoke, except one CT that gets a defuse kit 55 | - HalfBuy (shotguns and SMGs) 56 | - Weapons: Shotguns and SMGs 57 | - Armor: Kevlar and helmet 58 | - Util: One nade + 50% chance of a 2nd nade. Every CT has a defuse kit 59 | - FullBuy (Rifles, snipers, machine guns) 60 | - Weapons: Rifles, snipers, machine guns 61 | - Armor: Kevlar and helmet 62 | - Util: One nade + 50% chance of a 2nd nade. Every CT has a defuse kit 63 | 64 | How these round types are chosen can be configured. See more in the "Configuration" section below. 65 | 66 | ### Weapon Preferences 67 | 68 | There are a few different ways to set weapon preferences: 69 | 70 | - Built-in buy menu (See "Buy Menu" section for more info on how to set that up) 71 | - `!gun ` - Set a preference for a particular gun (will automatically figure out the round type) 72 | - `!awp` - Toggles if you want an AWP or not. 73 | - `!guns` - Opens a chat-based menu for weapon preferences 74 | 75 | See more info below about the commands in the "Commands" section. 76 | 77 | #### AWP Queue 78 | 79 | Currently one AWPer will be selected per team as long as at least one person on the team has chosen to get an AWP. AWP 80 | queue features will be expanded over time, you can take a look at existing Github Issues to see what has been proposed 81 | so far. 82 | 83 | ### Buy Menu 84 | 85 | If the convars are set to give players money and let them buy, player weapon choices can be selected via the buy menu. 86 | The buy menu will look like it allows you to buy any weapon, but it will only let you have weapons that are appropriate 87 | for the current round type. 88 | 89 | The convars can be tweaked to customize the experience. For example, if you dont want to allow people to use the buy 90 | menu the entire round, you can tweak the `mp_buytime` variable as you see fit. 91 | 92 | ### Configuration 93 | 94 | The config file is located in the plugin folder under `config/config.json`. 95 | 96 | #### Round Type Configuration 97 | 98 | - `RoundTypeSelection`: Which round type selection system to use. The options are: 99 | - `Random`: Randomly select a round based on the percentages set in `RoundTypePercentages` 100 | - `RandomFixedCounts`: Every round will be a random selection based on a fixed set of rounds per type, configured 101 | by `RoundTypeRandomFixedCounts`. 102 | - `ManualOrdering`: The round will follow the exact order you specify. 103 | - `RoundTypePercentages`: The frequency of each type of round. The values must add up to `100`. 104 | - Only used when `RoundTypeSelection` is `Random` 105 | - `RoundTypeRandomFixedCounts`: The fixed counts for each type of round. For example, if your config 106 | is `{"Pistol": 5, "HalfBuy": 10, "FullBuy": 15}`, then over the next 30 rounds, exactly 5 of them will be pistols, 10 107 | will be half buys, and 15 will be full buys, but the exact ordering of the rounds will be random. 108 | - Only used when `RoundTypeSelection` is `RandomFixedCounts` 109 | - The random ordering will restart back at the beginning if the map is not over. 110 | - A new random ordering will be selected at the start of each map. 111 | - `RoundTypeManualOrdering`: The exact order of rounds and how many of each round in that order. For example, if your 112 | config 113 | is `[{"Type": "Pistol", "Count": 5}, {"Type": "FullBuy", "Count": 25}, {"Type": "Pistol", "Count": 1}]`, then you will 114 | get 5 pistol rounds, 25 full buy rounds, a single pistol round, and then it will start from the beginning again. A new 115 | map always starts from the beginning. 116 | 117 | #### Weapon Configuration 118 | 119 | For any of the weapon configs, the valid weapon names come 120 | from [here](https://github.com/roflmuffin/CounterStrikeSharp/blob/main/managed/CounterStrikeSharp.API/Modules/Entities/Constants/CsItem.cs). 121 | For example in 122 | 123 | ```cs 124 | [EnumMember(Value = "item_kevlar")] 125 | Kevlar = 000, 126 | ``` 127 | 128 | `Kevlar` is the name of the weapon, not `item_kevlar`. 129 | In 130 | 131 | ```cs 132 | [EnumMember(Value = "weapon_m4a1_silencer")] 133 | M4A1S = 401, 134 | SilencedM4 = M4A1S, 135 | ``` 136 | 137 | both `M4A1S` and `SilencedM4` are valid weapon names, but `weapon_m4a1_silencer` is not. 138 | 139 | Here are the weapon configs: 140 | 141 | - `UsableWeapons`: The weapons that can be allocated. Any weapon removed from this list cannot be used. 142 | - `DefaultWeapons`: This lets you configure the default weapon for each weapon allocation type. The type of this config 143 | is map of `Team => WeaponAllocationType => Item`. 144 | - The valid keys for `DefaultWeapons` are: `Terrorist` and `CounterTerrorist` 145 | - Under each of those, the valid keys are: 146 | - `PistolRound`: The pistol round pistol 147 | - `Secondary`: The pistol for non-pistol rounds 148 | - `HalfBuyPrimary`: The primary weapon for half buy rounds 149 | - `FullBuyPrimary`: The primary weapon for full buy rounds 150 | - The valid values for each subkey this are any `CsItem` that is a weapon. 151 | To better understand how `DefaultWeapons` works, here is the default config for `DefaultWeapons` as an example: 152 | ```json 153 | { 154 | "DefaultWeapons": { 155 | "Terrorist": { 156 | "PistolRound": "Glock", 157 | "Secondary": "Deagle", 158 | "HalfBuyPrimary": "Mac10", 159 | "FullBuyPrimary": "AK47" 160 | }, 161 | "CounterTerrorist": { 162 | "PistolRound": "USPS", 163 | "Secondary": "Deagle", 164 | "HalfBuyPrimary": "MP9", 165 | "FullBuyPrimary": "M4A1" 166 | } 167 | } 168 | } 169 | ``` 170 | 171 | 172 | - `ZeusPreference`: Whether or not to give a Zeus. Options are `Always` or `Never`. Defaults to `Never`. 173 | - `AllowPreferredWeaponForEveryone`: If `true`, everyone can get the AWP. This overrides every other "preferred" weapon 174 | setting. Defaults to `false`. 175 | - `MaxPreferredWeaponsPerTeam`: The maximum number of AWPs for each team. 176 | - `MinPlayersPerTeamForPreferredWeapon`: The minimum number of players on each team necessary for someone to get an AWP. 177 | - `ChanceForPreferredWeapon`: The % chance that the round will have an AWP. 178 | 179 | #### Nade Configuration 180 | 181 | - `MaxNades`: You can set the maximum number of each type of nade for each team and on each map (or default). By default 182 | the config includes some limits that you may want to change. 183 | The way `MaxNades` works is that the GLOBAL option sets the max for *all* maps, and then you can also specify subsets 184 | of the config for specific maps. 185 | For example, if your config is: 186 | 187 | ```json 188 | { 189 | "MaxNades": { 190 | "GLOBAL": { 191 | "Terrorist": { 192 | "Flashbang": 2, 193 | "Smoke": 1, 194 | "Molotov": 1, 195 | "HighExplosive": 1 196 | }, 197 | "CounterTerrorist": { 198 | "Flashbang": 2, 199 | "Smoke": 1, 200 | "Molotov": 2, 201 | "HighExplosive": 1 202 | } 203 | } 204 | } 205 | } 206 | ``` 207 | 208 | but you specifically want to allow 2 smokes for CT on mirage, you can do: 209 | 210 | ```json 211 | { 212 | "MaxNades": { 213 | "GLOBAL": { 214 | "Terrorist": { 215 | "Flashbang": 2, 216 | "Smoke": 1, 217 | "Molotov": 1, 218 | "HighExplosive": 1 219 | }, 220 | "CounterTerrorist": { 221 | "Flashbang": 2, 222 | "Smoke": 1, 223 | "Incendiary": 2, 224 | "HighExplosive": 1 225 | } 226 | }, 227 | "de_mirage": { 228 | "CounterTerrorist": { 229 | "Smoke": 2 230 | } 231 | } 232 | } 233 | } 234 | ``` 235 | 236 | This will keep the defaults the same for everything but override just CT smokes on mirage. 237 | 238 | The valid keys for nades on `Terrorist` are: 239 | 240 | - `Flashbang` 241 | - `Smoke` 242 | - `Molotov` 243 | - `HighExplosive` 244 | 245 | The valid keys for nades on `CounterTerrorist` are: 246 | 247 | - `Flashbang` 248 | - `Smoke` 249 | - `Incendiary` 250 | - `HighExplosive` 251 | 252 | If you mix up `Incendiary` and `Molotov`, the plugin will fix it for you. 253 | 254 | - `MaxTeamNades` - This config works similarly to `MaxNades`, except it affects the max number of nades an entire team 255 | can have. The structure is the same as `MaxNades` except that after the map and team keys, it maps a round type to a 256 | max nade setting. The possible max nade settings are: 257 | - `One`, `Two`, ... until `Ten` 258 | - `AveragePointFivePerPlayer` (rounds up) 259 | - `AverageOnePerPlayer` (rounds up) 260 | - `AverageOnePointFivePerPlayer` (rounds up) 261 | - `AverageTwoPerPlayer` (rounds up) 262 | - `None` 263 | 264 | *NOTE: There is a bug right now where the plugin will not always give the maximum number of nades, even if players have 265 | room for it*. 266 | 267 | #### Other Configuration 268 | 269 | - `EnableNextRoundTypeVoting`: Whether to allow voting for the next round type via `!nextround`. `false` by default. 270 | - `NumberOfExtraVipChancesForPreferredWeapon`: When randomly selecting preferred weapons per team (ie. "AWP queue"), how 271 | many extra chances should VIPs get. 272 | - The default is 1, meaning VIPs will get 1 extra chance. For example, lets say 273 | there are 3 players on the team and this config is set to 1. Normally each person would have a 33% chance of 274 | getting 275 | the AWP, but in this case, since one of the players is a VIP, the VIP will get a 50% chance of getting the AWP, 276 | and 277 | the other two players will each have 25% chance of getting the AWP. 278 | - If you set this to 0, there will be no preference for VIPs. 279 | - If you set this to -1, only VIPs can get the AWP 280 | - `ChanceForPreferredWeapon`: This allows you to determine chance of players getting preferred weapon. (ie. 100 = %100, 50 = %50) 281 | - `AllowedWeaponSelectionTypes`: The types of weapon allocation that are allowed. 282 | - Choices: 283 | - `PlayerChoice` - Allow players to choose their preferences for the round type 284 | - `Random` - Everyone gets a random weapon for the round type 285 | - `Default` - Everyone gets a default weapon for the round type. The defaults are: 286 | - T Pistol: Glock 287 | - CT Pistol: USPS 288 | - T HalfBuy: Mac10 289 | - CT HalfBuy: MP9 290 | - T Rifle: AK47 291 | - CT Rifle: M4A4 292 | - These will be tried in order of `PlayerChoice`, `Random`, and `Default`. If a player preference is not available, 293 | or this type is removed from the config, a random weapon will be tried. If random weapons are removed from the 294 | config, a default weapon will be tried. If default weapons are removed from the config, no weapons will be 295 | allocated. 296 | - `DatabaseProvider`: Which database provider you want to use. The default is `Sqlite`, which requires no setup. The 297 | available options are: 298 | - `Sqlite` 299 | - `MySql` 300 | - `DatabaseConnectionString`: How you connect to the database 301 | - The connection string for `Sqlite` probably doesnt need to be changed from the default, but you can change it if 302 | you want the db file to be in a different location. 303 | - More info on formatting the string here: https://www.connectionstrings.com/sqlite/ 304 | - The connection string for `MySql` should be configured per instructions 305 | here: https://www.connectionstrings.com/mysql/ 306 | - `LogLevel`: Desired level of logging. Can be set to `Debug` or `Trace` when collecting information for a bug report. 307 | You probably want the default, which is `Information`. I strongly recommend against setting any higher than `Warning`. 308 | The options are: 309 | - `Trace` (spam of information) 310 | - `Debug` (some useful information for debugging) 311 | - `Information` 312 | - `Warning` (warnings & errors only) 313 | - `Error` (errors only) 314 | - `Critical` 315 | - `None` (no logs at all; use with caution) 316 | - `ChatMessagePluginName`: The name that you want to appear between [] in chat messages from the plugin. Defaults 317 | to `Retakes`. 318 | - For example, `[Retakes] Next round will be a Terrorist round.` 319 | - `ChatMessagePluginPrefix`: The *entire* prefix that appears in front of chat messages. If set, this 320 | overrides `ChatMessagePluginName`. If you want the prefix to be colored, the config must also specify the colors. It 321 | must also specify a space after the prefix if you want one. 322 | - `MigrateOnStartup`: Whether or not to migrate the database on startup. This defaults to yes for now, but production 323 | servers may want to change this to false so they can control when database migrations are applied. 324 | - `EnableRoundTypeAnnouncement`: Whether or not to announce the round type. 325 | - `EnableRoundTypeAnnouncementCenter`: Whether or not to announce the round type in the center of the users screen. Only 326 | applies if `EnableRoundTypeAnnouncement` is also set to `true`. 327 | - `UseOnTickFeatures`: Set to false if you want better performance and dont want any OnTick features, including: 328 | - Bombsite center announcement 329 | - Advanced gun menu 330 | - `AutoUpdateSignatures `: When true, the plugin will always try to download the latest custom game data/signatures on startup. A game server restart may be required to apply the new signatures after they have been downloaded. If this is disabled, the plugin will fallback to using the default CS# game data/signatures. 331 | - `CapabilityWeaponPaints`: When true, will try to use the custom game data `GiveNamedItem2` that will maintain weapon paints in non-standard situations. This is enabled by default for backwards compatibility, but is less stable. If this option is enabled, `AutoUpdateSignatures` should also be enabled. If you dont want to use `AutoUpdateSignatures`, at least ensure that the custom game data/signatures are updated correctly, since this `GiveNamedItem2` is not in the default game data/signatures. 332 | 333 | ### Commands 334 | 335 | You can use the following commands to select specific weapon preferences per-user: 336 | 337 | - `!gun [T|CT]` - Set a preference the chosen weapon for the team you are currently on, or T/CT if provided 338 | - For example, if you are currently a terrorist and you do `!gun galil`, your preference for rifle rounds will be 339 | Galil 340 | - `!guns` - Opens up a chat-based menu for setting weapon preferences. 341 | - `!awp` - Toggle whether or not you want to get an AWP. 342 | - `!removegun [T|CT]` - Remove a preference for the chosen weapon for the team you are currently on, or T/CT if 343 | provided 344 | - For example, if you previously did `!gun galil` while a terrorist, and you do `!removegun galil` while a 345 | terrorist, you will no longer prefer the galil, and will instead get a random weapon 346 | - `!nextround` - Vote for the next round type. Can be enabled with the `EnableNextRoundTypeVoting` config, which 347 | is `false` by default. 348 | - `!setnextround ` - For admins only. Force the next round to be the selected type. 349 | - `!reload_allocator_config` - For admins only. Reload the JSON config in-place. 350 | - `!print_config ` - For admins only. Print out the config with the given name. 351 | 352 | # Building 353 | 354 | To automatically copy the built DLL to your running server location, set the build variable `CopyPath` to the folder 355 | where the mod should be copied to. *This only works on Windows.* 356 | 357 | Notes: 358 | 359 | - Run the dedicated server 360 | with `start cs2.exe -dedicated -insecure +game_type 0 +game_mode 0 +map de_dust2 +servercfgfile server.cfg` 361 | -------------------------------------------------------------------------------- /Resources/ASITE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yonilerner/cs2-retakes-allocator/f622e51a8e20c5622794fb9e2f806d42bf5d04b8/Resources/ASITE.png -------------------------------------------------------------------------------- /Resources/BSITE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yonilerner/cs2-retakes-allocator/f622e51a8e20c5622794fb9e2f806d42bf5d04b8/Resources/BSITE.png -------------------------------------------------------------------------------- /Resources/RetakesAllocator_gamedata.json: -------------------------------------------------------------------------------- 1 | { 2 | "GetCSWeaponDataFromKey": { 3 | "signatures": { 4 | "library": "server", 5 | "windows": "48 89 5C 24 ? 48 89 74 24 ? 57 48 83 EC ? 48 8B FA 8B F1 48 85 D2 0F 84", 6 | "linux": "55 48 89 E5 41 57 41 56 41 89 FE 41 55 41 54 45" 7 | } 8 | }, 9 | "CCSPlayer_ItemServices_CanAcquire": { 10 | "signatures": { 11 | "library": "server", 12 | "windows": "44 89 44 24 ? 48 89 54 24 ? 48 89 4C 24 ? 55 56 57 41 54 41 55 41 56 41 57 48 8B EC", 13 | "linux": "55 48 89 E5 41 57 41 56 48 8D 45 ? 41 55 41 54 53 48 89 CB" 14 | } 15 | }, 16 | "GiveNamedItem2": { 17 | "signatures": { 18 | "library": "server", 19 | "windows": "48 83 EC ? 48 C7 44 24 ? ? ? ? ? 45 33 C9 45 33 C0 C6 44 24 ? ? E8 ? ? ? ? 48 83 C4 ? C3 CC CC CC CC CC CC CC CC CC CC CC CC CC CC 48 83 EC", 20 | "linux": "55 48 89 E5 41 57 41 56 41 55 41 54 53 48 83 EC ? 48 89 7D ? 44 89 45" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Resources/left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yonilerner/cs2-retakes-allocator/f622e51a8e20c5622794fb9e2f806d42bf5d04b8/Resources/left.gif -------------------------------------------------------------------------------- /Resources/right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yonilerner/cs2-retakes-allocator/f622e51a8e20c5622794fb9e2f806d42bf5d04b8/Resources/right.gif -------------------------------------------------------------------------------- /Resources/tab.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yonilerner/cs2-retakes-allocator/f622e51a8e20c5622794fb9e2f806d42bf5d04b8/Resources/tab.gif -------------------------------------------------------------------------------- /RetakesAllocator/CustomGameData.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using CounterStrikeSharp.API; 3 | using CounterStrikeSharp.API.Core; 4 | using RetakesAllocatorCore.Config; 5 | using RetakesAllocatorCore; 6 | using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions; 7 | using System.Text.Json; 8 | // ReSharper disable InconsistentNaming 9 | 10 | namespace RetakesAllocator; 11 | 12 | public class CustomGameData 13 | { 14 | private static readonly Dictionary> _customGameData = new(); 15 | private MemoryFunctionVoid? GiveNamedItem2; 16 | public MemoryFunctionWithReturn? CCSPlayer_ItemServices_CanAcquireFunc; 17 | public MemoryFunctionWithReturn? GetCSWeaponDataFromKeyFunc; 18 | 19 | public CustomGameData() 20 | { 21 | LoadCustomGameData(); 22 | } 23 | 24 | public void LoadCustomGameData() 25 | { 26 | if (Configs.Shared.Module == null) 27 | { 28 | Log.Error("Module path is null. Returning without loading custom game data."); 29 | return; 30 | } 31 | var jsonFilePath = Path.Combine(Configs.Shared.Module, "gamedata/RetakesAllocator_gamedata.json"); 32 | if (File.Exists(jsonFilePath)) 33 | { 34 | try 35 | { 36 | var jsonData = File.ReadAllText(jsonFilePath); 37 | var jsonDocument = JsonDocument.Parse(jsonData); 38 | 39 | foreach (var element in jsonDocument.RootElement.EnumerateObject()) 40 | { 41 | string key = element.Name; 42 | 43 | var platformData = new Dictionary(); 44 | 45 | if (element.Value.TryGetProperty("signatures", out var signatures)) 46 | { 47 | if (signatures.TryGetProperty("windows", out var windows)) 48 | { 49 | platformData[OSPlatform.Windows] = windows.GetString()!; 50 | } 51 | 52 | if (signatures.TryGetProperty("linux", out var linux)) 53 | { 54 | platformData[OSPlatform.Linux] = linux.GetString()!; 55 | } 56 | } 57 | _customGameData[key] = platformData; 58 | } 59 | } 60 | catch (Exception ex) 61 | { 62 | Log.Error($"Error loading custom game data: {ex.Message}"); 63 | } 64 | } 65 | else 66 | { 67 | Log.Debug($"JSON file does not exist at path: {jsonFilePath}. Returning without loading custom game data."); 68 | } 69 | 70 | try 71 | { 72 | GiveNamedItem2 = new(GetCustomGameDataKey("GiveNamedItem2")); 73 | } 74 | catch 75 | { 76 | // GiveNamedItem2 failing to load shouldnt crash because we will try to fallback to GiveNamedItem 77 | } 78 | GetCSWeaponDataFromKeyFunc = new(GetCustomGameDataKey("GetCSWeaponDataFromKey")); 79 | CCSPlayer_ItemServices_CanAcquireFunc = new(GetCustomGameDataKey("CCSPlayer_ItemServices_CanAcquire")); 80 | } 81 | 82 | private string GetCustomGameDataKey(string key) 83 | { 84 | if (!_customGameData.TryGetValue(key, out var customGameData)) 85 | { 86 | try 87 | { 88 | var defaultGameData = GameData.GetSignature(key); 89 | Log.Info($"Using default gamedata for {key} because no custom data was found."); 90 | return defaultGameData; 91 | } 92 | catch 93 | { 94 | // ignored 95 | } 96 | 97 | throw new Exception($"Invalid key {key}"); 98 | } 99 | 100 | OSPlatform platform; 101 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 102 | { 103 | platform = OSPlatform.Linux; 104 | } 105 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 106 | { 107 | platform = OSPlatform.Windows; 108 | } 109 | else 110 | { 111 | throw new Exception("Unsupported platform"); 112 | } 113 | 114 | return customGameData.TryGetValue(platform, out var customData) 115 | ? customData 116 | : throw new Exception($"Missing custom data for {key} on {platform}"); 117 | } 118 | 119 | public bool PlayerGiveNamedItemEnabled() 120 | { 121 | return GiveNamedItem2 != null; 122 | } 123 | 124 | public void PlayerGiveNamedItem(CCSPlayerController player, string item) 125 | { 126 | if (!player.PlayerPawn.IsValid) return; 127 | if (player.PlayerPawn.Value == null) return; 128 | if (!player.PlayerPawn.Value.IsValid) return; 129 | if (player.PlayerPawn.Value.ItemServices == null) return; 130 | 131 | // Log.Debug("Using custom function for GiveNamedItem2"); 132 | GiveNamedItem2?.Invoke(player.PlayerPawn.Value.ItemServices.Handle, item, 0, 0, 0, 0, 0, 0); 133 | } 134 | } 135 | 136 | // Possible results for CSPlayer::CanAcquire 137 | public enum AcquireResult 138 | { 139 | Allowed = 0, 140 | InvalidItem, 141 | AlreadyOwned, 142 | AlreadyPurchased, 143 | ReachedGrenadeTypeLimit, 144 | ReachedGrenadeTotalLimit, 145 | NotAllowedByTeam, 146 | NotAllowedByMap, 147 | NotAllowedByMode, 148 | NotAllowedForPurchase, 149 | NotAllowedByProhibition, 150 | }; 151 | 152 | // Possible results for CSPlayer::CanAcquire 153 | public enum AcquireMethod 154 | { 155 | PickUp = 0, 156 | Buy, 157 | }; 158 | -------------------------------------------------------------------------------- /RetakesAllocator/Helpers.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using CounterStrikeSharp.API; 3 | using CounterStrikeSharp.API.Core; 4 | using CounterStrikeSharp.API.Modules.Admin; 5 | using CounterStrikeSharp.API.Modules.Commands; 6 | using CounterStrikeSharp.API.Modules.Entities.Constants; 7 | using CounterStrikeSharp.API.Modules.Utils; 8 | using RetakesAllocatorCore.Config; 9 | using RetakesAllocatorCore; 10 | 11 | namespace RetakesAllocator; 12 | 13 | public static class Helpers 14 | { 15 | public static bool PlayerIsValid(CCSPlayerController? player) 16 | { 17 | return player is not null && player.IsValid; 18 | } 19 | 20 | public static void WriteNewlineDelimited(string message, Action writer) 21 | { 22 | foreach (var line in message.Split("\n")) 23 | { 24 | writer($"{PluginInfo.MessagePrefix}{line}"); 25 | } 26 | } 27 | 28 | public static ICollection CommandInfoToArgList(CommandInfo commandInfo, bool includeFirst = false) 29 | { 30 | var result = new List(); 31 | 32 | for (var i = includeFirst ? 0 : 1; i < commandInfo.ArgCount; i++) 33 | { 34 | result.Add(commandInfo.GetArg(i)); 35 | } 36 | 37 | return result; 38 | } 39 | 40 | public static ulong GetSteamId(CCSPlayerController? player) 41 | { 42 | if (!PlayerIsValid(player)) 43 | { 44 | return 0; 45 | } 46 | 47 | return player?.AuthorizedSteamID?.SteamId64 ?? 0; 48 | } 49 | 50 | public static CsTeam GetTeam(CCSPlayerController player) 51 | { 52 | return player.Team; 53 | } 54 | 55 | public static void RemoveArmor(CCSPlayerController player) 56 | { 57 | if (!PlayerIsValid(player) || player.PlayerPawn.Value?.ItemServices is null) 58 | { 59 | return; 60 | } 61 | 62 | var itemServices = new CCSPlayer_ItemServices(player.PlayerPawn.Value.ItemServices.Handle); 63 | itemServices.HasHelmet = false; 64 | itemServices.HasHeavyArmor = false; 65 | } 66 | 67 | public static CsItem? GetPlayerWeaponItem(CCSPlayerController player, Func pred) 68 | { 69 | if (!PlayerIsValid(player) || player.PlayerPawn.Value?.WeaponServices is null) 70 | { 71 | return null; 72 | } 73 | 74 | foreach (var weapon in player.PlayerPawn.Value.WeaponServices.MyWeapons) 75 | { 76 | if (weapon is not {IsValid: true, Value.IsValid: true}) 77 | { 78 | continue; 79 | } 80 | 81 | CsItem? item = Utils.ToEnum(weapon.Value.DesignerName); 82 | if (item is not null && pred(item.Value)) 83 | { 84 | return item; 85 | } 86 | } 87 | 88 | return null; 89 | } 90 | 91 | public static CHandle? GetPlayerWeapon(CCSPlayerController player, 92 | Func pred) 93 | { 94 | if (!PlayerIsValid(player) || player.PlayerPawn.Value?.WeaponServices is null) 95 | { 96 | return null; 97 | } 98 | 99 | foreach (var weapon in player.PlayerPawn.Value.WeaponServices.MyWeapons) 100 | { 101 | if (weapon is not {IsValid: true, Value.IsValid: true}) 102 | { 103 | continue; 104 | } 105 | 106 | CsItem? item = Utils.ToEnum(weapon.Value.DesignerName); 107 | if (item is not null && pred(weapon.Value, item.Value)) 108 | { 109 | return weapon; 110 | } 111 | } 112 | 113 | return null; 114 | } 115 | 116 | public static bool RemoveWeapons(CCSPlayerController player, Func? where = null) 117 | { 118 | if (!PlayerIsValid(player) || player.PlayerPawn.Value?.WeaponServices is null) 119 | { 120 | return false; 121 | } 122 | 123 | var removed = false; 124 | 125 | foreach (var weapon in player.PlayerPawn.Value.WeaponServices.MyWeapons) 126 | { 127 | // Log.Write($"want to remove wep {weapon.Value?.DesignerName} {weapon.IsValid}"); 128 | if (weapon is not {IsValid: true, Value.IsValid: true}) 129 | { 130 | continue; 131 | } 132 | 133 | CsItem? item = Utils.ToEnum(weapon.Value.DesignerName); 134 | // Log.Write($"item to remove: {item}"); 135 | 136 | if ( 137 | where is not null && 138 | (item is null || !where(item.Value)) 139 | ) 140 | { 141 | continue; 142 | } 143 | 144 | if (weapon.Value.DesignerName is "weapon_knife" or "weapon_knife_t") 145 | { 146 | continue; 147 | } 148 | 149 | // Log.Write($"Removing weapon {weapon.Value.DesignerName} {weapon.IsValid}"); 150 | 151 | Utilities.RemoveItemByDesignerName(player, weapon.Value.ToString()!, true); 152 | weapon.Value.Remove(); 153 | 154 | removed = true; 155 | } 156 | 157 | return removed; 158 | } 159 | 160 | private static CCSGameRules? GetGameRules() 161 | { 162 | try 163 | { 164 | var gameRulesEntities = Utilities.FindAllEntitiesByDesignerName("cs_gamerules"); 165 | return gameRulesEntities.First().GameRules; 166 | } 167 | catch 168 | { 169 | return null; 170 | } 171 | } 172 | 173 | public static bool IsWarmup() 174 | { 175 | return GetGameRules()?.WarmupPeriod ?? false; 176 | } 177 | 178 | public static bool IsWeaponAllocationAllowed() 179 | { 180 | return WeaponHelpers.IsWeaponAllocationAllowed(GetGameRules()?.FreezePeriod ?? false); 181 | } 182 | 183 | public static double GetVectorDistance(Vector v1, Vector v2) 184 | { 185 | var dx = v1.X - v2.X; 186 | var dy = v1.Y - v2.Y; 187 | 188 | return Math.Sqrt(Math.Pow(dx, 2) + Math.Pow(dy, 2)); 189 | } 190 | 191 | public static int GetNumPlayersOnTeam() 192 | { 193 | return Utilities.GetPlayers() 194 | .Where(player => player.IsValid) 195 | .Where(player => player.Team is CsTeam.Terrorist or CsTeam.CounterTerrorist).ToList() 196 | .Count; 197 | } 198 | 199 | public static bool IsWindows() 200 | { 201 | return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 202 | } 203 | 204 | public static bool IsVip(CCSPlayerController player) => AdminManager.PlayerHasPermissions(player, "@css/vip"); 205 | 206 | public static async Task DownloadMissingFiles() 207 | { 208 | if (!Configs.GetConfigData().AutoUpdateSignatures) 209 | { 210 | return false; 211 | } 212 | string baseFolderPath = Configs.Shared.Module!; 213 | 214 | string gamedataFileName = "gamedata/RetakesAllocator_gamedata.json"; 215 | string gamedataGithubUrl = "https://raw.githubusercontent.com/yonilerner/cs2-retakes-allocator/main/Resources/RetakesAllocator_gamedata.json"; 216 | string gamedataFilePath = Path.Combine(baseFolderPath, gamedataFileName); 217 | string gamedataDirectoryPath = Path.GetDirectoryName(gamedataFilePath)!; 218 | 219 | return await CheckAndDownloadFile(gamedataFilePath, gamedataGithubUrl, gamedataDirectoryPath); 220 | } 221 | 222 | private static async Task CheckAndDownloadFile(string filePath, string githubUrl, string directoryPath) 223 | { 224 | if (!File.Exists(filePath)) 225 | { 226 | if (!Directory.Exists(directoryPath)) 227 | { 228 | Directory.CreateDirectory(directoryPath); 229 | } 230 | await DownloadFileFromGithub(githubUrl, filePath); 231 | return true; 232 | } 233 | 234 | bool isFileDifferent = await IsFileDifferent(filePath, githubUrl); 235 | if (isFileDifferent) 236 | { 237 | File.Delete(filePath); 238 | await DownloadFileFromGithub(githubUrl, filePath); 239 | return true; 240 | } 241 | 242 | return false; 243 | } 244 | 245 | private static async Task IsFileDifferent(string localFilePath, string githubUrl) 246 | { 247 | try 248 | { 249 | byte[] localFileBytes = await File.ReadAllBytesAsync(localFilePath); 250 | string localFileHash = GetFileHash(localFileBytes); 251 | 252 | using (HttpClient client = new HttpClient()) 253 | { 254 | byte[] githubFileBytes = await client.GetByteArrayAsync(githubUrl); 255 | string githubFileHash = GetFileHash(githubFileBytes); 256 | return localFileHash != githubFileHash; 257 | } 258 | } 259 | catch (Exception ex) 260 | { 261 | Log.Warn($"Error comparing files: {ex.Message}"); 262 | return false; 263 | } 264 | } 265 | 266 | private static string GetFileHash(byte[] fileBytes) 267 | { 268 | using (var md5 = System.Security.Cryptography.MD5.Create()) 269 | { 270 | byte[] hashBytes = md5.ComputeHash(fileBytes); 271 | return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); 272 | } 273 | } 274 | 275 | private static async Task DownloadFileFromGithub(string url, string destinationPath) 276 | { 277 | using (HttpClient client = new HttpClient()) 278 | { 279 | try 280 | { 281 | byte[] fileBytes = await client.GetByteArrayAsync(url); 282 | await File.WriteAllBytesAsync(destinationPath, fileBytes); 283 | } 284 | catch (Exception ex) 285 | { 286 | Log.Warn($"Error downloading file: {ex.Message}"); 287 | } 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /RetakesAllocator/Managers/AbstractVoteManager.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API; 2 | using CounterStrikeSharp.API.Core; 3 | using RetakesAllocatorCore; 4 | using Timer = CounterStrikeSharp.API.Modules.Timers.Timer; 5 | 6 | namespace RetakesAllocator.Managers; 7 | 8 | public abstract class AbstractVoteManager 9 | { 10 | protected const float VoteTimeout = 30.0f; 11 | protected const float EnoughPlayersVotedThreshold = 0.5f; 12 | 13 | private readonly string _voteFor; 14 | private readonly string _voteCommand; 15 | private Timer? _voteTimer; 16 | private readonly Dictionary _votes = new(); 17 | 18 | protected AbstractVoteManager(string voteFor, string voteCommand) 19 | { 20 | _voteFor = voteFor; 21 | _voteCommand = voteCommand; 22 | } 23 | 24 | public abstract IEnumerable GetVoteOptions(); 25 | 26 | protected abstract void HandleVoteResult(string result); 27 | 28 | public void CastVote(CCSPlayerController player, string vote) 29 | { 30 | if (_voteTimer == null) 31 | { 32 | _voteTimer = new Timer(VoteTimeout, CompleteVote); 33 | 34 | PrintToServer($"A vote has been started! Type {_voteCommand} to vote!"); 35 | } 36 | 37 | _votes[player] = vote; 38 | PrintToPlayer(player, "Your vote has been registered!"); 39 | } 40 | 41 | public void CompleteVote() 42 | { 43 | if (_voteTimer is null) 44 | { 45 | return; 46 | } 47 | _voteTimer.Kill(); 48 | _voteTimer = null; 49 | 50 | var countedVotes = new Dictionary(); 51 | 52 | foreach (var (player, vote) in _votes) 53 | { 54 | if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected) 55 | { 56 | continue; 57 | } 58 | 59 | if (!countedVotes.TryAdd(vote, 1)) 60 | { 61 | countedVotes[vote]++; 62 | } 63 | } 64 | 65 | _votes.Clear(); 66 | 67 | var highestScore = 0; 68 | var highestVoted = new HashSet(); 69 | foreach (var (vote, count) in countedVotes) 70 | { 71 | if (count > highestScore) 72 | { 73 | highestScore = count; 74 | highestVoted.Clear(); 75 | highestVoted.Add(vote); 76 | } 77 | else if (count == highestScore) 78 | { 79 | highestVoted.Add(vote); 80 | } 81 | } 82 | 83 | var numPlayers = Helpers.GetNumPlayersOnTeam(); 84 | if (numPlayers == 0 || (float) highestScore / numPlayers < EnoughPlayersVotedThreshold) 85 | { 86 | PrintToServer("Vote failed: Not enough players voted!"); 87 | return; 88 | } 89 | 90 | var random = new Random(); 91 | var chosenVote = highestVoted.ElementAt(random.Next(highestVoted.Count)); 92 | 93 | HandleVoteResult(chosenVote); 94 | } 95 | 96 | public string VoteMessagePrefix => $"{PluginInfo.MessagePrefix}[Voting for {_voteFor}] "; 97 | 98 | protected void PrintToPlayer(CCSPlayerController player, string message) 99 | { 100 | player.PrintToChat($"{VoteMessagePrefix}{message}"); 101 | } 102 | 103 | protected void PrintToServer(string message) 104 | { 105 | Server.PrintToChatAll($"{VoteMessagePrefix}{message}"); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /RetakesAllocator/Managers/NextRoundVoteManager.cs: -------------------------------------------------------------------------------- 1 | using RetakesAllocatorCore; 2 | using RetakesAllocatorCore.Managers; 3 | 4 | namespace RetakesAllocator.Managers; 5 | 6 | public class NextRoundVoteManager : AbstractVoteManager 7 | { 8 | private readonly IEnumerable _options = RoundTypeHelpers 9 | .GetRoundTypes() 10 | .Select(r => r.ToString()); 11 | 12 | public NextRoundVoteManager() : base("the next round", "!nextround") 13 | { 14 | } 15 | 16 | 17 | public override IEnumerable GetVoteOptions() 18 | { 19 | return _options; 20 | } 21 | 22 | protected override void HandleVoteResult(string option) 23 | { 24 | RoundTypeManager.Instance.SetNextRoundTypeOverride(RoundTypeHelpers.ParseRoundType(option)); 25 | PrintToServer($"Vote complete! The next round will be {option}!"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /RetakesAllocator/Menus/GunsMenu.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Core; 2 | using CounterStrikeSharp.API.Modules.Entities.Constants; 3 | using CounterStrikeSharp.API.Modules.Menu; 4 | using CounterStrikeSharp.API.Modules.Utils; 5 | using RetakesAllocator.Menus.Interfaces; 6 | using RetakesAllocatorCore; 7 | using static RetakesAllocatorCore.PluginInfo; 8 | using Timer = CounterStrikeSharp.API.Modules.Timers.Timer; 9 | 10 | namespace RetakesAllocator.Menus; 11 | 12 | public class GunsMenu : AbstractBaseMenu 13 | { 14 | private readonly Dictionary _menuTimeoutTimers = new(); 15 | 16 | private static void Print(CCSPlayerController player, string message) 17 | { 18 | Helpers.WriteNewlineDelimited(message, player.PrintToChat); 19 | } 20 | 21 | public override void OpenMenu(CCSPlayerController player) 22 | { 23 | if (Helpers.GetSteamId(player) == 0) 24 | { 25 | Print(player, Translator.Instance["guns_menu.invalid_steam_id"]); 26 | return; 27 | } 28 | 29 | PlayersInMenu.Add(player); 30 | 31 | OpenTPrimaryMenu(player); 32 | } 33 | 34 | public override bool PlayerIsInMenu(CCSPlayerController player) 35 | { 36 | return PlayersInMenu.Contains(player); 37 | } 38 | 39 | private void OnMenuTimeout(CCSPlayerController player) 40 | { 41 | Print(player, Translator.Instance["menu.timeout", MenuTimeout]); 42 | 43 | PlayersInMenu.Remove(player); 44 | if (_menuTimeoutTimers.Remove(player, out var playerTimer)) 45 | { 46 | playerTimer.Kill(); 47 | } 48 | } 49 | 50 | private void CreateMenuTimeoutTimer(CCSPlayerController player) 51 | { 52 | if (_menuTimeoutTimers.Remove(player, out var existingTimer)) 53 | { 54 | existingTimer.Kill(); 55 | } 56 | 57 | _menuTimeoutTimers[player] = new Timer(MenuTimeout, () => OnMenuTimeout(player)); 58 | } 59 | 60 | private void OnMenuComplete(CCSPlayerController player) 61 | { 62 | Print(player, Translator.Instance["guns_menu.complete"]); 63 | 64 | PlayersInMenu.Remove(player); 65 | if (_menuTimeoutTimers.Remove(player, out var playerTimer)) 66 | { 67 | playerTimer.Kill(); 68 | } 69 | } 70 | 71 | private void OnSelectExit(CCSPlayerController player, ChatMenuOption option) 72 | { 73 | if (!PlayersInMenu.Contains(player)) 74 | { 75 | return; 76 | } 77 | 78 | OnMenuComplete(player); 79 | } 80 | 81 | private ChatMenu CreateMenu(CsTeam team, string weaponTypeString) 82 | { 83 | var teamString = Utils.TeamString(team); 84 | return new ChatMenu( 85 | $"{MessagePrefix}{Translator.Instance["guns_menu.select_weapon", teamString, weaponTypeString]}"); 86 | } 87 | 88 | private void OpenTPrimaryMenu(CCSPlayerController player) 89 | { 90 | var menu = CreateMenu(CsTeam.Terrorist, Translator.Instance["weapon_type.primary"]); 91 | 92 | foreach (var weapon in WeaponHelpers.GetPossibleWeaponsForAllocationType(WeaponAllocationType.FullBuyPrimary, 93 | CsTeam.Terrorist)) 94 | { 95 | menu.AddMenuOption(weapon.GetName(), OnTPrimarySelect); 96 | } 97 | 98 | menu.AddMenuOption(Translator.Instance["menu.exit"], OnSelectExit); 99 | 100 | MenuManager.OpenChatMenu(player, menu); 101 | CreateMenuTimeoutTimer(player); 102 | } 103 | 104 | private void OnTPrimarySelect(CCSPlayerController player, ChatMenuOption option) 105 | { 106 | if (!PlayersInMenu.Contains(player)) 107 | { 108 | return; 109 | } 110 | 111 | var weaponName = option.Text; 112 | 113 | Print(player, Translator.Instance[ 114 | "guns_menu.weapon_selected", 115 | weaponName, 116 | Utils.TeamString(CsTeam.Terrorist), 117 | Translator.Instance["weapon_type.primary"] 118 | ]); 119 | 120 | HandlePreferenceSelection(player, CsTeam.Terrorist, weaponName); 121 | 122 | OpenTSecondaryMenu(player); 123 | } 124 | 125 | private void OpenTSecondaryMenu(CCSPlayerController player) 126 | { 127 | var menu = CreateMenu(CsTeam.Terrorist, Translator.Instance["weapon_type.secondary"]); 128 | 129 | foreach (var weapon in WeaponHelpers.GetPossibleWeaponsForAllocationType(WeaponAllocationType.Secondary, 130 | CsTeam.Terrorist)) 131 | { 132 | menu.AddMenuOption(weapon.GetName(), OnTSecondarySelect); 133 | } 134 | 135 | menu.AddMenuOption(Translator.Instance["menu.exit"], OnSelectExit); 136 | 137 | MenuManager.OpenChatMenu(player, menu); 138 | CreateMenuTimeoutTimer(player); 139 | } 140 | 141 | private void OnTSecondarySelect(CCSPlayerController player, ChatMenuOption option) 142 | { 143 | if (!PlayersInMenu.Contains(player)) 144 | { 145 | return; 146 | } 147 | 148 | var weaponName = option.Text; 149 | 150 | Print(player, Translator.Instance[ 151 | "guns_menu.weapon_selected", 152 | weaponName, 153 | Utils.TeamString(CsTeam.Terrorist), 154 | Translator.Instance["weapon_type.secondary"] 155 | ]); 156 | HandlePreferenceSelection(player, CsTeam.Terrorist, weaponName, RoundType.FullBuy); 157 | 158 | OpenCtPrimaryMenu(player); 159 | } 160 | 161 | private void OpenCtPrimaryMenu(CCSPlayerController player) 162 | { 163 | var menu = CreateMenu(CsTeam.CounterTerrorist, Translator.Instance["weapon_type.primary"]); 164 | 165 | foreach (var weapon in WeaponHelpers.GetPossibleWeaponsForAllocationType(WeaponAllocationType.FullBuyPrimary, 166 | CsTeam.CounterTerrorist)) 167 | { 168 | menu.AddMenuOption(weapon.GetName(), OnCtPrimarySelect); 169 | } 170 | 171 | menu.AddMenuOption(Translator.Instance["menu.exit"], OnSelectExit); 172 | 173 | MenuManager.OpenChatMenu(player, menu); 174 | CreateMenuTimeoutTimer(player); 175 | } 176 | 177 | private void OnCtPrimarySelect(CCSPlayerController player, ChatMenuOption option) 178 | { 179 | if (!PlayersInMenu.Contains(player)) 180 | { 181 | return; 182 | } 183 | 184 | var weaponName = option.Text; 185 | 186 | Print(player, Translator.Instance[ 187 | "guns_menu.weapon_selected", 188 | weaponName, 189 | Utils.TeamString(CsTeam.CounterTerrorist), 190 | Translator.Instance["weapon_type.primary"] 191 | ]); 192 | HandlePreferenceSelection(player, CsTeam.CounterTerrorist, weaponName); 193 | 194 | OpenCtSecondaryMenu(player); 195 | } 196 | 197 | private void OpenCtSecondaryMenu(CCSPlayerController player) 198 | { 199 | var menu = CreateMenu(CsTeam.CounterTerrorist, Translator.Instance["weapon_type.secondary"]); 200 | 201 | foreach (var weapon in WeaponHelpers.GetPossibleWeaponsForAllocationType(WeaponAllocationType.Secondary, 202 | CsTeam.CounterTerrorist)) 203 | { 204 | menu.AddMenuOption(weapon.GetName(), OnCtSecondarySelect); 205 | } 206 | 207 | menu.AddMenuOption(Translator.Instance["menu.exit"], OnSelectExit); 208 | 209 | MenuManager.OpenChatMenu(player, menu); 210 | CreateMenuTimeoutTimer(player); 211 | } 212 | 213 | private void OnCtSecondarySelect(CCSPlayerController player, ChatMenuOption option) 214 | { 215 | if (!PlayersInMenu.Contains(player)) 216 | { 217 | return; 218 | } 219 | 220 | var weaponName = option.Text; 221 | 222 | Print(player, Translator.Instance[ 223 | "guns_menu.weapon_selected", 224 | weaponName, 225 | Utils.TeamString(CsTeam.CounterTerrorist), 226 | Translator.Instance["weapon_type.secondary"] 227 | ]); 228 | HandlePreferenceSelection(player, CsTeam.CounterTerrorist, weaponName, RoundType.FullBuy); 229 | 230 | OpenTPistolMenu(player); 231 | } 232 | 233 | private void OpenTPistolMenu(CCSPlayerController player) 234 | { 235 | var menu = CreateMenu(CsTeam.Terrorist, 236 | Translator.Instance["announcement.roundtype", Translator.Instance["roundtype.Pistol"]]); 237 | 238 | foreach (var weapon in WeaponHelpers.GetPossibleWeaponsForAllocationType(WeaponAllocationType.PistolRound, 239 | CsTeam.Terrorist)) 240 | { 241 | menu.AddMenuOption(weapon.GetName(), OnTPistolSelect); 242 | } 243 | 244 | menu.AddMenuOption(Translator.Instance["menu.exit"], OnSelectExit); 245 | 246 | MenuManager.OpenChatMenu(player, menu); 247 | CreateMenuTimeoutTimer(player); 248 | } 249 | 250 | private void OnTPistolSelect(CCSPlayerController player, ChatMenuOption option) 251 | { 252 | if (!PlayersInMenu.Contains(player)) 253 | { 254 | return; 255 | } 256 | 257 | var weaponName = option.Text; 258 | 259 | Print(player, Translator.Instance[ 260 | "guns_menu.weapon_selected", 261 | weaponName, 262 | Utils.TeamString(CsTeam.Terrorist), 263 | Translator.Instance["announcement.roundtype", Translator.Instance["roundtype.Pistol"]] 264 | ]); 265 | HandlePreferenceSelection(player, CsTeam.Terrorist, weaponName); 266 | 267 | OpenCtPistolMenu(player); 268 | } 269 | 270 | private void OpenCtPistolMenu(CCSPlayerController player) 271 | { 272 | var menu = CreateMenu(CsTeam.CounterTerrorist, 273 | Translator.Instance["announcement.roundtype", Translator.Instance["roundtype.Pistol"]]); 274 | 275 | foreach (var weapon in WeaponHelpers.GetPossibleWeaponsForAllocationType(WeaponAllocationType.PistolRound, 276 | CsTeam.CounterTerrorist)) 277 | { 278 | menu.AddMenuOption(weapon.GetName(), OnCtPistolSelect); 279 | } 280 | 281 | menu.AddMenuOption(Translator.Instance["menu.exit"], OnSelectExit); 282 | 283 | MenuManager.OpenChatMenu(player, menu); 284 | CreateMenuTimeoutTimer(player); 285 | } 286 | 287 | private void OnCtPistolSelect(CCSPlayerController player, ChatMenuOption option) 288 | { 289 | if (!PlayersInMenu.Contains(player)) 290 | { 291 | return; 292 | } 293 | 294 | var weaponName = option.Text; 295 | 296 | Print(player, Translator.Instance[ 297 | "guns_menu.weapon_selected", 298 | weaponName, 299 | Utils.TeamString(CsTeam.CounterTerrorist), 300 | Translator.Instance["announcement.roundtype", Translator.Instance["roundtype.Pistol"]] 301 | ]); 302 | HandlePreferenceSelection(player, CsTeam.CounterTerrorist, weaponName); 303 | 304 | OpenTHalfBuyMenu(player); 305 | } 306 | 307 | private void OpenTHalfBuyMenu(CCSPlayerController player) 308 | { 309 | var menu = CreateMenu(CsTeam.Terrorist, Translator.Instance["roundtype.HalfBuy"]); 310 | 311 | foreach (var weapon in WeaponHelpers.GetPossibleWeaponsForAllocationType(WeaponAllocationType.HalfBuyPrimary, 312 | CsTeam.Terrorist)) 313 | { 314 | menu.AddMenuOption(weapon.GetName(), OnTHalfBuySelect); 315 | } 316 | 317 | menu.AddMenuOption(Translator.Instance["menu.exit"], OnSelectExit); 318 | 319 | MenuManager.OpenChatMenu(player, menu); 320 | CreateMenuTimeoutTimer(player); 321 | } 322 | 323 | private void OnTHalfBuySelect(CCSPlayerController player, ChatMenuOption option) 324 | { 325 | if (!PlayersInMenu.Contains(player)) 326 | { 327 | return; 328 | } 329 | 330 | var weaponName = option.Text; 331 | 332 | Print(player, Translator.Instance[ 333 | "guns_menu.weapon_selected", 334 | weaponName, 335 | Utils.TeamString(CsTeam.Terrorist), 336 | Translator.Instance["roundtype.HalfBuy"] 337 | ]); 338 | HandlePreferenceSelection(player, CsTeam.Terrorist, weaponName); 339 | 340 | OpenCTHalfBuyMenu(player); 341 | } 342 | 343 | private void OpenCTHalfBuyMenu(CCSPlayerController player) 344 | { 345 | var menu = CreateMenu(CsTeam.CounterTerrorist, Translator.Instance["roundtype.HalfBuy"]); 346 | 347 | foreach (var weapon in WeaponHelpers.GetPossibleWeaponsForAllocationType(WeaponAllocationType.HalfBuyPrimary, 348 | CsTeam.CounterTerrorist)) 349 | { 350 | menu.AddMenuOption(weapon.GetName(), OnCTHalfBuySelect); 351 | } 352 | 353 | menu.AddMenuOption(Translator.Instance["menu.exit"], OnSelectExit); 354 | 355 | MenuManager.OpenChatMenu(player, menu); 356 | CreateMenuTimeoutTimer(player); 357 | } 358 | 359 | private void OnCTHalfBuySelect(CCSPlayerController player, ChatMenuOption option) 360 | { 361 | if (!PlayersInMenu.Contains(player)) 362 | { 363 | return; 364 | } 365 | 366 | var weaponName = option.Text; 367 | 368 | Print(player, Translator.Instance[ 369 | "guns_menu.weapon_selected", 370 | weaponName, 371 | Utils.TeamString(CsTeam.CounterTerrorist), 372 | Translator.Instance["roundtype.HalfBuy"] 373 | ]); 374 | HandlePreferenceSelection(player, CsTeam.CounterTerrorist, weaponName); 375 | 376 | OpenGiveAwpMenu(player); 377 | } 378 | 379 | private string AwpNeverOption => Translator.Instance["guns_menu.awp_never"]; 380 | private string AwpMyTurnOption => Translator.Instance["guns_menu.awp_always"]; 381 | 382 | private void OpenGiveAwpMenu(CCSPlayerController player) 383 | { 384 | var menu = new ChatMenu($"{MessagePrefix}{Translator.Instance["guns_menu.awp_menu"]}"); 385 | 386 | menu.AddMenuOption(AwpNeverOption, OnGiveAwpSelect); 387 | // Implementing "Sometimes" will require a more complex AWP queue 388 | // menu.AddMenuOption("Sometimes", OnGiveAwpSelect); 389 | menu.AddMenuOption(AwpMyTurnOption, OnGiveAwpSelect); 390 | 391 | menu.AddMenuOption(Translator.Instance["menu.exit"], OnSelectExit); 392 | 393 | MenuManager.OpenChatMenu(player, menu); 394 | CreateMenuTimeoutTimer(player); 395 | } 396 | 397 | private void OnGiveAwpSelect(CCSPlayerController player, ChatMenuOption option) 398 | { 399 | if (!PlayersInMenu.Contains(player)) 400 | { 401 | return; 402 | } 403 | 404 | Print( 405 | player, 406 | Translator.Instance["guns_menu.awp_preference_selected", option.Text] 407 | ); 408 | 409 | if (option.Text == AwpNeverOption) 410 | { 411 | // Team doesnt matter for AWP 412 | HandlePreferenceSelection(player, CsTeam.Terrorist, CsItem.AWP.ToString(), remove: true); 413 | } 414 | else if (option.Text == AwpMyTurnOption) 415 | { 416 | HandlePreferenceSelection(player, CsTeam.Terrorist, CsItem.AWP.ToString(), remove: false); 417 | } 418 | 419 | OnMenuComplete(player); 420 | } 421 | 422 | // TODO This is temporary until this menu knows about the current round 423 | public static void HandlePreferenceSelection(CCSPlayerController player, CsTeam team, string weapon, 424 | bool remove = false) 425 | { 426 | HandlePreferenceSelection(player, team, weapon, null, remove); 427 | } 428 | 429 | public static void HandlePreferenceSelection(CCSPlayerController player, CsTeam team, string weapon, 430 | RoundType? roundTypeOverride, 431 | bool remove = false) 432 | { 433 | var message = OnWeaponCommandHelper.Handle( 434 | new List {weapon}, 435 | Helpers.GetSteamId(player), 436 | roundTypeOverride, 437 | team, 438 | remove, 439 | out _ 440 | ); 441 | Log.Debug(message); 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /RetakesAllocator/Menus/Interfaces/AbstractBaseMenu.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Core; 2 | 3 | namespace RetakesAllocator.Menus.Interfaces; 4 | 5 | public abstract class AbstractBaseMenu 6 | { 7 | protected const float MenuTimeout = 30.0f; 8 | 9 | protected readonly HashSet PlayersInMenu = new(); 10 | 11 | public abstract void OpenMenu(CCSPlayerController player); 12 | public abstract bool PlayerIsInMenu(CCSPlayerController player); 13 | } 14 | -------------------------------------------------------------------------------- /RetakesAllocator/Menus/MenuManager.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Core; 2 | using RetakesAllocator.Managers; 3 | using RetakesAllocator.Menus.Interfaces; 4 | using RetakesAllocatorCore; 5 | 6 | namespace RetakesAllocator.Menus; 7 | 8 | public enum MenuType 9 | { 10 | Guns, 11 | NextRoundVote, 12 | } 13 | 14 | public class AllocatorMenuManager 15 | { 16 | private readonly Dictionary _menus = new() 17 | { 18 | {MenuType.Guns, new GunsMenu()}, 19 | {MenuType.NextRoundVote, new VoteMenu(new NextRoundVoteManager())}, 20 | }; 21 | 22 | private bool MenuAlreadyOpenCheck(CCSPlayerController player) 23 | { 24 | if (IsUserInMenu(player)) 25 | { 26 | Helpers.WriteNewlineDelimited(Translator.Instance["menu.already_in_menu"], player.PrintToChat); 27 | return true; 28 | } 29 | 30 | return false; 31 | } 32 | 33 | public T GetMenu(MenuType menuType) 34 | where T : AbstractBaseMenu 35 | { 36 | return (T) _menus[menuType]; 37 | } 38 | 39 | public bool OpenMenuForPlayer(CCSPlayerController player, MenuType menuType) 40 | { 41 | if (MenuAlreadyOpenCheck(player)) 42 | { 43 | return false; 44 | } 45 | 46 | if (!_menus.TryGetValue(menuType, out var menu)) 47 | { 48 | return false; 49 | } 50 | 51 | menu.OpenMenu(player); 52 | return true; 53 | } 54 | 55 | private bool IsUserInMenu(CCSPlayerController player) 56 | { 57 | return _menus.Values.Any(menu => menu.PlayerIsInMenu(player)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /RetakesAllocator/Menus/VoteMenu.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Core; 2 | using CounterStrikeSharp.API.Modules.Menu; 3 | using RetakesAllocator.Managers; 4 | using RetakesAllocator.Menus.Interfaces; 5 | using RetakesAllocatorCore; 6 | using static RetakesAllocatorCore.PluginInfo; 7 | using Timer = CounterStrikeSharp.API.Modules.Timers.Timer; 8 | 9 | namespace RetakesAllocator.Menus; 10 | 11 | public class VoteMenu : AbstractBaseMenu 12 | { 13 | private new const float MenuTimeout = 20.0f; 14 | private readonly Dictionary _menuTimeoutTimers = new(); 15 | private readonly AbstractVoteManager _voteManager; 16 | 17 | public VoteMenu(AbstractVoteManager voteManager) 18 | { 19 | _voteManager = voteManager; 20 | } 21 | 22 | public override void OpenMenu(CCSPlayerController player) 23 | { 24 | PlayersInMenu.Add(player); 25 | 26 | var menu = new ChatMenu($"{_voteManager.VoteMessagePrefix}{Translator.Instance["vote_menu.select_vote"]}"); 27 | 28 | foreach (var option in _voteManager.GetVoteOptions()) 29 | { 30 | menu.AddMenuOption(option, OnVoteSelect); 31 | } 32 | 33 | menu.AddMenuOption(Translator.Instance["menu.exit"], OnSelectExit); 34 | 35 | MenuManager.OpenChatMenu(player, menu); 36 | CreateMenuTimeoutTimer(player); 37 | } 38 | 39 | public void GatherAndHandleVotes() 40 | { 41 | _voteManager.CompleteVote(); 42 | } 43 | 44 | public override bool PlayerIsInMenu(CCSPlayerController player) 45 | { 46 | return PlayersInMenu.Contains(player); 47 | } 48 | 49 | private void OnMenuTimeout(CCSPlayerController player) 50 | { 51 | Helpers.WriteNewlineDelimited( 52 | Translator.Instance["menu.timeout", MenuTimeout], 53 | player.PrintToChat 54 | ); 55 | 56 | PlayersInMenu.Remove(player); 57 | _menuTimeoutTimers[player].Kill(); 58 | _menuTimeoutTimers.Remove(player); 59 | } 60 | 61 | private void CreateMenuTimeoutTimer(CCSPlayerController player) 62 | { 63 | if (_menuTimeoutTimers.TryGetValue(player, out var existingTimer)) 64 | { 65 | existingTimer.Kill(); 66 | _menuTimeoutTimers.Remove(player); 67 | } 68 | 69 | _menuTimeoutTimers[player] = new Timer(MenuTimeout, () => OnMenuTimeout(player)); 70 | } 71 | 72 | private void OnMenuComplete(CCSPlayerController player) 73 | { 74 | PlayersInMenu.Remove(player); 75 | _menuTimeoutTimers[player].Kill(); 76 | _menuTimeoutTimers.Remove(player); 77 | } 78 | 79 | private void OnSelectExit(CCSPlayerController player, ChatMenuOption option) 80 | { 81 | if (!PlayersInMenu.Contains(player)) 82 | { 83 | return; 84 | } 85 | 86 | OnMenuComplete(player); 87 | } 88 | 89 | 90 | private void OnVoteSelect(CCSPlayerController player, ChatMenuOption option) 91 | { 92 | if (!PlayersInMenu.Contains(player)) 93 | { 94 | return; 95 | } 96 | 97 | _voteManager.CastVote(player, option.Text); 98 | 99 | OnMenuComplete(player); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /RetakesAllocator/RetakesAllocator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /RetakesAllocator/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "roundtype.Pistol": "Pistol", 3 | "roundtype.HalfBuy": "Half Buy", 4 | "roundtype.FullBuy": "Full Buy", 5 | 6 | "weapon_type.primary": "Primary", 7 | "weapon_type.secondary": "Secondary", 8 | 9 | "teams.terrorist": "Terrorist", 10 | "teams.terrorist_short": "T", 11 | "teams.counter_terrorist": "Counter Terrorist", 12 | "teams.counter_terrorist_short": "CT", 13 | 14 | "announcement.roundtype": "{0} Round", 15 | "announcement.next_roundtype_set": "Next round will be a {0} round.", 16 | "announcement.next_roundtype_set_invalid": "Invalid round type: {0}.", 17 | 18 | "weapon_preference.cannot_choose": "Players cannot choose their weapons on this server.", 19 | "weapon_preference.gun_usage": "Usage: !gun . Partial matches for any gun will be found.\nValid guns for {0}:\nPistols: {1}\nHalf buy: {2}\nFull buy: {3}", 20 | "weapon_preference.invalid_team": "Invalid team provided: {0}.", 21 | "weapon_preference.join_team": "You must join a team before running this command.", 22 | "weapon_preference.not_found": "Weapon '{0}' not found.", 23 | "weapon_preference.not_allowed": "Weapon '{0}' is not allowed to be selected.", 24 | "weapon_preference.invalid_weapon": "Invalid weapon: '{0}'.", 25 | "weapon_preference.not_valid_for_team": "Weapon '{0}' is not valid for {1}", 26 | "weapon_preference.unset_preference_preferred": "You will no longer receive '{0}'.", 27 | "weapon_preference.unset_preference": "Weapon '{0}' is no longer your {1} preference for {2}.", 28 | "weapon_preference.set_preference_preferred": "You will now get a '{0}' when its your turn for a sniper.", 29 | "weapon_preference.set_preference": "Weapon '{0}' is now your {1} preference for {2}.", 30 | "weapon_preference.receive_next_round": " You will get it at the next {0} round.", 31 | "weapon_preference.not_saved": "Without a valid Steam ID, your preferences will not be saved.", 32 | "weapon_preference.only_vip_can_use": "Only VIP players can use this command!", 33 | 34 | "menu.timeout": "You did not interact with the menu in {0} seconds!", 35 | "menu.exit": "Exit", 36 | "menu.already_in_menu": "You are already using another menu!", 37 | 38 | "vote_menu.select_vote": "Select your vote!", 39 | 40 | "guns_menu.invalid_steam_id": "You cannot set weapon preferences with invalid Steam ID.", 41 | "guns_menu.complete": "You have finished setting up your weapons!\nThe weapons you have selected will be given to you at the start of the next round!", 42 | "guns_menu.select_weapon": "Select a {0} {1} Weapon", 43 | "guns_menu.weapon_selected": "You selected {0} as {1} {2} weapon!", 44 | "guns_menu.awp_menu": "Select when to give the AWP", 45 | "guns_menu.awp_always": "Always", 46 | "guns_menu.awp_never": "Never", 47 | "guns_menu.awp_preference_selected": "You selected '{0}' as when to give the AWP!", 48 | 49 | "menu.main.tloadout": "█░ T Loadout ░█", 50 | "menu.main.ctloadout": "█░ CT Loadout ░█", 51 | "menu.main.awp": "█░ AWP ░█", 52 | 53 | "menu.tprimary": "█ T Primary █", 54 | "menu.tsecondary": "█ T Secondary █", 55 | "menu.tPistol": "█ T Pistol Round █", 56 | "menu.tHalfbuy": "█ T Half Buy █", 57 | 58 | "menu.ctprimary": "█ CT Primary █", 59 | "menu.ctsecondary": "█ CT Secondary █", 60 | "menu.ctPistol": "█ CT Pistol Round █", 61 | "menu.ctHalfbuy": "█ CT Half Buy █", 62 | 63 | "menu.awp.always": "Always", 64 | "menu.awp.never": "Never", 65 | 66 | "menu.left.image": "", 67 | "menu.right.image": "", 68 | "menu.bottom.text": "
[ WASD - To Native ]
[ - To Exit ]
", 69 | "menu.bottom.text.pistol": "[ - To Exit ]
", 70 | 71 | "BombSite.A": "", 72 | "BombSite.B": "", 73 | "T.Message": "DEFEND SITE {0}
{1}
{2} T vs. {3} CT", 74 | "CT.Message": "RETAKE SITE {0}
{1}
{2} T vs. {3} CT", 75 | 76 | "chatAsite.line1": "{DarkBlue}[Retakes] {Yellow} ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎回︎回︎回︎回︎", 77 | "chatAsite.line2": "{DarkBlue}[Retakes] {Yellow} 回︎回︎‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎回︎回︎", 78 | "chatAsite.line3": "{DarkBlue}[Retakes] {Yellow} 回︎回︎‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‎‏‏‎ ‎‏‏‎ ‎‏‏‎ 回︎回︎", 79 | "chatAsite.line4": "{DarkBlue}[Retakes] {Yellow} 回︎回︎回︎回︎回︎回︎", 80 | "chatAsite.line5": "{DarkBlue}[Retakes] {Yellow} 回︎回︎‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‎‏‏‎ ‎‏‏‎ ‎‏‏‎ 回︎回︎", 81 | "chatAsite.line6": "{DarkBlue}[Retakes] {Yellow} 回︎回︎‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‎‏‏‎ ‎‏‏‎ ‎‏‏‎ 回︎回︎", 82 | 83 | "chatBsite.line1": "{DarkBlue}[Retakes] {Yellow} 回︎回︎回︎回︎", 84 | "chatBsite.line2": "{DarkBlue}[Retakes] {Yellow} 回︎回︎‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‎‏‏‎ ‎‏‏‎ ‎‏‏‎回︎", 85 | "chatBsite.line3": "{DarkBlue}[Retakes] {Yellow} 回︎回︎回︎回︎", 86 | "chatBsite.line4": "{DarkBlue}[Retakes] {Yellow} 回︎回︎‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‎‏‏‎ ‎‏‏‎ 回︎", 87 | "chatBsite.line5": "{DarkBlue}[Retakes] {Yellow} 回︎回︎‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‎‏‏‎ ‎‏‏‎ 回︎", 88 | "chatBsite.line6": "{DarkBlue}[Retakes] {Yellow} 回︎回︎回︎回︎" 89 | } 90 | -------------------------------------------------------------------------------- /RetakesAllocator/lang/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "roundtype.Pistol": "Pistola", 3 | "roundtype.HalfBuy": "Compra Parcial", 4 | "roundtype.FullBuy": "Compra Completa", 5 | 6 | "weapon_type.primary": "Primária", 7 | "weapon_type.secondary": "Secundária", 8 | 9 | "teams.terrorist": "Terrorista", 10 | "teams.terrorist_short": "T", 11 | "teams.counter_terrorist": "Contra Terrorista", 12 | "teams.counter_terrorist_short": "CT", 13 | 14 | "announcement.roundtype": "Rodada {0}", 15 | "announcement.next_roundtype_set": "A próxima rodada será uma rodada de {0}.", 16 | "announcement.next_roundtype_set_invalid": "Tipo de rodada inválido: {0}.", 17 | 18 | "weapon_preference.cannot_choose": "Os jogadores não podem escolher suas armas neste servidor.", 19 | "weapon_preference.gun_usage": "Use: !gun . Correspondências parciais para qualquer arma serão encontradas.\nArmas válidas para {0}:\nPistolas: {1}\nCompra parcial: {2}\nCompra completa: {3}", 20 | "weapon_preference.invalid_team": "Equipe fornecida inválida: {0}.", 21 | "weapon_preference.join_team": "Você deve entrar em uma equipe antes de executar este comando.", 22 | "weapon_preference.not_found": "Arma '{0}' não encontrada.", 23 | "weapon_preference.not_allowed": "Arma '{0}' não é permitida ser selecionada.", 24 | "weapon_preference.invalid_weapon": "Arma inválida: '{0}'.", 25 | "weapon_preference.not_valid_for_team": "Arma '{0}' não é válida para {1}", 26 | "weapon_preference.unset_preference_preferred": "Você não receberá mais '{0}'.", 27 | "weapon_preference.unset_preference": "Arma '{0}' não é mais sua preferência de {1} para {2}.", 28 | "weapon_preference.set_preference_preferred": "Você receberá agora um '{0}' quando for sua vez de um atirador de elite.", 29 | "weapon_preference.set_preference": "Arma '{0}' agora é sua preferência de {1} para {2}.", 30 | "weapon_preference.receive_next_round": " Você a receberá na próxima rodada de {0}.", 31 | "weapon_preference.not_saved": "Sem um STEAM ID válido, suas preferências não serão salvas.", 32 | "weapon_preference.only_vip_can_use": "Somente jogadores VIP podem usar este comando!", 33 | 34 | "menu.timeout": "Você não interagiu com o menu em {0} segundos!", 35 | "menu.exit": "Sair", 36 | "menu.already_in_menu": "Você já está usando outro menu!", 37 | 38 | "vote_menu.select_vote": "Selecione seu voto!", 39 | 40 | "guns_menu.invalid_steam_id": "Você não pode definir preferências de armas com um STEAM ID inválido.", 41 | "guns_menu.complete": "Você terminou de configurar suas armas!\nAs armas que você selecionou serão entregues a você no início da próxima rodada!", 42 | "guns_menu.select_weapon": "Selecione uma arma {0} {1}", 43 | "guns_menu.weapon_selected": "Você selecionou {0} como arma {1} {2}!", 44 | "guns_menu.awp_menu": "Selecione quando receber a AWP", 45 | "guns_menu.awp_always": "Sempre", 46 | "guns_menu.awp_never": "Nunca", 47 | "guns_menu.awp_preference_selected": "Você selecionou '{0}' para receber a AWP!", 48 | 49 | "menu.main.tloadout": "█ Loadout T █", 50 | "menu.main.ctloadout": "█ Loadout CT █", 51 | "menu.main.awp": "█ AWP █", 52 | 53 | "menu.tprimary": "█ Primária T █", 54 | "menu.tsecondary": "█ Secundária T █", 55 | "menu.tPistol": "█ Rodada de Pistola T █", 56 | 57 | "menu.ctprimary": "█ Primária CT █", 58 | "menu.ctsecondary": "█ Secundária CT █", 59 | "menu.ctPistol": "█ Rodada de Pistola CT █", 60 | 61 | "menu.awp.always": "Sempre", 62 | "menu.awp.never": "Nunca", 63 | 64 | "menu.left.image": "", 65 | "menu.right.image": "", 66 | "menu.bottom.text": "
[ WASD - Para Mover ]
[ - Para Sair ]
", 67 | "menu.bottom.text.pistol": "[ - Para Sair ]
" 68 | } 69 | -------------------------------------------------------------------------------- /RetakesAllocator/lang/pt-PT.json: -------------------------------------------------------------------------------- 1 | { 2 | "roundtype.Pistol": "Pistola", 3 | "roundtype.HalfBuy": "Compra Parcial", 4 | "roundtype.FullBuy": "Compra Completa", 5 | 6 | "weapon_type.primary": "Primária", 7 | "weapon_type.secondary": "Secundária", 8 | 9 | "teams.terrorist": "Terrorista", 10 | "teams.terrorist_short": "T", 11 | "teams.counter_terrorist": "Contra-Terrorista", 12 | "teams.counter_terrorist_short": "CT", 13 | 14 | "announcement.roundtype": "Ronda {0}", 15 | "announcement.next_roundtype_set": "A próxima ronda será uma ronda de {0}.", 16 | "announcement.next_roundtype_set_invalid": "Tipo de ronda inválido: {0}.", 17 | 18 | "weapon_preference.cannot_choose": "Os jogadores não podem escolher as suas armas neste servidor.", 19 | "weapon_preference.gun_usage": "Use: !gun . Correspondências parciais para qualquer arma serão encontradas.\nArmas válidas para {0}:\nPistolas: {1}\nCompra parcial: {2}\nCompra completa: {3}", 20 | "weapon_preference.invalid_team": "Equipa fornecida inválida: {0}.", 21 | "weapon_preference.join_team": "Deves juntar-te a uma equipa antes de executar este comando.", 22 | "weapon_preference.not_found": "Arma '{0}' não encontrada.", 23 | "weapon_preference.not_allowed": "Não é permitido selecionar a arma '{0}'.", 24 | "weapon_preference.invalid_weapon": "Arma inválida: '{0}'.", 25 | "weapon_preference.not_valid_for_team": "A arma '{0}' não é válida para {1}.", 26 | "weapon_preference.unset_preference_preferred": "Já não vais receber '{0}'.", 27 | "weapon_preference.unset_preference": "A arma '{0}' já não é a tua preferência de {1} para {2}.", 28 | "weapon_preference.set_preference_preferred": "Agora vais receber um '{0}' quando for a tua vez de ser sniper.", 29 | "weapon_preference.set_preference": "A arma '{0}' é agora a tua preferência de {1} para {2}.", 30 | "weapon_preference.receive_next_round": " Vais recebê-la na próxima ronda de {0}.", 31 | "weapon_preference.not_saved": "Sem um STEAM ID válido, as tuas preferências não serão guardadas.", 32 | "weapon_preference.only_vip_can_use": "Só os jogadores VIP podem usar este comando!", 33 | 34 | "menu.timeout": "Não interagiste com o menu em {0} segundos!", 35 | "menu.exit": "Sair", 36 | "menu.already_in_menu": "Já estás a usar outro menu!", 37 | 38 | "vote_menu.select_vote": "Seleciona o teu voto!", 39 | 40 | "guns_menu.invalid_steam_id": "Não podes definir preferências de armas com um STEAM ID inválido.", 41 | "guns_menu.complete": "Terminaste de configurar as tuas armas!\nAs armas que selecionaste ser-te-ão entregues no início da próxima ronda!", 42 | "guns_menu.select_weapon": "Seleciona uma arma {0} {1}", 43 | "guns_menu.weapon_selected": "Selecionaste {0} como arma {1} {2}!", 44 | "guns_menu.awp_menu": "Seleciona quando receber a AWP", 45 | "guns_menu.awp_always": "Sempre", 46 | "guns_menu.awp_never": "Nunca", 47 | "guns_menu.awp_preference_selected": "Selecionaste '{0}' para receber a AWP!", 48 | 49 | "menu.main.tloadout": "█ Loadout T █", 50 | "menu.main.ctloadout": "█ Loadout CT █", 51 | "menu.main.awp": "█ AWP █", 52 | 53 | "menu.tprimary": "█ Primária T █", 54 | "menu.tsecondary": "█ Secundária T █", 55 | "menu.tPistol": "█ Ronda de Pistola T █", 56 | 57 | "menu.ctprimary": "█ Primária CT █", 58 | "menu.ctsecondary": "█ Secundária CT █", 59 | "menu.ctPistol": "█ Ronda de Pistola CT █", 60 | 61 | "menu.awp.always": "Sempre", 62 | "menu.awp.never": "Nunca", 63 | 64 | "menu.left.image": "", 65 | "menu.right.image": "", 66 | "menu.bottom.text": "
[ WASD - Para Mover ]
[ - Para Sair ]
", 67 | "menu.bottom.text.pistol": "[ - Para Sair ]
" 68 | } 69 | -------------------------------------------------------------------------------- /RetakesAllocator/lang/zh-Hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "roundtype.Pistol": "手枪", 3 | "roundtype.HalfBuy": "半起", 4 | "roundtype.FullBuy": "长枪", 5 | 6 | "weapon_type.primary": "主武器", 7 | "weapon_type.secondary": "手枪", 8 | 9 | "teams.terrorist": "恐怖分子", 10 | "teams.terrorist_short": "T", 11 | "teams.counter_terrorist": "反恐精英", 12 | "teams.counter_terrorist_short": "CT", 13 | 14 | "announcement.roundtype": "{0} 局", 15 | "announcement.next_roundtype_set": "下一回合将是 {0} 回合。", 16 | "announcement.next_roundtype_set_invalid": "无效的回合类型:{0}。", 17 | 18 | "weapon_preference.cannot_choose": "玩家无法在此服务器上选择他们的武器。", 19 | "weapon_preference.gun_usage": "用法:!gun 。将找到任何枪的部分匹配。\n{0} 的有效枪支:\n手枪:{1}\n半购买:{2}\n全购买:{3}", 20 | "weapon_preference.invalid_team": "提供的队伍无效:{0}。", 21 | "weapon_preference.join_team": "在运行此命令之前,您必须加入一个队伍。", 22 | "weapon_preference.not_found": "未找到武器 '{0}'。", 23 | "weapon_preference.not_allowed": "不允许选择武器 '{0}'。", 24 | "weapon_preference.invalid_weapon": "无效的武器:'{0}'。", 25 | "weapon_preference.not_valid_for_team": "武器 '{0}' 不适用于 {1}", 26 | "weapon_preference.unset_preference_preferred": "您将不再获得 '{0}'。", 27 | "weapon_preference.unset_preference": "武器 '{0}' 不再是您在 {2} 中的 {1} 偏好。", 28 | "weapon_preference.set_preference_preferred": "轮到您使用狙击步枪时,您将获得一个 '{0}'。", 29 | "weapon_preference.set_preference": "武器 '{0}' 现在是您在 {2} 中的 {1} 偏好。", 30 | "weapon_preference.receive_next_round": " 您将在下一轮 {0} 中获得它。", 31 | "weapon_preference.not_saved": "没有有效的 Steam ID,您的偏好设置将不会被保存。", 32 | 33 | "menu.timeout": "您在 {0} 秒内未与菜单交互!", 34 | "menu.exit": "退出", 35 | "menu.already_in_menu": "您已经在使用另一个菜单!", 36 | 37 | "vote_menu.select_vote": "选择您的投票!", 38 | 39 | "guns_menu.invalid_steam_id": "您不能使用无效的 Steam ID 设置武器偏好。", 40 | "guns_menu.complete": "您已经完成了武器设置!\n您选择的武器将在下一轮开始时给予您!", 41 | "guns_menu.select_weapon": "选择一个 {0} {1} 武器", 42 | "guns_menu.weapon_selected": "您选择了 {0} 作为 {1} {2} 武器!", 43 | "guns_menu.awp_menu": "选择何时给予 AWP", 44 | "guns_menu.awp_always": "始终", 45 | "guns_menu.awp_never": "从不", 46 | "guns_menu.awp_preference_selected": "您选择了 '{0}' 作为何时给予 AWP!", 47 | 48 | "menu.main.tloadout": "█░ T 装备 ░█", 49 | "menu.main.ctloadout": "█░ CT 装备 ░█", 50 | "menu.main.awp": "█░ AWP ░█", 51 | 52 | "menu.tprimary": "█ T 主武器 █", 53 | "menu.tsecondary": "█ T 手枪 █", 54 | "menu.tPistol": "█ T 手枪局 █", 55 | 56 | "menu.ctprimary": "█ CT 主武器 █", 57 | "menu.ctsecondary": "█ CT 手枪 █", 58 | "menu.ctPistol": "█ CT 手枪局 █", 59 | 60 | "menu.awp.always": "始终", 61 | "menu.awp.never": "从不", 62 | 63 | "menu.left.image": "", 64 | "menu.right.image": "", 65 | "menu.bottom.text": "
[ WASD - 进入 ]
[ - 退出 ]
", 66 | "menu.bottom.text.pistol": "[ - 退出 ]
" 67 | } 68 | -------------------------------------------------------------------------------- /RetakesAllocator/release.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | TARGET_NAME="RetakesAllocator" 4 | TARGET_DIR="./bin/Release/net8.0" 5 | NEW_DIR="./bin/Release/RetakesAllocator" 6 | 7 | echo $TARGET_NAME 8 | echo $TARGET_DIR 9 | echo $NEW_DIR 10 | 11 | ls $TARGET_DIR/** 12 | 13 | echo cp -r $TARGET_DIR $NEW_DIR 14 | cp -r $TARGET_DIR $NEW_DIR 15 | echo rm -rf "$NEW_DIR/runtimes" 16 | rm -rf "$NEW_DIR/runtimes" 17 | echo mkdir "$NEW_DIR/runtimes" 18 | mkdir "$NEW_DIR/runtimes" 19 | echo cp -rf "$TARGET_DIR/runtimes/linux-x64" "$NEW_DIR/runtimes" 20 | cp -rf "$TARGET_DIR/runtimes/linux-x64" "$NEW_DIR/runtimes" 21 | echo cp -rf "$TARGET_DIR/runtimes/win-x64" "$NEW_DIR/runtimes" 22 | cp -rf "$TARGET_DIR/runtimes/win-x64" "$NEW_DIR/runtimes" 23 | 24 | # Remove unnecessary files 25 | rm "$NEW_DIR/CounterStrikeSharp.API.dll" 26 | 27 | tree ./bin -------------------------------------------------------------------------------- /RetakesAllocatorCore/Config/Configs.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using CounterStrikeSharp.API.Modules.Entities.Constants; 4 | using CounterStrikeSharp.API.Modules.Utils; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace RetakesAllocatorCore.Config; 8 | 9 | public static class Configs 10 | { 11 | public static class Shared 12 | { 13 | public static string? Module { get; set; } 14 | } 15 | private static readonly string ConfigDirectoryName = "config"; 16 | private static readonly string ConfigFileName = "config.json"; 17 | 18 | private static string? _configFilePath; 19 | private static ConfigData? _configData; 20 | 21 | private static readonly JsonSerializerOptions SerializationOptions = new() 22 | { 23 | Converters = 24 | { 25 | new JsonStringEnumConverter() 26 | }, 27 | WriteIndented = true, 28 | AllowTrailingCommas = true, 29 | ReadCommentHandling = JsonCommentHandling.Skip, 30 | }; 31 | 32 | public static bool IsLoaded() 33 | { 34 | return _configData is not null; 35 | } 36 | 37 | public static ConfigData GetConfigData() 38 | { 39 | if (_configData is null) 40 | { 41 | throw new Exception("Config not yet loaded."); 42 | } 43 | 44 | return _configData; 45 | } 46 | 47 | public static ConfigData Load(string modulePath, bool saveAfterLoad = false) 48 | { 49 | var configFileDirectory = Path.Combine(modulePath, ConfigDirectoryName); 50 | Directory.CreateDirectory(configFileDirectory); 51 | 52 | _configFilePath = Path.Combine(configFileDirectory, ConfigFileName); 53 | if (File.Exists(_configFilePath)) 54 | { 55 | _configData = 56 | JsonSerializer.Deserialize(File.ReadAllText(_configFilePath), SerializationOptions); 57 | } 58 | else 59 | { 60 | _configData = new ConfigData(); 61 | } 62 | 63 | if (_configData is null) 64 | { 65 | throw new Exception("Failed to load configs."); 66 | } 67 | 68 | if (saveAfterLoad) 69 | { 70 | SaveConfigData(_configData); 71 | } 72 | 73 | _configData.Validate(); 74 | 75 | return _configData; 76 | } 77 | 78 | public static ConfigData OverrideConfigDataForTests( 79 | ConfigData configData 80 | ) 81 | { 82 | configData.Validate(); 83 | _configData = configData; 84 | return _configData; 85 | } 86 | 87 | private static void SaveConfigData(ConfigData configData) 88 | { 89 | if (_configFilePath is null) 90 | { 91 | throw new Exception("Config not yet loaded."); 92 | } 93 | 94 | File.WriteAllText(_configFilePath, JsonSerializer.Serialize(configData, SerializationOptions)); 95 | } 96 | 97 | public static string? StringifyConfig(string? configName) 98 | { 99 | var configData = GetConfigData(); 100 | if (configName is null) 101 | { 102 | return JsonSerializer.Serialize(configData, SerializationOptions); 103 | } 104 | var property = configData.GetType().GetProperty(configName); 105 | if (property is null) 106 | { 107 | return null; 108 | } 109 | return JsonSerializer.Serialize(property.GetValue(configData), SerializationOptions); 110 | } 111 | } 112 | 113 | public enum WeaponSelectionType 114 | { 115 | PlayerChoice, 116 | Random, 117 | Default, 118 | } 119 | 120 | public enum DatabaseProvider 121 | { 122 | Sqlite, 123 | MySql, 124 | } 125 | 126 | public enum RoundTypeSelectionOption 127 | { 128 | Random, 129 | RandomFixedCounts, 130 | ManualOrdering, 131 | } 132 | 133 | public record RoundTypeManualOrderingItem(RoundType Type, int Count); 134 | 135 | public record ConfigData 136 | { 137 | public List UsableWeapons { get; set; } = WeaponHelpers.AllWeapons; 138 | 139 | public List AllowedWeaponSelectionTypes { get; set; } = 140 | Enum.GetValues().ToList(); 141 | 142 | public Dictionary> DefaultWeapons { get; set; } = 143 | WeaponHelpers.DefaultWeaponsByTeamAndAllocationType; 144 | 145 | public Dictionary< 146 | string, 147 | Dictionary< 148 | CsTeam, 149 | Dictionary 150 | > 151 | > MaxNades { get; set; } = new() 152 | { 153 | { 154 | NadeHelpers.GlobalSettingName, new() 155 | { 156 | { 157 | CsTeam.Terrorist, new() 158 | { 159 | {CsItem.Flashbang, 2}, 160 | {CsItem.Smoke, 1}, 161 | {CsItem.Molotov, 1}, 162 | {CsItem.HE, 1}, 163 | } 164 | }, 165 | { 166 | CsTeam.CounterTerrorist, new() 167 | { 168 | {CsItem.Flashbang, 2}, 169 | {CsItem.Smoke, 1}, 170 | {CsItem.Incendiary, 2}, 171 | {CsItem.HE, 1}, 172 | } 173 | }, 174 | } 175 | } 176 | }; 177 | 178 | public Dictionary< 179 | string, 180 | Dictionary< 181 | CsTeam, 182 | Dictionary 183 | > 184 | > MaxTeamNades { get; set; } = new() 185 | { 186 | { 187 | NadeHelpers.GlobalSettingName, new() 188 | { 189 | { 190 | CsTeam.Terrorist, new() 191 | { 192 | {RoundType.Pistol, MaxTeamNadesSetting.AverageOnePerPlayer}, 193 | {RoundType.HalfBuy, MaxTeamNadesSetting.AverageOnePointFivePerPlayer}, 194 | {RoundType.FullBuy, MaxTeamNadesSetting.AverageOnePointFivePerPlayer}, 195 | } 196 | }, 197 | { 198 | CsTeam.CounterTerrorist, new() 199 | { 200 | {RoundType.Pistol, MaxTeamNadesSetting.AverageOnePerPlayer}, 201 | {RoundType.HalfBuy, MaxTeamNadesSetting.AverageOnePointFivePerPlayer}, 202 | {RoundType.FullBuy, MaxTeamNadesSetting.AverageOnePointFivePerPlayer}, 203 | } 204 | }, 205 | } 206 | } 207 | }; 208 | 209 | public RoundTypeSelectionOption RoundTypeSelection { get; set; } = RoundTypeSelectionOption.Random; 210 | 211 | public Dictionary RoundTypePercentages { get; set; } = new() 212 | { 213 | {RoundType.Pistol, 15}, 214 | {RoundType.HalfBuy, 25}, 215 | {RoundType.FullBuy, 60}, 216 | }; 217 | 218 | public Dictionary RoundTypeRandomFixedCounts { get; set; } = new() 219 | { 220 | {RoundType.Pistol, 5}, 221 | {RoundType.HalfBuy, 10}, 222 | {RoundType.FullBuy, 15}, 223 | }; 224 | 225 | public List RoundTypeManualOrdering { get; set; } = new() 226 | { 227 | new RoundTypeManualOrderingItem(RoundType.Pistol, 5), 228 | new RoundTypeManualOrderingItem(RoundType.HalfBuy, 10), 229 | new RoundTypeManualOrderingItem(RoundType.FullBuy, 15), 230 | }; 231 | 232 | public bool MigrateOnStartup { get; set; } = true; 233 | public bool ResetStateOnGameRestart { get; set; } = true; 234 | public bool AllowAllocationAfterFreezeTime { get; set; } = true; 235 | public bool UseOnTickFeatures { get; set; } = true; 236 | public bool CapabilityWeaponPaints { get; set; } = true; 237 | public bool EnableRoundTypeAnnouncement { get; set; } = true; 238 | public bool EnableRoundTypeAnnouncementCenter { get; set; } = false; 239 | public bool EnableBombSiteAnnouncementCenter { get; set; } = false; 240 | public bool BombSiteAnnouncementCenterToCTOnly { get; set; } = false; 241 | public bool DisableDefaultBombPlantedCenterMessage { get; set; } = false; 242 | public bool ForceCloseBombSiteAnnouncementCenterOnPlant { get; set; } = true; 243 | public float BombSiteAnnouncementCenterDelay { get; set; } = 1.0f; 244 | public float BombSiteAnnouncementCenterShowTimer { get; set; } = 5.0f; 245 | public bool EnableBombSiteAnnouncementChat { get; set; } = false; 246 | public bool EnableNextRoundTypeVoting { get; set; } = false; 247 | public int NumberOfExtraVipChancesForPreferredWeapon { get; set; } = 1; 248 | public bool AllowPreferredWeaponForEveryone { get; set; } = false; 249 | 250 | public double ChanceForPreferredWeapon { get; set; } = 100; 251 | 252 | public Dictionary MaxPreferredWeaponsPerTeam { get; set; } = new() 253 | { 254 | {CsTeam.Terrorist, 1}, 255 | {CsTeam.CounterTerrorist, 1}, 256 | }; 257 | 258 | public Dictionary MinPlayersPerTeamForPreferredWeapon { get; set; } = new() 259 | { 260 | {CsTeam.Terrorist, 1}, 261 | {CsTeam.CounterTerrorist, 1}, 262 | }; 263 | 264 | public bool EnableCanAcquireHook { get; set; } = true; 265 | 266 | public LogLevel LogLevel { get; set; } = LogLevel.Information; 267 | public string ChatMessagePluginName { get; set; } = "Retakes"; 268 | public string? ChatMessagePluginPrefix { get; set; } 269 | 270 | public string InGameGunMenuCenterCommands { get; set; } = 271 | "gunsmenu,gunmenu,!gunmenu,!gunsmenu,!menugun,!menuguns,/gunsmenu,/gunmenu"; 272 | 273 | public string InGameGunMenuChatCommands { get; set; } = "guns,!guns,/guns"; 274 | public ZeusPreference ZeusPreference { get; set; } = ZeusPreference.Never; 275 | 276 | public DatabaseProvider DatabaseProvider { get; set; } = DatabaseProvider.Sqlite; 277 | public string DatabaseConnectionString { get; set; } = "Data Source=data.db; Pooling=False"; 278 | public bool AutoUpdateSignatures { get; set; } = true; 279 | 280 | public IList Validate() 281 | { 282 | if (RoundTypePercentages.Values.Sum() != 100) 283 | { 284 | throw new Exception("'RoundTypePercentages' values must add up to 100"); 285 | } 286 | 287 | var warnings = new List(); 288 | warnings.AddRange(ValidateDefaultWeapons(CsTeam.Terrorist)); 289 | warnings.AddRange(ValidateDefaultWeapons(CsTeam.CounterTerrorist)); 290 | 291 | foreach (var warning in warnings) 292 | { 293 | Log.Warn($"[CONFIG WARNING] {warning}"); 294 | } 295 | 296 | return warnings; 297 | } 298 | 299 | private ICollection ValidateDefaultWeapons(CsTeam team) 300 | { 301 | var warnings = new List(); 302 | if (!DefaultWeapons.TryGetValue(team, out var defaultWeapons)) 303 | { 304 | warnings.Add($"Missing {team} in DefaultWeapons config."); 305 | return warnings; 306 | } 307 | 308 | if (defaultWeapons.ContainsKey(WeaponAllocationType.Preferred)) 309 | { 310 | throw new Exception( 311 | $"Preferred is not a valid default weapon allocation type " + 312 | $"for config DefaultWeapons.{team}."); 313 | } 314 | 315 | var allocationTypes = WeaponHelpers.WeaponAllocationTypes; 316 | allocationTypes.Remove(WeaponAllocationType.Preferred); 317 | 318 | foreach (var allocationType in allocationTypes) 319 | { 320 | if (!defaultWeapons.TryGetValue(allocationType, out var w)) 321 | { 322 | warnings.Add($"Missing {allocationType} in DefaultWeapons.{team} config."); 323 | continue; 324 | } 325 | 326 | if (!WeaponHelpers.IsWeapon(w)) 327 | { 328 | throw new Exception($"{w} is not a valid weapon in config DefaultWeapons.{team}.{allocationType}."); 329 | } 330 | 331 | if (!UsableWeapons.Contains(w)) 332 | { 333 | warnings.Add( 334 | $"{w} in the DefaultWeapons.{team}.{allocationType} config " + 335 | $"is not in the UsableWeapons list."); 336 | } 337 | } 338 | 339 | return warnings; 340 | } 341 | 342 | public double GetRoundTypePercentage(RoundType roundType) 343 | { 344 | return Math.Round(RoundTypePercentages[roundType] / 100.0, 2); 345 | } 346 | 347 | public bool CanPlayersSelectWeapons() 348 | { 349 | return AllowedWeaponSelectionTypes.Contains(WeaponSelectionType.PlayerChoice); 350 | } 351 | 352 | public bool CanAssignRandomWeapons() 353 | { 354 | return AllowedWeaponSelectionTypes.Contains(WeaponSelectionType.Random); 355 | } 356 | 357 | public bool CanAssignDefaultWeapons() 358 | { 359 | return AllowedWeaponSelectionTypes.Contains(WeaponSelectionType.Default); 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/Config/ZeusPreference.cs: -------------------------------------------------------------------------------- 1 | namespace RetakesAllocatorCore.Config; 2 | 3 | public enum ZeusPreference 4 | { 5 | Never, 6 | Always, 7 | } -------------------------------------------------------------------------------- /RetakesAllocatorCore/Db/Db.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Modules.Entities.Constants; 2 | using CounterStrikeSharp.API.Modules.Utils; 3 | using Microsoft.EntityFrameworkCore; 4 | using RetakesAllocatorCore.Config; 5 | 6 | namespace RetakesAllocatorCore.Db; 7 | 8 | public class Db : DbContext 9 | { 10 | public DbSet UserSettings { get; set; } 11 | 12 | private static Db? Instance { get; set; } 13 | 14 | public static Db GetInstance() 15 | { 16 | return Instance ??= new Db(); 17 | } 18 | 19 | public static void Disconnect() 20 | { 21 | Instance?.Dispose(); 22 | Instance = null; 23 | } 24 | 25 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 26 | { 27 | optionsBuilder 28 | .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); 29 | 30 | // TODO This whole thing needs to be fixed per 31 | // https://jasonwatmore.com/post/2020/01/03/aspnet-core-ef-core-migrations-for-multiple-databases-sqlite-and-sql-server 32 | var configData = Configs.IsLoaded() ? Configs.GetConfigData() : new ConfigData(); 33 | var databaseConnectionString = configData.DatabaseConnectionString; 34 | switch (configData.DatabaseProvider) 35 | { 36 | case DatabaseProvider.Sqlite: 37 | Utils.SetupSqlite(databaseConnectionString, optionsBuilder); 38 | break; 39 | case DatabaseProvider.MySql: 40 | Utils.SetupMySql(databaseConnectionString, optionsBuilder); 41 | break; 42 | default: 43 | throw new ArgumentOutOfRangeException(); 44 | } 45 | } 46 | 47 | protected override void OnModelCreating(ModelBuilder modelBuilder) 48 | { 49 | modelBuilder.Entity() 50 | .Property(e => e.WeaponPreferences) 51 | .IsRequired(false); 52 | base.OnModelCreating(modelBuilder); 53 | } 54 | 55 | protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) 56 | { 57 | UserSetting.Configure(configurationBuilder); 58 | configurationBuilder 59 | .Properties() 60 | .HaveConversion(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/Db/Queries.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Modules.Entities.Constants; 2 | using CounterStrikeSharp.API.Modules.Utils; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace RetakesAllocatorCore.Db; 6 | 7 | public class Queries 8 | { 9 | public static async Task GetUserSettings(ulong userId) 10 | { 11 | return await Db.GetInstance().UserSettings.AsNoTracking().FirstOrDefaultAsync(u => u.UserId == userId); 12 | } 13 | 14 | private static async Task UpsertUserSettings(ulong userId, Action mutation) 15 | { 16 | if (userId == 0) 17 | { 18 | Log.Debug("Encountered userid 0, not upserting user settings"); 19 | return null; 20 | } 21 | 22 | Log.Debug($"Upserting settings for {userId}"); 23 | 24 | var instance = Db.GetInstance(); 25 | var isNew = false; 26 | var userSettings = await instance.UserSettings.AsNoTracking().FirstOrDefaultAsync(u => u.UserId == userId); 27 | if (userSettings is null) 28 | { 29 | userSettings = new UserSetting {UserId = userId}; 30 | await instance.UserSettings.AddAsync(userSettings); 31 | isNew = true; 32 | } 33 | 34 | instance.Entry(userSettings).State = isNew ? EntityState.Added : EntityState.Modified; 35 | 36 | mutation(userSettings); 37 | 38 | await instance.SaveChangesAsync(); 39 | instance.Entry(userSettings).State = EntityState.Detached; 40 | 41 | return userSettings; 42 | } 43 | 44 | public static async Task SetWeaponPreferenceForUserAsync(ulong userId, CsTeam team, 45 | WeaponAllocationType weaponAllocationType, 46 | CsItem? item) 47 | { 48 | await UpsertUserSettings(userId, 49 | userSetting => { userSetting.SetWeaponPreference(team, weaponAllocationType, item); }); 50 | } 51 | 52 | public static void SetWeaponPreferenceForUser(ulong userId, CsTeam team, WeaponAllocationType weaponAllocationType, 53 | CsItem? item) 54 | { 55 | Task.Run(async () => { await SetWeaponPreferenceForUserAsync(userId, team, weaponAllocationType, item); }); 56 | } 57 | 58 | public static async Task ClearWeaponPreferencesForUserAsync(ulong userId) 59 | { 60 | await UpsertUserSettings(userId, userSetting => { userSetting.WeaponPreferences = new(); }); 61 | } 62 | 63 | public static void ClearWeaponPreferencesForUser(ulong userId) 64 | { 65 | Task.Run(async () => { await ClearWeaponPreferencesForUserAsync(userId); }); 66 | } 67 | 68 | public static async Task SetPreferredWeaponPreferenceAsync(ulong userId, CsItem? item) 69 | { 70 | await UpsertUserSettings(userId, userSetting => 71 | { 72 | userSetting.SetWeaponPreference(CsTeam.Terrorist, WeaponAllocationType.Preferred, 73 | WeaponHelpers.CoercePreferredTeam(item, CsTeam.Terrorist)); 74 | userSetting.SetWeaponPreference(CsTeam.CounterTerrorist, WeaponAllocationType.Preferred, 75 | WeaponHelpers.CoercePreferredTeam(item, CsTeam.CounterTerrorist)); 76 | }); 77 | } 78 | 79 | public static void SetPreferredWeaponPreference(ulong userId, CsItem? item) 80 | { 81 | Task.Run(async () => { await SetPreferredWeaponPreferenceAsync(userId, item); }); 82 | } 83 | 84 | public static IDictionary GetUsersSettings(ICollection userIds) 85 | { 86 | var userSettingsList = Db.GetInstance() 87 | .UserSettings 88 | .AsNoTracking() 89 | .Where(u => userIds.Contains(u.UserId)) 90 | .ToList(); 91 | if (userSettingsList.Count == 0) 92 | { 93 | return new Dictionary(); 94 | } 95 | 96 | return userSettingsList 97 | .GroupBy(p => p.UserId) 98 | .ToDictionary(g => g.Key, g => g.First()); 99 | } 100 | 101 | public static void Migrate() 102 | { 103 | Db.GetInstance().Database.Migrate(); 104 | } 105 | 106 | public static void Wipe() 107 | { 108 | Db.GetInstance().UserSettings.ExecuteDelete(); 109 | } 110 | 111 | public static void Disconnect() 112 | { 113 | Db.Disconnect(); 114 | } 115 | } -------------------------------------------------------------------------------- /RetakesAllocatorCore/Db/UserSetting.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using System.Text.Json; 4 | using CounterStrikeSharp.API.Modules.Entities.Constants; 5 | using CounterStrikeSharp.API.Modules.Utils; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.EntityFrameworkCore.ChangeTracking; 8 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 9 | 10 | namespace RetakesAllocatorCore.Db; 11 | 12 | using WeaponPreferencesType = Dictionary< 13 | CsTeam, 14 | Dictionary 15 | >; 16 | 17 | public class UserSetting 18 | { 19 | [Key] 20 | [Required] 21 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 22 | [Column(TypeName = "bigint")] 23 | public ulong UserId { get; set; } 24 | 25 | [Column(TypeName = "text"), MaxLength(10000)] 26 | public WeaponPreferencesType WeaponPreferences { get; set; } = new(); 27 | 28 | public static void Configure(ModelConfigurationBuilder configurationBuilder) 29 | { 30 | configurationBuilder 31 | .Properties() 32 | .HaveConversion(); 33 | } 34 | 35 | public void SetWeaponPreference(CsTeam team, WeaponAllocationType weaponAllocationType, CsItem? weapon) 36 | { 37 | // Log.Write($"Setting preference for {UserId} {team} {weaponAllocationType} {weapon}"); 38 | if (!WeaponPreferences.TryGetValue(team, out var allocationPreference)) 39 | { 40 | allocationPreference = new(); 41 | WeaponPreferences.Add(team, allocationPreference); 42 | } 43 | 44 | if (weapon is not null) 45 | { 46 | allocationPreference[weaponAllocationType] = (CsItem) weapon; 47 | } 48 | else 49 | { 50 | allocationPreference.Remove(weaponAllocationType); 51 | } 52 | } 53 | 54 | public CsItem? GetWeaponPreference(CsTeam team, WeaponAllocationType weaponAllocationType) 55 | { 56 | if (WeaponPreferences.TryGetValue(team, out var allocationPreference)) 57 | { 58 | if (allocationPreference.TryGetValue(weaponAllocationType, out var weapon)) 59 | { 60 | return weapon; 61 | } 62 | } 63 | 64 | return null; 65 | } 66 | } 67 | 68 | public class CsItemConverter : ValueConverter 69 | { 70 | public CsItemConverter() : base( 71 | v => CsItemSerializer(v), 72 | s => CsItemDeserializer(s) 73 | ) 74 | { 75 | } 76 | 77 | public static string CsItemSerializer(CsItem? item) 78 | { 79 | return JsonSerializer.Serialize(item); 80 | } 81 | 82 | public static CsItem? CsItemDeserializer(string? str) 83 | { 84 | if (str is null) 85 | { 86 | return null; 87 | } 88 | 89 | return JsonSerializer.Deserialize(str); 90 | } 91 | } 92 | 93 | public class WeaponPreferencesConverter : ValueConverter 94 | { 95 | public WeaponPreferencesConverter() : base( 96 | v => WeaponPreferenceSerialize(v), 97 | s => WeaponPreferenceDeserialize(s) 98 | ) 99 | { 100 | } 101 | 102 | public static string WeaponPreferenceSerialize(WeaponPreferencesType? value) 103 | { 104 | if (value is null) 105 | { 106 | return ""; 107 | } 108 | 109 | return JsonSerializer.Serialize(value); 110 | } 111 | 112 | public static WeaponPreferencesType WeaponPreferenceDeserialize(string value) 113 | { 114 | WeaponPreferencesType? parseResult = null; 115 | try { 116 | parseResult = JsonSerializer.Deserialize(value); 117 | } catch (Exception e) 118 | { 119 | Log.Error($"Failed to deserialize weapon preferences: {e.Message}"); 120 | } 121 | 122 | return parseResult ?? new WeaponPreferencesType(); 123 | } 124 | } 125 | 126 | public class WeaponPreferencesComparer : ValueComparer 127 | { 128 | public WeaponPreferencesComparer() : base( 129 | (a, b) => 130 | WeaponPreferencesConverter.WeaponPreferenceSerialize(a).Equals( 131 | WeaponPreferencesConverter.WeaponPreferenceSerialize(b) 132 | ), 133 | (v) => WeaponPreferencesConverter.WeaponPreferenceSerialize(v).GetHashCode() 134 | ) 135 | { 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/Log.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Modules.Utils; 2 | using Microsoft.Extensions.Logging; 3 | using RetakesAllocatorCore.Config; 4 | 5 | namespace RetakesAllocatorCore; 6 | 7 | public static class Log 8 | { 9 | private static void Write(string message, LogLevel level) 10 | { 11 | var currentLevel = Configs.IsLoaded() ? Configs.GetConfigData().LogLevel : LogLevel.Error; 12 | if (currentLevel == LogLevel.None || level < currentLevel) 13 | { 14 | return; 15 | } 16 | 17 | Console.ResetColor(); 18 | switch (level) 19 | { 20 | case LogLevel.Warning: 21 | Console.ForegroundColor = ConsoleColor.Yellow; 22 | break; 23 | case LogLevel.Critical: 24 | case LogLevel.Error: 25 | Console.ForegroundColor = ConsoleColor.Red; 26 | break; 27 | default: 28 | // Looks red?? 29 | // Console.ForegroundColor = ConsoleColor.White; 30 | break; 31 | } 32 | 33 | Console.WriteLine($"{PluginInfo.LogPrefix}{message}"); 34 | Console.ResetColor(); 35 | } 36 | 37 | public static void Trace(string message) 38 | { 39 | Write(message, LogLevel.Trace); 40 | } 41 | 42 | public static void Debug(string message) 43 | { 44 | Write(message, LogLevel.Debug); 45 | } 46 | 47 | public static void Info(string message) 48 | { 49 | Write(message, LogLevel.Information); 50 | } 51 | 52 | public static void Warn(string message) 53 | { 54 | Write(message, LogLevel.Warning); 55 | } 56 | 57 | public static void Error(string message) 58 | { 59 | Write(message, LogLevel.Error); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/Managers/RoundTypeManager.cs: -------------------------------------------------------------------------------- 1 | using RetakesAllocatorCore.Config; 2 | 3 | namespace RetakesAllocatorCore.Managers; 4 | 5 | public class RoundTypeManager 6 | { 7 | #region Instance management 8 | 9 | private static RoundTypeManager? _instance; 10 | 11 | public static RoundTypeManager Instance => _instance ??= new RoundTypeManager(); 12 | 13 | #endregion 14 | 15 | private string? _map; 16 | 17 | private RoundType? _nextRoundTypeOverride; 18 | private RoundType? _currentRoundType; 19 | 20 | private RoundTypeSelectionOption _roundTypeSelection; 21 | private readonly List _roundsOrder = new(); 22 | private int _roundTypeManualOrderingPosition; 23 | 24 | private RoundTypeManager() 25 | { 26 | Initialize(); 27 | } 28 | 29 | public void Initialize() 30 | { 31 | _nextRoundTypeOverride = null; 32 | _currentRoundType = null; 33 | _roundTypeSelection = Configs.GetConfigData().RoundTypeSelection; 34 | 35 | _roundsOrder.Clear(); 36 | switch (_roundTypeSelection) 37 | { 38 | case RoundTypeSelectionOption.RandomFixedCounts: 39 | foreach (var (roundType, fixedCount) in Configs.GetConfigData().RoundTypeRandomFixedCounts) 40 | { 41 | for (var i = 0; i < fixedCount; i++) 42 | { 43 | _roundsOrder.Add(roundType); 44 | } 45 | } 46 | Utils.Shuffle(_roundsOrder); 47 | break; 48 | case RoundTypeSelectionOption.ManualOrdering: 49 | foreach (var item in Configs.GetConfigData().RoundTypeManualOrdering) 50 | { 51 | for (var i = 0; i < item.Count; i++) 52 | { 53 | _roundsOrder.Add(item.Type); 54 | } 55 | } 56 | break; 57 | } 58 | _roundTypeManualOrderingPosition = 0; 59 | } 60 | 61 | public void SetMap(string map) 62 | { 63 | _map = map; 64 | } 65 | 66 | public string? Map => _map; 67 | 68 | public RoundType GetNextRoundType() 69 | { 70 | if (_nextRoundTypeOverride is not null) 71 | { 72 | return _nextRoundTypeOverride.Value; 73 | } 74 | 75 | switch (_roundTypeSelection) 76 | { 77 | case RoundTypeSelectionOption.Random: 78 | return GetRandomRoundType(); 79 | case RoundTypeSelectionOption.ManualOrdering: 80 | case RoundTypeSelectionOption.RandomFixedCounts: 81 | return GetNextRoundTypeInOrder(); 82 | } 83 | 84 | throw new Exception("No round type selection type was found."); 85 | } 86 | 87 | private RoundType GetNextRoundTypeInOrder() 88 | { 89 | if (_roundTypeManualOrderingPosition >= _roundsOrder.Count) 90 | { 91 | _roundTypeManualOrderingPosition = 0; 92 | } 93 | return _roundsOrder[_roundTypeManualOrderingPosition++]; 94 | } 95 | 96 | private RoundType GetRandomRoundType() 97 | { 98 | var randomValue = new Random().NextDouble(); 99 | 100 | var pistolPercentage = Configs.GetConfigData().GetRoundTypePercentage(RoundType.Pistol); 101 | 102 | if (randomValue < pistolPercentage) 103 | { 104 | return RoundType.Pistol; 105 | } 106 | 107 | if (randomValue < Configs.GetConfigData().GetRoundTypePercentage(RoundType.HalfBuy) + pistolPercentage) 108 | { 109 | return RoundType.HalfBuy; 110 | } 111 | 112 | return RoundType.FullBuy; 113 | } 114 | 115 | public void SetNextRoundTypeOverride(RoundType? nextRoundType) 116 | { 117 | _nextRoundTypeOverride = nextRoundType; 118 | } 119 | 120 | public RoundType? GetCurrentRoundType() 121 | { 122 | return _currentRoundType; 123 | } 124 | 125 | public void SetCurrentRoundType(RoundType? currentRoundType) 126 | { 127 | _currentRoundType = currentRoundType; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/Migrations/20240105045524_InitialCreate.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using RetakesAllocatorCore.Db; 7 | 8 | #nullable disable 9 | 10 | namespace RetakesAllocator.Migrations 11 | { 12 | [DbContext(typeof(Db))] 13 | [Migration("20240105045524_InitialCreate")] 14 | partial class InitialCreate 15 | { 16 | /// 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder.HasAnnotation("ProductVersion", "7.0.14"); 21 | 22 | modelBuilder.Entity("RetakesAllocator.db.UserSetting", b => 23 | { 24 | b.Property("UserId") 25 | .ValueGeneratedOnAdd() 26 | .HasColumnType("INTEGER"); 27 | 28 | b.Property("WeaponPreferences") 29 | .HasMaxLength(10000) 30 | .HasColumnType("TEXT"); 31 | 32 | b.HasKey("UserId"); 33 | 34 | b.ToTable("UserSettings"); 35 | }); 36 | #pragma warning restore 612, 618 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/Migrations/20240105045524_InitialCreate.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace RetakesAllocator.Migrations 6 | { 7 | /// 8 | public partial class InitialCreate : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.CreateTable( 14 | name: "UserSettings", 15 | columns: table => new 16 | { 17 | UserId = table.Column(type: "INTEGER", nullable: false) 18 | .Annotation("Sqlite:Autoincrement", true), 19 | WeaponPreferences = table.Column(type: "TEXT", maxLength: 10000, nullable: true) 20 | }, 21 | constraints: table => 22 | { 23 | table.PrimaryKey("PK_UserSettings", x => x.UserId); 24 | }); 25 | } 26 | 27 | /// 28 | protected override void Down(MigrationBuilder migrationBuilder) 29 | { 30 | migrationBuilder.DropTable( 31 | name: "UserSettings"); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/Migrations/20240105050248_DontAutoIncrement.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using RetakesAllocatorCore.Db; 7 | 8 | #nullable disable 9 | 10 | namespace RetakesAllocator.Migrations 11 | { 12 | [DbContext(typeof(Db))] 13 | [Migration("20240105050248_DontAutoIncrement")] 14 | partial class DontAutoIncrement 15 | { 16 | /// 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder.HasAnnotation("ProductVersion", "7.0.14"); 21 | 22 | modelBuilder.Entity("RetakesAllocator.db.UserSetting", b => 23 | { 24 | b.Property("UserId") 25 | .HasColumnType("INTEGER"); 26 | 27 | b.Property("WeaponPreferences") 28 | .HasMaxLength(10000) 29 | .HasColumnType("TEXT"); 30 | 31 | b.HasKey("UserId"); 32 | 33 | b.ToTable("UserSettings"); 34 | }); 35 | #pragma warning restore 612, 618 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/Migrations/20240105050248_DontAutoIncrement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace RetakesAllocator.Migrations 6 | { 7 | /// 8 | public partial class DontAutoIncrement : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AlterColumn( 14 | name: "UserId", 15 | table: "UserSettings", 16 | type: "INTEGER", 17 | nullable: false, 18 | oldClrType: typeof(int), 19 | oldType: "INTEGER") 20 | .OldAnnotation("Sqlite:Autoincrement", true); 21 | } 22 | 23 | /// 24 | protected override void Down(MigrationBuilder migrationBuilder) 25 | { 26 | migrationBuilder.AlterColumn( 27 | name: "UserId", 28 | table: "UserSettings", 29 | type: "INTEGER", 30 | nullable: false, 31 | oldClrType: typeof(int), 32 | oldType: "INTEGER") 33 | .Annotation("Sqlite:Autoincrement", true); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/Migrations/20240116025022_BigIntTime.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using RetakesAllocatorCore.Db; 7 | 8 | #nullable disable 9 | 10 | namespace RetakesAllocator.Migrations 11 | { 12 | [DbContext(typeof(Db))] 13 | [Migration("20240116025022_BigIntTime")] 14 | partial class BigIntTime 15 | { 16 | /// 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder.HasAnnotation("ProductVersion", "7.0.14"); 21 | 22 | modelBuilder.Entity("RetakesAllocatorCore.Db.UserSetting", b => 23 | { 24 | b.Property("UserId") 25 | .HasColumnType("bigint"); 26 | 27 | b.Property("WeaponPreferences") 28 | .HasMaxLength(10000) 29 | .HasColumnType("text"); 30 | 31 | b.HasKey("UserId"); 32 | 33 | b.ToTable("UserSettings"); 34 | }); 35 | #pragma warning restore 612, 618 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/Migrations/20240116025022_BigIntTime.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace RetakesAllocator.Migrations 6 | { 7 | /// 8 | public partial class BigIntTime : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AlterColumn( 14 | name: "WeaponPreferences", 15 | table: "UserSettings", 16 | type: "text", 17 | maxLength: 10000, 18 | nullable: true, 19 | oldClrType: typeof(string), 20 | oldType: "TEXT", 21 | oldMaxLength: 10000, 22 | oldNullable: true); 23 | 24 | migrationBuilder.AlterColumn( 25 | name: "UserId", 26 | table: "UserSettings", 27 | type: "bigint", 28 | nullable: false, 29 | oldClrType: typeof(int), 30 | oldType: "INTEGER"); 31 | } 32 | 33 | /// 34 | protected override void Down(MigrationBuilder migrationBuilder) 35 | { 36 | migrationBuilder.AlterColumn( 37 | name: "WeaponPreferences", 38 | table: "UserSettings", 39 | type: "TEXT", 40 | maxLength: 10000, 41 | nullable: true, 42 | oldClrType: typeof(string), 43 | oldType: "text", 44 | oldMaxLength: 10000, 45 | oldNullable: true); 46 | 47 | migrationBuilder.AlterColumn( 48 | name: "UserId", 49 | table: "UserSettings", 50 | type: "INTEGER", 51 | nullable: false, 52 | oldClrType: typeof(ulong), 53 | oldType: "bigint"); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/Migrations/DbModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 5 | using RetakesAllocatorCore.Db; 6 | 7 | #nullable disable 8 | 9 | namespace RetakesAllocator.Migrations 10 | { 11 | [DbContext(typeof(Db))] 12 | partial class DbModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder.HasAnnotation("ProductVersion", "7.0.14"); 18 | 19 | modelBuilder.Entity("RetakesAllocatorCore.Db.UserSetting", b => 20 | { 21 | b.Property("UserId") 22 | .HasColumnType("bigint"); 23 | 24 | b.Property("WeaponPreferences") 25 | .HasMaxLength(10000) 26 | .HasColumnType("text"); 27 | 28 | b.HasKey("UserId"); 29 | 30 | b.ToTable("UserSettings"); 31 | }); 32 | #pragma warning restore 612, 618 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/NadeHelpers.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Modules.Entities.Constants; 2 | using CounterStrikeSharp.API.Modules.Utils; 3 | using RetakesAllocatorCore.Config; 4 | 5 | namespace RetakesAllocatorCore; 6 | 7 | public enum MaxTeamNadesSetting 8 | { 9 | None, 10 | One, 11 | Two, 12 | Three, 13 | Four, 14 | Five, 15 | Six, 16 | Seven, 17 | Eight, 18 | Nine, 19 | Ten, 20 | AveragePointFivePerPlayer, 21 | AverageOnePerPlayer, 22 | AverageOnePointFivePerPlayer, 23 | AverageTwoPerPlayer, 24 | } 25 | 26 | public class NadeHelpers 27 | { 28 | public static string GlobalSettingName = "GLOBAL"; 29 | 30 | public static Stack GetUtilForTeam(string? map, RoundType roundType, CsTeam team, int numPlayers) 31 | { 32 | map ??= GlobalSettingName; 33 | 34 | var maxNadesSetting = GetMaxTeamNades(map, team, roundType); 35 | if (maxNadesSetting == MaxTeamNadesSetting.None) 36 | { 37 | return new(); 38 | } 39 | 40 | var multiplier = maxNadesSetting switch 41 | { 42 | MaxTeamNadesSetting.AveragePointFivePerPlayer => 0.5, 43 | MaxTeamNadesSetting.AverageOnePerPlayer => 1, 44 | MaxTeamNadesSetting.AverageOnePointFivePerPlayer => 1.5, 45 | MaxTeamNadesSetting.AverageTwoPerPlayer => 2, 46 | _ => 0, 47 | }; 48 | 49 | var maxTotalNades = maxNadesSetting switch 50 | { 51 | MaxTeamNadesSetting.One => 1, 52 | MaxTeamNadesSetting.Two => 2, 53 | MaxTeamNadesSetting.Three => 3, 54 | MaxTeamNadesSetting.Four => 4, 55 | MaxTeamNadesSetting.Five => 5, 56 | MaxTeamNadesSetting.Six => 6, 57 | MaxTeamNadesSetting.Seven => 7, 58 | MaxTeamNadesSetting.Eight => 8, 59 | MaxTeamNadesSetting.Nine => 9, 60 | MaxTeamNadesSetting.Ten => 10, 61 | _ => (int) Math.Ceiling(numPlayers * multiplier) 62 | }; 63 | 64 | Log.Debug($"Nade setting: {maxNadesSetting}. Total: {maxTotalNades}. Map: {map}"); 65 | 66 | var molly = team == CsTeam.Terrorist ? CsItem.Molotov : CsItem.Incendiary; 67 | var nadeDistribution = new List 68 | { 69 | CsItem.Flashbang, CsItem.Flashbang, CsItem.Flashbang, CsItem.Flashbang, 70 | CsItem.Smoke, CsItem.Smoke, CsItem.Smoke, 71 | CsItem.HE, CsItem.HE, CsItem.HE, 72 | molly, molly 73 | }; 74 | 75 | var nadeAllocations = new Dictionary 76 | { 77 | {CsItem.Flashbang, GetMaxNades(map, team, CsItem.Flashbang)}, 78 | {CsItem.Smoke, GetMaxNades(map, team, CsItem.Smoke)}, 79 | {CsItem.HE, GetMaxNades(map, team, CsItem.HE)}, 80 | {molly, GetMaxNades(map, team, molly)}, 81 | }; 82 | 83 | var nades = new Stack(); 84 | while (true) 85 | { 86 | if (nadeAllocations.Count == 0 || maxTotalNades <= 0) 87 | { 88 | break; 89 | } 90 | 91 | var nextNade = Utils.Choice(nadeDistribution); 92 | if (nadeAllocations[nextNade] <= 0) 93 | { 94 | nadeDistribution.RemoveAll(item => item == nextNade); 95 | nadeAllocations.Remove(nextNade); 96 | continue; 97 | } 98 | 99 | nades.Push(nextNade); 100 | nadeAllocations[nextNade]--; 101 | maxTotalNades--; 102 | } 103 | 104 | return nades; 105 | } 106 | 107 | private static MaxTeamNadesSetting GetMaxTeamNades(string map, CsTeam team, RoundType roundType) 108 | { 109 | if (Configs.GetConfigData().MaxTeamNades.TryGetValue(map, out var mapMaxNades)) 110 | { 111 | if (mapMaxNades.TryGetValue(team, out var teamMaxNades)) 112 | { 113 | if (teamMaxNades.TryGetValue(roundType, out var maxNadesSetting)) 114 | { 115 | return maxNadesSetting; 116 | } 117 | } 118 | } 119 | 120 | if (map == GlobalSettingName) 121 | { 122 | return MaxTeamNadesSetting.None; 123 | } 124 | 125 | return GetMaxTeamNades(GlobalSettingName, team, roundType); 126 | } 127 | 128 | private static int GetMaxNades(string map, CsTeam team, CsItem nade) 129 | { 130 | if (Configs.GetConfigData().MaxNades.TryGetValue(map, out var mapNades)) 131 | { 132 | if (mapNades.TryGetValue(team, out var teamNades)) 133 | { 134 | int nadeCount; 135 | if (teamNades.TryGetValue(nade, out nadeCount)) 136 | { 137 | return nadeCount; 138 | } 139 | 140 | if (nade is CsItem.Molotov or CsItem.Incendiary) 141 | { 142 | var otherNade = nade == CsItem.Molotov ? CsItem.Incendiary : CsItem.Molotov; 143 | if (teamNades.TryGetValue(otherNade, out nadeCount)) 144 | { 145 | return nadeCount; 146 | } 147 | } 148 | } 149 | } 150 | 151 | if (map == GlobalSettingName) 152 | { 153 | return 999999; 154 | } 155 | 156 | return GetMaxNades(GlobalSettingName, team, nade); 157 | } 158 | 159 | private static bool PlayerReachedMaxNades(ICollection nades) 160 | { 161 | var allowancePerType = new Dictionary 162 | { 163 | {CsItem.Flashbang, 2}, 164 | {CsItem.Smoke, 1}, 165 | {CsItem.HE, 1}, 166 | {CsItem.Molotov, 1}, 167 | {CsItem.Incendiary, 1}, 168 | }; 169 | foreach (var nade in nades) 170 | { 171 | if (!allowancePerType.ContainsKey(nade) || allowancePerType[nade] <= 0) 172 | { 173 | return true; 174 | } 175 | 176 | allowancePerType[nade]--; 177 | } 178 | 179 | return false; 180 | } 181 | 182 | public static void AllocateNadesToPlayers( 183 | Stack teamNades, 184 | ICollection teamPlayers, 185 | Dictionary> nadesByPlayer 186 | ) where T : notnull 187 | { 188 | // Copy to avoid mutating the actual player list 189 | var teamPlayersShuffled = new List(teamPlayers); 190 | // Shuffle for fairness 191 | Utils.Shuffle(teamPlayersShuffled); 192 | 193 | var playerI = 0; 194 | while (teamNades.Count != 0 && teamPlayersShuffled.Count != 0) 195 | { 196 | var player = teamPlayersShuffled[playerI]; 197 | 198 | if (!nadesByPlayer.TryGetValue(player, out var nadesForPlayer)) 199 | { 200 | nadesForPlayer = new List(); 201 | nadesByPlayer.Add(player, nadesForPlayer); 202 | } 203 | 204 | // If a player has reached max nades, remove them from the list and try again at the same index, 205 | // which is now the next player 206 | if (PlayerReachedMaxNades(nadesForPlayer)) 207 | { 208 | teamPlayersShuffled.RemoveAt(playerI); 209 | if (playerI >= teamPlayersShuffled.Count) 210 | { 211 | playerI = 0; 212 | } 213 | continue; 214 | } 215 | 216 | if (!teamNades.TryPop(out var nextNade)) 217 | { 218 | break; 219 | } 220 | 221 | nadesForPlayer.Add(nextNade); 222 | 223 | playerI++; 224 | if (playerI >= teamPlayersShuffled.Count) 225 | { 226 | playerI = 0; 227 | } 228 | } 229 | } 230 | } -------------------------------------------------------------------------------- /RetakesAllocatorCore/OnRoundPostStartHelper.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Modules.Entities.Constants; 2 | using CounterStrikeSharp.API.Modules.Utils; 3 | using RetakesAllocatorCore.Config; 4 | using RetakesAllocatorCore.Db; 5 | using RetakesAllocatorCore.Managers; 6 | using System; 7 | 8 | namespace RetakesAllocatorCore; 9 | 10 | public class OnRoundPostStartHelper 11 | { 12 | public static void Handle( 13 | ICollection allPlayers, 14 | Func getSteamId, 15 | Func getTeam, 16 | Action giveDefuseKit, 17 | Action, string?> allocateItemsForPlayer, 18 | Func isVip, 19 | out RoundType currentRoundType 20 | ) where T : notnull 21 | { 22 | var roundType = RoundTypeManager.Instance.GetNextRoundType(); 23 | currentRoundType = roundType; 24 | 25 | var tPlayers = new List(); 26 | var ctPlayers = new List(); 27 | var playerIds = new List(); 28 | foreach (var player in allPlayers) 29 | { 30 | var steamId = getSteamId(player); 31 | if (steamId != 0) 32 | { 33 | playerIds.Add(steamId); 34 | } 35 | 36 | var playerTeam = getTeam(player); 37 | if (playerTeam == CsTeam.Terrorist) 38 | { 39 | tPlayers.Add(player); 40 | } 41 | else if (playerTeam == CsTeam.CounterTerrorist) 42 | { 43 | ctPlayers.Add(player); 44 | } 45 | } 46 | 47 | Log.Debug($"#T Players: {string.Join(",", tPlayers.Select(getSteamId))}"); 48 | Log.Debug($"#CT Players: {string.Join(",", ctPlayers.Select(getSteamId))}"); 49 | 50 | var userSettingsByPlayerId = Queries.GetUsersSettings(playerIds); 51 | 52 | var defusingPlayer = Utils.Choice(ctPlayers); 53 | 54 | HashSet FilterByPreferredWeaponPreference(IEnumerable ps) => 55 | ps.Where(p => 56 | userSettingsByPlayerId.TryGetValue(getSteamId(p), out var userSetting) && 57 | userSetting.GetWeaponPreference(getTeam(p), WeaponAllocationType.Preferred) is not null) 58 | .ToHashSet(); 59 | 60 | ICollection tPreferredPlayers = new List(); 61 | ICollection ctPreferredPlayers = new List(); 62 | 63 | Random random = new Random(); 64 | double generatedChance = random.NextDouble() * 100; 65 | 66 | if (roundType == RoundType.FullBuy && generatedChance <= Configs.GetConfigData().ChanceForPreferredWeapon) 67 | { 68 | tPreferredPlayers = 69 | WeaponHelpers.SelectPreferredPlayers(FilterByPreferredWeaponPreference(tPlayers), isVip, 70 | CsTeam.Terrorist); 71 | ctPreferredPlayers = 72 | WeaponHelpers.SelectPreferredPlayers(FilterByPreferredWeaponPreference(ctPlayers), isVip, 73 | CsTeam.CounterTerrorist); 74 | } 75 | 76 | var nadesByPlayer = new Dictionary>(); 77 | NadeHelpers.AllocateNadesToPlayers( 78 | NadeHelpers.GetUtilForTeam( 79 | RoundTypeManager.Instance.Map, 80 | roundType, 81 | CsTeam.Terrorist, 82 | tPlayers.Count 83 | ), 84 | tPlayers, 85 | nadesByPlayer 86 | ); 87 | NadeHelpers.AllocateNadesToPlayers( 88 | NadeHelpers.GetUtilForTeam( 89 | RoundTypeManager.Instance.Map, 90 | roundType, 91 | CsTeam.CounterTerrorist, 92 | tPlayers.Count 93 | ), 94 | ctPlayers, 95 | nadesByPlayer 96 | ); 97 | 98 | foreach (var player in allPlayers) 99 | { 100 | var team = getTeam(player); 101 | var playerSteamId = getSteamId(player); 102 | userSettingsByPlayerId.TryGetValue(playerSteamId, out var userSetting); 103 | var items = new List 104 | { 105 | RoundTypeHelpers.GetArmorForRoundType(roundType), 106 | team == CsTeam.Terrorist ? CsItem.DefaultKnifeT : CsItem.DefaultKnifeCT, 107 | }; 108 | 109 | var givePreferred = team switch 110 | { 111 | CsTeam.Terrorist => tPreferredPlayers.Contains(player), 112 | CsTeam.CounterTerrorist => ctPreferredPlayers.Contains(player), 113 | _ => false, 114 | }; 115 | 116 | items.AddRange( 117 | WeaponHelpers.GetWeaponsForRoundType( 118 | roundType, 119 | team, 120 | userSetting, 121 | givePreferred 122 | ) 123 | ); 124 | 125 | if (nadesByPlayer.TryGetValue(player, out var playerNades)) 126 | { 127 | items.AddRange(playerNades); 128 | } 129 | 130 | if (team == CsTeam.CounterTerrorist) 131 | { 132 | // On non-pistol rounds, everyone gets defuse kit and util 133 | if (roundType != RoundType.Pistol) 134 | { 135 | giveDefuseKit(player); 136 | } 137 | else if (getSteamId(defusingPlayer) == getSteamId(player)) 138 | { 139 | // On pistol rounds, only one person gets a defuse kit 140 | giveDefuseKit(player); 141 | } 142 | } 143 | 144 | if (Configs.GetConfigData().ZeusPreference == ZeusPreference.Always) 145 | { 146 | items.Add(CsItem.Zeus); 147 | } 148 | 149 | allocateItemsForPlayer(player, items, team == CsTeam.Terrorist ? "slot5" : "slot1"); 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /RetakesAllocatorCore/OnWeaponCommandHelper.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Modules.Entities.Constants; 2 | using CounterStrikeSharp.API.Modules.Utils; 3 | using RetakesAllocatorCore.Db; 4 | using RetakesAllocatorCore.Config; 5 | 6 | namespace RetakesAllocatorCore; 7 | 8 | public class OnWeaponCommandHelper 9 | { 10 | public static string Handle(ICollection args, ulong userId, RoundType? roundType, CsTeam currentTeam, 11 | bool remove, out CsItem? outWeapon) 12 | { 13 | var result = Task.Run(() => HandleAsync(args, userId, roundType, currentTeam, remove)).Result; 14 | outWeapon = result.Item2; 15 | return result.Item1; 16 | } 17 | 18 | public static async Task> HandleAsync(ICollection args, ulong userId, 19 | RoundType? roundType, CsTeam currentTeam, 20 | bool remove) 21 | { 22 | CsItem? outWeapon = null; 23 | 24 | Tuple Ret(string str) => new(str, outWeapon); 25 | 26 | if (!Configs.GetConfigData().CanPlayersSelectWeapons()) 27 | { 28 | return Ret(Translator.Instance["weapon_preference.cannot_choose"]); 29 | } 30 | 31 | if (args.Count == 0) 32 | { 33 | var gunsMessage = Translator.Instance[ 34 | "weapon_preference.gun_usage", 35 | currentTeam, 36 | string.Join(", ", 37 | WeaponHelpers.GetPossibleWeaponsForAllocationType(WeaponAllocationType.PistolRound, currentTeam)), 38 | string.Join(", ", 39 | WeaponHelpers.GetPossibleWeaponsForAllocationType(WeaponAllocationType.HalfBuyPrimary, 40 | currentTeam)), 41 | string.Join(", ", 42 | WeaponHelpers.GetPossibleWeaponsForAllocationType(WeaponAllocationType.FullBuyPrimary, currentTeam)) 43 | ]; 44 | return Ret(gunsMessage); 45 | } 46 | 47 | var weaponInput = args.ElementAt(0).Trim(); 48 | 49 | CsTeam team; 50 | var teamInput = args.ElementAtOrDefault(1)?.Trim().ToLower(); 51 | if (teamInput is not null) 52 | { 53 | var parsedTeamInput = Utils.ParseTeam(teamInput); 54 | if (parsedTeamInput == CsTeam.None) 55 | { 56 | return Ret(Translator.Instance["weapon_preference.invalid_team", teamInput]); 57 | } 58 | 59 | team = parsedTeamInput; 60 | } 61 | else if (currentTeam is CsTeam.None or CsTeam.Spectator) 62 | { 63 | return Ret(Translator.Instance["weapon_preference.join_team"]); 64 | } 65 | else 66 | { 67 | team = currentTeam; 68 | } 69 | 70 | var foundWeapons = WeaponHelpers.FindValidWeaponsByName(weaponInput); 71 | if (foundWeapons.Count == 0) 72 | { 73 | return Ret(Translator.Instance["weapon_preference.not_found", weaponInput]); 74 | } 75 | 76 | var weapon = foundWeapons.First(); 77 | 78 | if (!WeaponHelpers.IsUsableWeapon(weapon)) 79 | { 80 | return Ret(Translator.Instance["weapon_preference.not_allowed", weapon]); 81 | } 82 | 83 | var weaponRoundTypes = WeaponHelpers.GetRoundTypesForWeapon(weapon); 84 | if (weaponRoundTypes.Count == 0) 85 | { 86 | return Ret(Translator.Instance["weapon_preference.invalid_weapon", weapon]); 87 | } 88 | 89 | var allocationType = WeaponHelpers.GetWeaponAllocationTypeForWeaponAndRound( 90 | roundType, team, weapon 91 | ); 92 | var isPreferred = allocationType == WeaponAllocationType.Preferred; 93 | 94 | var allocateImmediately = ( 95 | // Always true for pistols 96 | allocationType is not null && 97 | roundType is not null && 98 | weaponRoundTypes.Contains(roundType.Value) && 99 | // Only set the outWeapon if the user is setting the preference for their current team 100 | currentTeam == team && 101 | // TODO Allow immediate allocation of preferred if the config permits it (eg. unlimited preferred) 102 | // Could be tricky for max # per team config, since this function doesnt know # of players on the team 103 | !isPreferred 104 | ); 105 | 106 | if (allocationType is null) 107 | { 108 | return Ret(Translator.Instance["weapon_preference.not_valid_for_team", weapon, team]); 109 | } 110 | 111 | 112 | if (remove) 113 | { 114 | if (isPreferred) 115 | { 116 | _ = Queries.SetPreferredWeaponPreferenceAsync(userId, null); 117 | return Ret(Translator.Instance["weapon_preference.unset_preference_preferred", weapon]); 118 | } 119 | else 120 | { 121 | _ = Queries.SetWeaponPreferenceForUserAsync(userId, team, allocationType.Value, null); 122 | return Ret( 123 | Translator.Instance["weapon_preference.unset_preference", weapon, allocationType.Value, team]); 124 | } 125 | } 126 | 127 | string message; 128 | if (isPreferred) 129 | { 130 | _ = Queries.SetPreferredWeaponPreferenceAsync(userId, weapon); 131 | // If we ever add more preferred weapons, we need to change the wording of "sniper" here 132 | message = Translator.Instance["weapon_preference.set_preference_preferred", weapon]; 133 | } 134 | else 135 | { 136 | _ = Queries.SetWeaponPreferenceForUserAsync(userId, team, allocationType.Value, weapon); 137 | message = Translator.Instance["weapon_preference.set_preference", weapon, allocationType.Value, team]; 138 | } 139 | 140 | if (allocateImmediately) 141 | { 142 | outWeapon = weapon; 143 | } 144 | else if (!isPreferred) 145 | { 146 | message += Translator.Instance["weapon_preference.receive_next_round", weaponRoundTypes.First()]; 147 | } 148 | 149 | if (userId == 0) 150 | { 151 | message = Translator.Instance["weapon_preference.not_saved"]; 152 | } 153 | 154 | return Ret(message); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/PluginInfo.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Modules.Utils; 2 | using RetakesAllocatorCore.Config; 3 | 4 | namespace RetakesAllocatorCore; 5 | 6 | public static class PluginInfo 7 | { 8 | public const string Version = "2.4.1"; 9 | 10 | public static readonly string LogPrefix = $"[RetakesAllocator {Version}] "; 11 | 12 | public static string MessagePrefix 13 | { 14 | get 15 | { 16 | var name = "Retakes"; 17 | if (Configs.IsLoaded()) 18 | { 19 | if (Configs.GetConfigData().ChatMessagePluginPrefix is not null) 20 | { 21 | // If message starts with color code it wont work. Hacky fix. 22 | return " " + Translator.Color(Configs.GetConfigData().ChatMessagePluginPrefix!); 23 | } 24 | 25 | name = Configs.GetConfigData().ChatMessagePluginName; 26 | } 27 | 28 | return $"[{ChatColors.Green}{name}{ChatColors.White}] "; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/RetakesAllocatorCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/RoundType.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Modules.Entities.Constants; 2 | using CounterStrikeSharp.API.Modules.Utils; 3 | 4 | namespace RetakesAllocatorCore; 5 | 6 | public enum RoundType 7 | { 8 | Pistol, 9 | HalfBuy, 10 | FullBuy, 11 | } 12 | 13 | public static class RoundTypeHelpers 14 | { 15 | public static List GetRoundTypes() 16 | { 17 | return Enum.GetValues().ToList(); 18 | } 19 | 20 | public static IEnumerable GetRandomUtilForRoundType(RoundType roundType, CsTeam team) 21 | { 22 | // Limited util on pistol rounds 23 | if (roundType == RoundType.Pistol) 24 | { 25 | return new List 26 | { 27 | Utils.Choice(new List 28 | { 29 | CsItem.Flashbang, 30 | CsItem.Smoke, 31 | }), 32 | }; 33 | } 34 | 35 | // All util options are available on buy rounds 36 | var possibleItems = new HashSet() 37 | { 38 | CsItem.Flashbang, 39 | CsItem.Smoke, 40 | CsItem.HEGrenade, 41 | team == CsTeam.Terrorist ? CsItem.Molotov : CsItem.Incendiary, 42 | }; 43 | 44 | var firstUtil = Utils.Choice(possibleItems); 45 | 46 | // Everyone gets one util 47 | var randomUtil = new List 48 | { 49 | firstUtil, 50 | }; 51 | 52 | // 50% chance to get an extra util item 53 | if (new Random().NextDouble() < .5) 54 | { 55 | // We cant give people duplicate of anything other than a flash though 56 | if (firstUtil != CsItem.Flashbang) 57 | { 58 | possibleItems.Remove(firstUtil); 59 | } 60 | 61 | randomUtil.Add(Utils.Choice(possibleItems)); 62 | } 63 | 64 | return randomUtil; 65 | } 66 | 67 | public static CsItem GetArmorForRoundType(RoundType roundType) => 68 | roundType == RoundType.Pistol ? CsItem.Kevlar : CsItem.KevlarHelmet; 69 | 70 | public static RoundType? ParseRoundType(string roundType) 71 | { 72 | return roundType.ToLower() switch 73 | { 74 | "f" => RoundType.FullBuy, 75 | "full" => RoundType.FullBuy, 76 | "fullbuy" => RoundType.FullBuy, 77 | "h" => RoundType.HalfBuy, 78 | "half" => RoundType.HalfBuy, 79 | "halfbuy" => RoundType.HalfBuy, 80 | "force" => RoundType.HalfBuy, 81 | "forcebuy" => RoundType.HalfBuy, 82 | "p" => RoundType.Pistol, 83 | "pistol" => RoundType.Pistol, 84 | _ => null, 85 | }; 86 | } 87 | 88 | public static string TranslateRoundTypeName(RoundType roundType) 89 | { 90 | return Translator.Instance[$"roundtype.{roundType}"]; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/Translator.cs: -------------------------------------------------------------------------------- 1 | // Stolen from https://github.com/B3none/cs2-retakes/blob/014663222fa95bb9f506284814ae62205630416c/Modules/Translator.cs 2 | 3 | using CounterStrikeSharp.API.Modules.Utils; 4 | using Microsoft.Extensions.Localization; 5 | 6 | namespace RetakesAllocatorCore; 7 | 8 | public class Translator 9 | { 10 | private static Translator? _instance; 11 | 12 | public static Translator Initialize(IStringLocalizer localizer) 13 | { 14 | _instance = new(localizer); 15 | return _instance; 16 | } 17 | 18 | public static bool IsInitialized => _instance is not null; 19 | 20 | public static Translator Instance => _instance ?? throw new Exception("Translator is not initialized."); 21 | 22 | private IStringLocalizer _stringLocalizerImplementation; 23 | 24 | public Translator(IStringLocalizer localizer) 25 | { 26 | _stringLocalizerImplementation = localizer; 27 | } 28 | 29 | public IEnumerable GetAllStrings(bool includeParentCultures) 30 | { 31 | return _stringLocalizerImplementation.GetAllStrings(includeParentCultures); 32 | } 33 | 34 | public string this[string name] => Translate(name); 35 | 36 | public string this[string name, params object[] arguments] => Translate(name, arguments); 37 | 38 | private string Translate(string key, params object[] arguments) 39 | { 40 | var isCenter = key.StartsWith("center."); 41 | key = key.Replace("center.", ""); 42 | 43 | var localizedString = _stringLocalizerImplementation[key, arguments]; 44 | 45 | if (localizedString == null || localizedString.ResourceNotFound) 46 | { 47 | return key; 48 | } 49 | 50 | return isCenter ? localizedString.Value : Color(localizedString.Value); 51 | } 52 | 53 | public static string Color(string text) 54 | { 55 | return text 56 | .Replace("[GREEN]", ChatColors.Green.ToString()) 57 | .Replace("[RED]", ChatColors.Red.ToString()) 58 | .Replace("[YELLOW]", ChatColors.Yellow.ToString()) 59 | .Replace("[BLUE]", ChatColors.Blue.ToString()) 60 | .Replace("[PURPLE]", ChatColors.Purple.ToString()) 61 | .Replace("[ORANGE]", ChatColors.Orange.ToString()) 62 | .Replace("[WHITE]", ChatColors.White.ToString()) 63 | .Replace("[NORMAL]", ChatColors.White.ToString()) 64 | .Replace("[GREY]", ChatColors.Grey.ToString()) 65 | .Replace("[LIGHT_RED]", ChatColors.LightRed.ToString()) 66 | .Replace("[LIGHT_BLUE]", ChatColors.LightBlue.ToString()) 67 | .Replace("[LIGHT_PURPLE]", ChatColors.LightPurple.ToString()) 68 | .Replace("[LIGHT_YELLOW]", ChatColors.LightYellow.ToString()) 69 | .Replace("[DARK_RED]", ChatColors.DarkRed.ToString()) 70 | .Replace("[DARK_BLUE]", ChatColors.DarkBlue.ToString()) 71 | .Replace("[BLUE_GREY]", ChatColors.BlueGrey.ToString()) 72 | .Replace("[OLIVE]", ChatColors.Olive.ToString()) 73 | .Replace("[LIME]", ChatColors.Lime.ToString()) 74 | .Replace("[GOLD]", ChatColors.Gold.ToString()) 75 | .Replace("[SILVER]", ChatColors.Silver.ToString()) 76 | .Replace("[MAGENTA]", ChatColors.Magenta.ToString()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /RetakesAllocatorCore/Utils.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using CounterStrikeSharp.API.Modules.Utils; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace RetakesAllocatorCore; 6 | 7 | public static class Utils 8 | { 9 | /** 10 | * Randomly get an item from the collection 11 | */ 12 | public static T? Choice(ICollection items) 13 | { 14 | // Log.Write($"Item count: {items.Count}"); 15 | if (items.Count == 0) 16 | { 17 | return default; 18 | } 19 | 20 | var random = new Random().Next(items.Count); 21 | // Log.Write($"Random: {random}"); 22 | var item = items.ElementAt(random); 23 | // Log.Write($"Item: {item}"); 24 | return item; 25 | } 26 | 27 | public static void Shuffle(IList list) 28 | { 29 | var random = new Random(); 30 | var n = list.Count; 31 | while (n > 1) 32 | { 33 | n--; 34 | var k = random.Next(n + 1); 35 | (list[k], list[n]) = (list[n], list[k]); 36 | } 37 | } 38 | 39 | public static CsTeam ParseTeam(string teamInput) 40 | { 41 | return teamInput.ToLower() switch 42 | { 43 | "t" => CsTeam.Terrorist, 44 | "terrorist" => CsTeam.Terrorist, 45 | "ct" => CsTeam.CounterTerrorist, 46 | "counterterrorist" => CsTeam.CounterTerrorist, 47 | _ => CsTeam.None, 48 | }; 49 | } 50 | 51 | public static string TeamString(CsTeam team, bool shortName = false) 52 | { 53 | return team switch 54 | { 55 | CsTeam.Terrorist => Translator.Instance[shortName ? "teams.terrorist_short" : "teams.terrorist"], 56 | CsTeam.CounterTerrorist => Translator.Instance[ 57 | shortName ? "teams.counter_terrorist_short" : "teams.counter_terrorist" 58 | ], 59 | _ => "" 60 | }; 61 | } 62 | 63 | public static T? ToEnum(string str) 64 | { 65 | var enumType = typeof(T); 66 | try 67 | { 68 | foreach (var name in Enum.GetNames(enumType)) 69 | { 70 | // Log.Write($"Enum name {name}"); 71 | var enumMemberAttribute = 72 | ((EnumMemberAttribute[]) enumType.GetField(name)!.GetCustomAttributes(typeof(EnumMemberAttribute), 73 | true)).SingleOrDefault(); 74 | // Log.Write($"Custom attribute: {enumMemberAttribute?.Value}"); 75 | if (enumMemberAttribute?.Value == str) 76 | { 77 | return (T) Enum.Parse(enumType, name); 78 | } 79 | } 80 | } 81 | catch (Exception e) 82 | { 83 | Log.Error($"Exception parsing enum {e.Message}"); 84 | } 85 | 86 | return default; 87 | } 88 | 89 | public static void SetupMySql(string connectionString, DbContextOptionsBuilder optionsBuilder) 90 | { 91 | var version = ServerVersion.AutoDetect(connectionString); 92 | optionsBuilder.UseMySql(connectionString, version); 93 | } 94 | 95 | public static void SetupSqlite(string connectionString, DbContextOptionsBuilder optionsBuilder) 96 | { 97 | optionsBuilder.UseSqlite(connectionString); 98 | } 99 | } -------------------------------------------------------------------------------- /RetakesAllocatorCore/WeaponHelpers.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using CounterStrikeSharp.API.Modules.Admin; 3 | using CounterStrikeSharp.API.Modules.Entities.Constants; 4 | using CounterStrikeSharp.API.Modules.Utils; 5 | using RetakesAllocatorCore.Config; 6 | using RetakesAllocatorCore.Db; 7 | 8 | namespace RetakesAllocatorCore; 9 | 10 | public enum WeaponAllocationType 11 | { 12 | FullBuyPrimary, 13 | HalfBuyPrimary, 14 | Secondary, 15 | PistolRound, 16 | 17 | // eg. AWP is a preferred gun - you cant always get it even if its your preference 18 | // Right now its only snipers, but if we make this configurable, we need to change: 19 | // - CoercePreferredTeam 20 | // - "your turn" wording in the weapon command handler 21 | Preferred, 22 | } 23 | 24 | public enum ItemSlotType 25 | { 26 | Primary, 27 | Secondary, 28 | Util 29 | } 30 | 31 | public static class WeaponHelpers 32 | { 33 | private static readonly ICollection _sharedPistols = new HashSet 34 | { 35 | CsItem.Deagle, 36 | CsItem.P250, 37 | CsItem.CZ, 38 | CsItem.Dualies, 39 | CsItem.R8, 40 | }; 41 | 42 | private static readonly ICollection _tPistols = new HashSet 43 | { 44 | CsItem.Glock, 45 | CsItem.Tec9, 46 | }; 47 | 48 | 49 | private static readonly ICollection _ctPistols = new HashSet 50 | { 51 | CsItem.USPS, 52 | CsItem.P2000, 53 | CsItem.FiveSeven, 54 | }; 55 | 56 | private static readonly ICollection _pistolsForT = 57 | _sharedPistols.Concat(_tPistols).ToHashSet(); 58 | 59 | private static readonly ICollection _pistolsForCt = 60 | _sharedPistols.Concat(_ctPistols).ToHashSet(); 61 | 62 | private static readonly ICollection _sharedMidRange = new HashSet 63 | { 64 | // SMG 65 | CsItem.P90, 66 | CsItem.UMP45, 67 | CsItem.MP7, 68 | CsItem.Bizon, 69 | CsItem.MP5, 70 | 71 | // Shotgun 72 | CsItem.XM1014, 73 | CsItem.Nova, 74 | 75 | // Sniper 76 | CsItem.Scout, 77 | }; 78 | 79 | private static readonly ICollection _tMidRange = new HashSet 80 | { 81 | CsItem.Mac10, 82 | CsItem.SawedOff, 83 | }; 84 | 85 | 86 | private static readonly ICollection _ctMidRange = new HashSet 87 | { 88 | CsItem.MP9, 89 | CsItem.MAG7, 90 | }; 91 | 92 | private static readonly ICollection _midRangeForCt = _sharedMidRange.Concat(_ctMidRange).ToHashSet(); 93 | private static readonly ICollection _midRangeForT = _sharedMidRange.Concat(_tMidRange).ToHashSet(); 94 | 95 | private static readonly int _maxSmgItemValue = (int)CsItem.UMP; 96 | 97 | private static readonly ICollection _smgsForT = 98 | _sharedMidRange.Concat(_tMidRange).Where(i => (int)i <= _maxSmgItemValue).ToHashSet(); 99 | 100 | private static readonly ICollection _smgsForCt = 101 | _sharedMidRange.Concat(_ctMidRange).Where(i => (int)i <= _maxSmgItemValue).ToHashSet(); 102 | 103 | private static readonly ICollection _tRifles = new HashSet 104 | { 105 | CsItem.AK47, 106 | CsItem.Galil, 107 | CsItem.Krieg, 108 | }; 109 | 110 | private static readonly ICollection _ctRifles = new HashSet 111 | { 112 | CsItem.M4A1S, 113 | CsItem.M4A4, 114 | CsItem.Famas, 115 | CsItem.AUG, 116 | }; 117 | 118 | private static readonly ICollection _sharedPreferred = new HashSet 119 | { 120 | CsItem.AWP, 121 | }; 122 | 123 | private static readonly ICollection _tPreferred = new HashSet 124 | { 125 | CsItem.AutoSniperT, 126 | }; 127 | 128 | private static readonly ICollection _ctPreferred = new HashSet 129 | { 130 | CsItem.AutoSniperCT, 131 | }; 132 | 133 | private static readonly ICollection _preferredForT = _sharedPreferred.Concat(_tPreferred).ToHashSet(); 134 | private static readonly ICollection _preferredForCt = _sharedPreferred.Concat(_ctPreferred).ToHashSet(); 135 | 136 | private static readonly ICollection _allPreferred = 137 | _preferredForT.Concat(_preferredForCt).ToHashSet(); 138 | 139 | private static readonly ICollection _heavys = new HashSet 140 | { 141 | CsItem.M249, 142 | CsItem.Negev, 143 | }; 144 | 145 | private static readonly ICollection _fullBuyPrimaryForT = 146 | _tRifles.Concat(_heavys).ToHashSet(); 147 | 148 | private static readonly ICollection _fullBuyPrimaryForCt = 149 | _ctRifles.Concat(_heavys).ToHashSet(); 150 | 151 | private static readonly ICollection _allWeapons = Enum.GetValues() 152 | .Where(item => (int)item >= 200 && (int)item < 500) 153 | .ToHashSet(); 154 | 155 | private static readonly ICollection _allFullBuy = 156 | _allPreferred.Concat(_heavys).Concat(_tRifles).Concat(_ctRifles).ToHashSet(); 157 | 158 | private static readonly ICollection _allHalfBuy = 159 | _midRangeForT.Concat(_midRangeForCt).ToHashSet(); 160 | 161 | private static readonly ICollection _allPistols = 162 | _pistolsForT.Concat(_pistolsForCt).ToHashSet(); 163 | 164 | private static readonly ICollection _allPrimary = _allPreferred 165 | .Concat(_allFullBuy) 166 | .Concat(_allHalfBuy) 167 | .ToHashSet(); 168 | 169 | private static readonly ICollection _allSecondary = _allPistols.ToHashSet(); 170 | 171 | private static readonly ICollection _allUtil = new HashSet 172 | { 173 | CsItem.Flashbang, 174 | CsItem.HE, 175 | CsItem.Molotov, 176 | CsItem.Incendiary, 177 | CsItem.Smoke, 178 | CsItem.Decoy, 179 | }; 180 | 181 | private static readonly Dictionary> 182 | _validAllocationTypesForRound = new() 183 | { 184 | {RoundType.Pistol, new HashSet {WeaponAllocationType.PistolRound}}, 185 | { 186 | RoundType.HalfBuy, 187 | new HashSet {WeaponAllocationType.Secondary, WeaponAllocationType.HalfBuyPrimary} 188 | }, 189 | { 190 | RoundType.FullBuy, 191 | new HashSet 192 | { 193 | WeaponAllocationType.Secondary, WeaponAllocationType.FullBuyPrimary, WeaponAllocationType.Preferred 194 | } 195 | }, 196 | }; 197 | 198 | private static readonly Dictionary< 199 | CsTeam, 200 | Dictionary> 201 | > _validWeaponsByTeamAndAllocationType = new() 202 | { 203 | { 204 | CsTeam.Terrorist, new() 205 | { 206 | {WeaponAllocationType.PistolRound, _pistolsForT}, 207 | {WeaponAllocationType.Secondary, _pistolsForT}, 208 | {WeaponAllocationType.HalfBuyPrimary, _midRangeForT}, 209 | {WeaponAllocationType.FullBuyPrimary, _fullBuyPrimaryForT}, 210 | {WeaponAllocationType.Preferred, _preferredForT}, 211 | } 212 | }, 213 | { 214 | CsTeam.CounterTerrorist, new() 215 | { 216 | {WeaponAllocationType.PistolRound, _pistolsForCt}, 217 | {WeaponAllocationType.Secondary, _pistolsForCt}, 218 | {WeaponAllocationType.HalfBuyPrimary, _midRangeForCt}, 219 | {WeaponAllocationType.FullBuyPrimary, _fullBuyPrimaryForCt}, 220 | {WeaponAllocationType.Preferred, _preferredForCt}, 221 | } 222 | } 223 | }; 224 | 225 | private static readonly Dictionary< 226 | CsTeam, 227 | Dictionary 228 | > _defaultWeaponsByTeamAndAllocationType = new() 229 | { 230 | { 231 | CsTeam.Terrorist, new() 232 | { 233 | {WeaponAllocationType.FullBuyPrimary, CsItem.AK47}, 234 | {WeaponAllocationType.HalfBuyPrimary, CsItem.Mac10}, 235 | {WeaponAllocationType.Secondary, CsItem.Deagle}, 236 | {WeaponAllocationType.PistolRound, CsItem.Glock}, 237 | } 238 | }, 239 | { 240 | CsTeam.CounterTerrorist, new() 241 | { 242 | {WeaponAllocationType.FullBuyPrimary, CsItem.M4A1S}, 243 | {WeaponAllocationType.HalfBuyPrimary, CsItem.MP9}, 244 | {WeaponAllocationType.Secondary, CsItem.Deagle}, 245 | {WeaponAllocationType.PistolRound, CsItem.USPS}, 246 | } 247 | } 248 | }; 249 | 250 | private static readonly Dictionary _weaponNameSearchOverrides = new() 251 | { 252 | {"m4a1", CsItem.M4A1S}, 253 | {"m4a1-s", CsItem.M4A1S}, 254 | }; 255 | 256 | private static readonly Dictionary _weaponNameOverrides = new() 257 | { 258 | {CsItem.M4A4, "M4A4"}, 259 | }; 260 | 261 | public static List WeaponAllocationTypes => 262 | Enum.GetValues().ToList(); 263 | 264 | public static Dictionary< 265 | CsTeam, 266 | Dictionary 267 | > DefaultWeaponsByTeamAndAllocationType => new(_defaultWeaponsByTeamAndAllocationType); 268 | 269 | public static List AllWeapons => _allWeapons.ToList(); 270 | 271 | public static bool IsWeapon(CsItem item) => _allWeapons.Contains(item); 272 | 273 | public static string GetName(this CsItem item) => 274 | _weaponNameOverrides.TryGetValue(item, out var overrideName) 275 | ? overrideName 276 | : item.ToString(); 277 | 278 | public static ItemSlotType? GetSlotTypeForItem(CsItem? item) 279 | { 280 | if (item is null) 281 | { 282 | return null; 283 | } 284 | 285 | if (_allSecondary.Contains(item.Value)) 286 | { 287 | return ItemSlotType.Secondary; 288 | } 289 | 290 | if (_allPrimary.Contains(item.Value)) 291 | { 292 | return ItemSlotType.Primary; 293 | } 294 | 295 | if (_allUtil.Contains(item.Value)) 296 | { 297 | return ItemSlotType.Util; 298 | } 299 | 300 | return null; 301 | } 302 | 303 | public static string GetSlotNameForSlotType(ItemSlotType? slotType) 304 | { 305 | return slotType switch 306 | { 307 | ItemSlotType.Primary => "slot1", 308 | ItemSlotType.Secondary => "slot2", 309 | ItemSlotType.Util => "slot4", 310 | _ => throw new ArgumentOutOfRangeException() 311 | }; 312 | } 313 | 314 | public static ICollection GetPossibleWeaponsForAllocationType(WeaponAllocationType allocationType, 315 | CsTeam team) 316 | { 317 | if (team != CsTeam.Terrorist && team != CsTeam.CounterTerrorist) 318 | { 319 | return new List(); 320 | } 321 | return _validWeaponsByTeamAndAllocationType[team][allocationType].Where(IsUsableWeapon).ToList(); 322 | } 323 | 324 | public static bool IsAllocationTypeValidForRound(WeaponAllocationType? allocationType, RoundType? roundType) 325 | { 326 | if (allocationType is null || roundType is null) 327 | { 328 | return false; 329 | } 330 | 331 | return _validAllocationTypesForRound[roundType.Value].Contains(allocationType.Value); 332 | } 333 | 334 | public static bool IsPreferred(CsTeam team, CsItem weapon) 335 | { 336 | return team switch 337 | { 338 | CsTeam.Terrorist => _preferredForT.Contains(weapon), 339 | CsTeam.CounterTerrorist => _preferredForCt.Contains(weapon), 340 | _ => false, 341 | }; 342 | } 343 | 344 | public static IList SelectPreferredPlayers(IEnumerable players, Func isVip, CsTeam team) 345 | { 346 | if (Configs.GetConfigData().AllowPreferredWeaponForEveryone) 347 | { 348 | return new List(players); 349 | } 350 | 351 | var playersList = players.ToList(); 352 | 353 | if (Configs.GetConfigData().MinPlayersPerTeamForPreferredWeapon.TryGetValue(team, out var minTeamPlayers)) 354 | { 355 | if (playersList.Count < minTeamPlayers) 356 | { 357 | return new List(); 358 | } 359 | } 360 | 361 | if (!Configs.GetConfigData().MaxPreferredWeaponsPerTeam.TryGetValue(team, out var maxPerTeam)) 362 | { 363 | maxPerTeam = 1; 364 | } 365 | 366 | if (maxPerTeam == 0) 367 | { 368 | return new List(); 369 | } 370 | 371 | var choicePlayers = new List(); 372 | foreach (var p in playersList) 373 | { 374 | if (Configs.GetConfigData().NumberOfExtraVipChancesForPreferredWeapon == -1) 375 | { 376 | if (isVip(p)) 377 | { 378 | choicePlayers.Add(p); 379 | } 380 | } 381 | else 382 | { 383 | choicePlayers.Add(p); 384 | if (isVip(p)) 385 | { 386 | for (var i = 0; i < Configs.GetConfigData().NumberOfExtraVipChancesForPreferredWeapon; i++) 387 | { 388 | choicePlayers.Add(p); 389 | } 390 | } 391 | } 392 | } 393 | 394 | Utils.Shuffle(choicePlayers); 395 | return new HashSet(choicePlayers).Take(maxPerTeam).ToList(); 396 | } 397 | 398 | public static bool IsUsableWeapon(CsItem weapon) 399 | { 400 | return Configs.GetConfigData().UsableWeapons.Contains(weapon); 401 | } 402 | 403 | public static CsItem? CoercePreferredTeam(CsItem? item, CsTeam team) 404 | { 405 | if (item == null || !_allPreferred.Contains(item.Value)) 406 | { 407 | return null; 408 | } 409 | 410 | if (team != CsTeam.Terrorist && team != CsTeam.CounterTerrorist) 411 | { 412 | return null; 413 | } 414 | 415 | if (item == CsItem.AWP) 416 | { 417 | return item; 418 | } 419 | 420 | // Right now these are the only other preferred guns 421 | // If we make preferred guns configurable, we'll have to change this 422 | return team == CsTeam.Terrorist ? CsItem.AutoSniperT : CsItem.AutoSniperCT; 423 | } 424 | 425 | public static ICollection GetRoundTypesForWeapon(CsItem weapon) 426 | { 427 | if (_allPistols.Contains(weapon)) 428 | { 429 | return new HashSet {RoundType.Pistol, RoundType.HalfBuy, RoundType.FullBuy}; 430 | } 431 | 432 | if (_allHalfBuy.Contains(weapon)) 433 | { 434 | return new HashSet {RoundType.HalfBuy}; 435 | } 436 | 437 | if (_allFullBuy.Contains(weapon)) 438 | { 439 | return new HashSet {RoundType.FullBuy}; 440 | } 441 | 442 | return new HashSet(); 443 | } 444 | 445 | public static ICollection FindValidWeaponsByName(string needle) 446 | { 447 | return FindItemsByName(needle) 448 | .Where(item => _allWeapons.Contains(item)) 449 | .ToList(); 450 | } 451 | 452 | public static WeaponAllocationType? GetWeaponAllocationTypeForWeaponAndRound(RoundType? roundType, CsTeam team, 453 | CsItem weapon) 454 | { 455 | if (team != CsTeam.Terrorist && team != CsTeam.CounterTerrorist) 456 | { 457 | return null; 458 | } 459 | 460 | // First populate all allocation types that could match 461 | // For a pistol this could be multiple allocation types, for any other weapon type only one can match 462 | var potentialAllocationTypes = new HashSet(); 463 | foreach (var (allocationType, items) in _validWeaponsByTeamAndAllocationType[team]) 464 | { 465 | if (items.Contains(weapon)) 466 | { 467 | potentialAllocationTypes.Add(allocationType); 468 | } 469 | } 470 | 471 | // If theres only 1 to choose from, return that, or return null if there are none 472 | if (potentialAllocationTypes.Count == 1) 473 | { 474 | return potentialAllocationTypes.First(); 475 | } 476 | 477 | if (potentialAllocationTypes.Count == 0) 478 | { 479 | return null; 480 | } 481 | 482 | // For a pistol, the set will be {PistolRound, Secondary} 483 | // We need to find which of those matches the current round type 484 | foreach (var allocationType in potentialAllocationTypes) 485 | { 486 | if (roundType is null || IsAllocationTypeValidForRound(allocationType, roundType)) 487 | { 488 | return allocationType; 489 | } 490 | } 491 | 492 | return null; 493 | } 494 | 495 | /** 496 | * This function should only be used when you have an item that you want to find out what *replacement* 497 | * allocation type it belongs to. Eg. if you have a Preferred, it should be replaced with a PrimaryFullBuy 498 | */ 499 | public static WeaponAllocationType? GetReplacementWeaponAllocationTypeForWeapon(RoundType? roundType) 500 | { 501 | return roundType switch 502 | { 503 | RoundType.Pistol => WeaponAllocationType.PistolRound, 504 | RoundType.HalfBuy => WeaponAllocationType.HalfBuyPrimary, 505 | RoundType.FullBuy => WeaponAllocationType.FullBuyPrimary, 506 | _ => null, 507 | }; 508 | } 509 | 510 | public static ICollection GetWeaponsForRoundType( 511 | RoundType roundType, 512 | CsTeam team, 513 | UserSetting? userSetting, 514 | bool givePreferred 515 | ) 516 | { 517 | WeaponAllocationType? primaryWeaponAllocation = 518 | givePreferred 519 | ? WeaponAllocationType.Preferred 520 | : roundType switch 521 | { 522 | RoundType.HalfBuy => WeaponAllocationType.HalfBuyPrimary, 523 | RoundType.FullBuy => WeaponAllocationType.FullBuyPrimary, 524 | _ => null, 525 | }; 526 | 527 | var secondaryWeaponAllocation = roundType switch 528 | { 529 | RoundType.Pistol => WeaponAllocationType.PistolRound, 530 | _ => WeaponAllocationType.Secondary, 531 | }; 532 | 533 | var weapons = new List(); 534 | var secondary = GetWeaponForAllocationType(secondaryWeaponAllocation, team, userSetting); 535 | if (secondary is not null) 536 | { 537 | weapons.Add(secondary.Value); 538 | } 539 | 540 | if (primaryWeaponAllocation is null) 541 | { 542 | return weapons; 543 | } 544 | 545 | var primary = GetWeaponForAllocationType(primaryWeaponAllocation.Value, team, userSetting); 546 | if (primary is not null) 547 | { 548 | weapons.Add(primary.Value); 549 | } 550 | 551 | return weapons; 552 | } 553 | 554 | private static ICollection FindItemsByName(string needle) 555 | { 556 | needle = needle.ToLower(); 557 | if (_weaponNameSearchOverrides.TryGetValue(needle, out var nameOverride)) 558 | { 559 | return new List {nameOverride}; 560 | } 561 | 562 | return Enum.GetNames() 563 | .Where(name => name.ToLower().Contains(needle)) 564 | .Select(Enum.Parse) 565 | .ToList(); 566 | } 567 | 568 | private static CsItem? GetDefaultWeaponForAllocationType(WeaponAllocationType allocationType, CsTeam team) 569 | { 570 | if (team is CsTeam.None or CsTeam.Spectator) 571 | { 572 | return null; 573 | } 574 | 575 | if (allocationType == WeaponAllocationType.Preferred) 576 | { 577 | return null; 578 | } 579 | 580 | CsItem? defaultWeapon = null; 581 | 582 | var configDefaultWeapons = Configs.GetConfigData().DefaultWeapons; 583 | if (configDefaultWeapons.TryGetValue(team, out var teamDefaults)) 584 | { 585 | if (teamDefaults.TryGetValue(allocationType, out var configuredDefaultWeapon)) 586 | { 587 | defaultWeapon = configuredDefaultWeapon; 588 | } 589 | } 590 | 591 | defaultWeapon ??= _defaultWeaponsByTeamAndAllocationType[team][allocationType]; 592 | 593 | return IsUsableWeapon(defaultWeapon.Value) ? defaultWeapon : null; 594 | } 595 | 596 | private static CsItem GetRandomWeaponForAllocationType(WeaponAllocationType allocationType, CsTeam team) 597 | { 598 | if (team != CsTeam.Terrorist && team != CsTeam.CounterTerrorist) 599 | { 600 | return CsItem.Deagle; 601 | } 602 | 603 | var collectionToCheck = allocationType switch 604 | { 605 | WeaponAllocationType.PistolRound => team == CsTeam.Terrorist ? _pistolsForT : _pistolsForCt, 606 | WeaponAllocationType.Secondary => team == CsTeam.Terrorist ? _pistolsForT : _pistolsForCt, 607 | WeaponAllocationType.HalfBuyPrimary => team == CsTeam.Terrorist ? _smgsForT : _smgsForCt, 608 | WeaponAllocationType.FullBuyPrimary => team == CsTeam.Terrorist ? _tRifles : _ctRifles, 609 | WeaponAllocationType.Preferred => team == CsTeam.Terrorist ? _preferredForT : _preferredForCt, 610 | _ => _sharedPistols, 611 | }; 612 | return Utils.Choice(collectionToCheck.Where(IsUsableWeapon).ToList()); 613 | } 614 | 615 | private static CsItem? GetWeaponForAllocationType(WeaponAllocationType allocationType, CsTeam team, 616 | UserSetting? userSetting) 617 | { 618 | CsItem? weapon = null; 619 | 620 | if (Configs.GetConfigData().CanPlayersSelectWeapons() && userSetting is not null) 621 | { 622 | var weaponPreference = userSetting.GetWeaponPreference(team, allocationType); 623 | if (weaponPreference is not null && IsUsableWeapon(weaponPreference.Value)) 624 | { 625 | weapon = weaponPreference; 626 | } 627 | } 628 | 629 | if (weapon is null && Configs.GetConfigData().CanAssignRandomWeapons()) 630 | { 631 | weapon = GetRandomWeaponForAllocationType(allocationType, team); 632 | } 633 | 634 | if (weapon is null && Configs.GetConfigData().CanAssignDefaultWeapons()) 635 | { 636 | weapon = GetDefaultWeaponForAllocationType(allocationType, team); 637 | } 638 | 639 | return weapon; 640 | } 641 | 642 | public static bool IsWeaponAllocationAllowed(bool isFreezePeriod) 643 | { 644 | return Configs.GetConfigData().AllowAllocationAfterFreezeTime || isFreezePeriod; 645 | } 646 | } 647 | -------------------------------------------------------------------------------- /RetakesAllocatorTest/ConfigTests.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Modules.Entities.Constants; 2 | using CounterStrikeSharp.API.Modules.Utils; 3 | using RetakesAllocatorCore; 4 | using RetakesAllocatorCore.Config; 5 | 6 | namespace RetakesAllocatorTest; 7 | 8 | public class ConfigTests : BaseTestFixture 9 | { 10 | [Test] 11 | public void TestDefaultWeaponsValidation() 12 | { 13 | var usableWeapons = WeaponHelpers.AllWeapons; 14 | usableWeapons.Remove(CsItem.Glock); 15 | var warnings = Configs.OverrideConfigDataForTests( 16 | new ConfigData() 17 | { 18 | UsableWeapons = usableWeapons, 19 | } 20 | ).Validate(); 21 | Assert.That(warnings[0], 22 | Is.EqualTo( 23 | "Glock18 in the DefaultWeapons.Terrorist.PistolRound " + 24 | "config is not in the UsableWeapons list.")); 25 | 26 | var defaults = 27 | new Dictionary>(Configs.GetConfigData().DefaultWeapons); 28 | defaults[CsTeam.Terrorist] = new Dictionary(defaults[CsTeam.Terrorist]); 29 | defaults[CsTeam.Terrorist].Remove(WeaponAllocationType.FullBuyPrimary); 30 | warnings = Configs.OverrideConfigDataForTests( 31 | new ConfigData() 32 | { 33 | DefaultWeapons = defaults 34 | } 35 | ).Validate(); 36 | Assert.That(warnings[0], Is.EqualTo("Missing FullBuyPrimary in DefaultWeapons.Terrorist config.")); 37 | 38 | defaults.Remove(CsTeam.CounterTerrorist); 39 | warnings = Configs.OverrideConfigDataForTests( 40 | new ConfigData() 41 | { 42 | DefaultWeapons = defaults 43 | } 44 | ).Validate(); 45 | Assert.That(warnings[0], Is.EqualTo("Missing FullBuyPrimary in DefaultWeapons.Terrorist config.")); 46 | Assert.That(warnings[1], Is.EqualTo("Missing CounterTerrorist in DefaultWeapons config.")); 47 | 48 | defaults[CsTeam.Terrorist][WeaponAllocationType.FullBuyPrimary] = CsItem.Kevlar; 49 | var error = Assert.Catch(() => 50 | { 51 | Configs.OverrideConfigDataForTests( 52 | new ConfigData() 53 | { 54 | DefaultWeapons = defaults 55 | } 56 | ); 57 | }); 58 | Assert.That(error?.Message, 59 | Is.EqualTo("Kevlar is not a valid weapon in config DefaultWeapons.Terrorist.FullBuyPrimary.")); 60 | 61 | defaults = 62 | new Dictionary>(Configs.GetConfigData().DefaultWeapons); 63 | defaults[CsTeam.Terrorist][WeaponAllocationType.Preferred] = CsItem.AWP; 64 | error = Assert.Catch(() => 65 | { 66 | Configs.OverrideConfigDataForTests( 67 | new ConfigData() 68 | { 69 | DefaultWeapons = defaults 70 | } 71 | ); 72 | }); 73 | Assert.That(error?.Message, Is.EqualTo( 74 | "Preferred is not a valid default weapon allocation type for config DefaultWeapons.Terrorist." 75 | )); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /RetakesAllocatorTest/DbTests.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Modules.Entities.Constants; 2 | using CounterStrikeSharp.API.Modules.Utils; 3 | using RetakesAllocatorCore; 4 | using RetakesAllocatorCore.Db; 5 | using static RetakesAllocatorTest.TestConstants; 6 | 7 | namespace RetakesAllocatorTest; 8 | 9 | public class DbTests : BaseTestFixture 10 | { 11 | [Test] 12 | public async Task TestGetUsersSettings() 13 | { 14 | var usersSettings = Queries.GetUsersSettings(new List()); 15 | Assert.That(usersSettings, Is.EqualTo(new Dictionary())); 16 | 17 | await Queries.SetWeaponPreferenceForUserAsync(TestSteamId, CsTeam.Terrorist, WeaponAllocationType.HalfBuyPrimary, 18 | CsItem.Bizon); 19 | await Queries.SetWeaponPreferenceForUserAsync(TestSteamId, CsTeam.Terrorist, WeaponAllocationType.PistolRound, null); 20 | await Queries.SetWeaponPreferenceForUserAsync(TestSteamId, CsTeam.Terrorist, WeaponAllocationType.HalfBuyPrimary, 21 | CsItem.MP5); 22 | await Queries.SetWeaponPreferenceForUserAsync(TestSteamId, CsTeam.CounterTerrorist, WeaponAllocationType.FullBuyPrimary, 23 | CsItem.AK47); 24 | // Should set for both T and CT 25 | await Queries.SetPreferredWeaponPreferenceAsync(TestSteamId, CsItem.AWP); 26 | 27 | await Queries.SetWeaponPreferenceForUserAsync(2, CsTeam.Terrorist, WeaponAllocationType.FullBuyPrimary, CsItem.AK47); 28 | await Queries.SetWeaponPreferenceForUserAsync(2, CsTeam.Terrorist, WeaponAllocationType.Secondary, CsItem.Deagle); 29 | await Queries.SetWeaponPreferenceForUserAsync(2, CsTeam.CounterTerrorist, WeaponAllocationType.Secondary, 30 | CsItem.FiveSeven); 31 | // Will get different snipers for different teams 32 | await Queries.SetPreferredWeaponPreferenceAsync(2, CsItem.SCAR20); 33 | 34 | usersSettings = Queries.GetUsersSettings(new List {TestSteamId}); 35 | Assert.Multiple(() => 36 | { 37 | Assert.That(usersSettings.Keys, Is.EquivalentTo(new List {TestSteamId})); 38 | Assert.That(usersSettings.Values.Select(v => v.UserId), Is.EquivalentTo(new List {TestSteamId})); 39 | }); 40 | usersSettings = Queries.GetUsersSettings(new List {2}); 41 | Assert.Multiple(() => 42 | { 43 | Assert.That(usersSettings.Keys, Is.EquivalentTo(new List {2})); 44 | Assert.That(usersSettings.Values.Select(v => v.UserId), Is.EquivalentTo(new List {2})); 45 | }); 46 | usersSettings = Queries.GetUsersSettings(new List {TestSteamId, 2}); 47 | Assert.Multiple(() => 48 | { 49 | Assert.That(usersSettings.Keys, Is.EquivalentTo(new List {TestSteamId, 2})); 50 | Assert.That(usersSettings.Values.Select(v => v.UserId), 51 | Is.EquivalentTo(new List {TestSteamId, 2})); 52 | 53 | Assert.That( 54 | usersSettings[TestSteamId].GetWeaponPreference(CsTeam.Terrorist, WeaponAllocationType.HalfBuyPrimary), 55 | Is.EqualTo(CsItem.MP5)); 56 | Assert.That( 57 | usersSettings[TestSteamId].GetWeaponPreference(CsTeam.Terrorist, WeaponAllocationType.PistolRound), 58 | Is.EqualTo(null)); 59 | Assert.That( 60 | usersSettings[TestSteamId] 61 | .GetWeaponPreference(CsTeam.CounterTerrorist, WeaponAllocationType.FullBuyPrimary), 62 | Is.EqualTo(CsItem.AK47)); 63 | Assert.That( 64 | usersSettings[TestSteamId] 65 | .GetWeaponPreference(CsTeam.CounterTerrorist, WeaponAllocationType.HalfBuyPrimary), 66 | Is.EqualTo(null)); 67 | Assert.That( 68 | usersSettings[TestSteamId] 69 | .GetWeaponPreference(CsTeam.CounterTerrorist, WeaponAllocationType.Preferred), 70 | Is.EqualTo(CsItem.AWP)); 71 | Assert.That( 72 | usersSettings[TestSteamId].GetWeaponPreference(CsTeam.Terrorist, WeaponAllocationType.Preferred), 73 | Is.EqualTo(CsItem.AWP)); 74 | 75 | Assert.That(usersSettings[2].GetWeaponPreference(CsTeam.Terrorist, WeaponAllocationType.FullBuyPrimary), 76 | Is.EqualTo(CsItem.AK47)); 77 | Assert.That(usersSettings[2].GetWeaponPreference(CsTeam.Terrorist, WeaponAllocationType.Secondary), 78 | Is.EqualTo(CsItem.Deagle)); 79 | Assert.That(usersSettings[2].GetWeaponPreference(CsTeam.CounterTerrorist, WeaponAllocationType.Secondary), 80 | Is.EqualTo(CsItem.FiveSeven)); 81 | Assert.That(usersSettings[2].GetWeaponPreference(CsTeam.Terrorist, WeaponAllocationType.Preferred), 82 | Is.EqualTo(CsItem.AutoSniperT)); 83 | Assert.That(usersSettings[2].GetWeaponPreference(CsTeam.CounterTerrorist, WeaponAllocationType.Preferred), 84 | Is.EqualTo(CsItem.AutoSniperCT)); 85 | }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /RetakesAllocatorTest/GlobalSetup.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Core.Translations; 2 | using RetakesAllocatorCore; 3 | using RetakesAllocatorCore.Config; 4 | using RetakesAllocatorCore.Db; 5 | 6 | namespace RetakesAllocatorTest; 7 | 8 | [SetUpFixture] 9 | public class GlobalSetup 10 | { 11 | [OneTimeSetUp] 12 | public void Setup() 13 | { 14 | Configs.Load(".", true); 15 | Queries.Migrate(); 16 | Translator.Initialize(new JsonStringLocalizer("../../../../RetakesAllocator/lang")); 17 | } 18 | 19 | [OneTimeTearDown] 20 | public void TearDown() 21 | { 22 | Queries.Disconnect(); 23 | } 24 | } 25 | 26 | public abstract class BaseTestFixture 27 | { 28 | [SetUp] 29 | public void GlobalSetup() 30 | { 31 | Configs.Load("."); 32 | Queries.Wipe(); 33 | } 34 | } -------------------------------------------------------------------------------- /RetakesAllocatorTest/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using NUnit.Framework; 2 | -------------------------------------------------------------------------------- /RetakesAllocatorTest/NadeAllocationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using CounterStrikeSharp.API.Modules.Entities.Constants; 3 | using CounterStrikeSharp.API.Modules.Utils; 4 | using RetakesAllocatorCore; 5 | 6 | namespace RetakesAllocatorTest; 7 | 8 | public class NadeAllocationTests : BaseTestFixture 9 | { 10 | [Test] 11 | public void TestGetUtilForTeam() 12 | { 13 | var util = NadeHelpers.GetUtilForTeam("de_mirage", RoundType.Pistol, CsTeam.Terrorist, 4); 14 | Assert.That(util.Count, Is.EqualTo(4)); 15 | 16 | util = NadeHelpers.GetUtilForTeam(null, RoundType.Pistol, CsTeam.CounterTerrorist, 0); 17 | Assert.That(util.Count, Is.EqualTo(0)); 18 | } 19 | 20 | [Test] 21 | public void TestAllocateNadesToPlayers() 22 | { 23 | var util = NadeHelpers.GetUtilForTeam(null, RoundType.Pistol, CsTeam.Terrorist, 4); 24 | Dictionary> nadesByPlayer = new(); 25 | NadeHelpers.AllocateNadesToPlayers(new Stack(util), new List {1, 2, 3, 4}, nadesByPlayer); 26 | Assert.That(util, Is.EquivalentTo(nadesByPlayer.Values.SelectMany(x => x))); 27 | 28 | util = NadeHelpers.GetUtilForTeam("de_dust2", RoundType.Pistol, CsTeam.CounterTerrorist, 0); 29 | nadesByPlayer = new(); 30 | NadeHelpers.AllocateNadesToPlayers(new Stack(util), new List(), nadesByPlayer); 31 | Assert.That(util, Is.EquivalentTo(nadesByPlayer.Values.SelectMany(x => x))); 32 | } 33 | } -------------------------------------------------------------------------------- /RetakesAllocatorTest/RetakesAllocatorTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /RetakesAllocatorTest/RoundStartTests.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Modules.Utils; 2 | using RetakesAllocatorCore; 3 | 4 | namespace RetakesAllocatorTest; 5 | 6 | public class RoundStartTests : BaseTestFixture 7 | { 8 | [Test] 9 | public void TestRoundStartCanRunInCore() 10 | { 11 | OnRoundPostStartHelper.Handle( 12 | new List(), 13 | i => 1, 14 | x => CsTeam.None, 15 | x => {}, 16 | (x, y, z) => {}, 17 | x => false, 18 | out _ 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /RetakesAllocatorTest/RoundTypeTests.cs: -------------------------------------------------------------------------------- 1 | using RetakesAllocatorCore; 2 | using RetakesAllocatorCore.Config; 3 | 4 | namespace RetakesAllocatorTest; 5 | 6 | public class RoundTypeTests : BaseTestFixture 7 | { 8 | [Test] 9 | [TestCase(10, .1f)] 10 | [TestCase(100, 1f)] 11 | [TestCase(33, .33f)] 12 | public void TestRoundPercentages(int configPercentage, double expectedPercentage) 13 | { 14 | Configs.OverrideConfigDataForTests( 15 | new ConfigData 16 | { 17 | RoundTypePercentages = new() 18 | { 19 | {RoundType.Pistol, configPercentage}, 20 | {RoundType.HalfBuy, 0}, 21 | {RoundType.FullBuy, 100 - configPercentage} 22 | } 23 | } 24 | ); 25 | 26 | expectedPercentage = Math.Round(expectedPercentage, 2); 27 | 28 | Assert.That(Configs.GetConfigData().GetRoundTypePercentage(RoundType.Pistol), Is.EqualTo(expectedPercentage)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /RetakesAllocatorTest/TestConstants.cs: -------------------------------------------------------------------------------- 1 | namespace RetakesAllocatorTest; 2 | 3 | public static class TestConstants 4 | { 5 | public const long TestSteamId = 74561198018763982; 6 | } 7 | -------------------------------------------------------------------------------- /RetakesAllocatorTest/WeaponHelpersTests.cs: -------------------------------------------------------------------------------- 1 | using RetakesAllocatorCore; 2 | using RetakesAllocatorCore.Config; 3 | 4 | namespace RetakesAllocatorTest; 5 | 6 | public class WeaponHelpersTests : BaseTestFixture 7 | { 8 | [Test] 9 | [TestCase(true, true, true)] 10 | [TestCase(true, false, true)] 11 | [TestCase(false, true, true)] 12 | [TestCase(false, false, false)] 13 | public void TestIsWeaponAllocationAllowed(bool allowAfterFreezeTime, bool isFreezeTime, bool expected) 14 | { 15 | Configs.OverrideConfigDataForTests(new ConfigData() {AllowAllocationAfterFreezeTime = allowAfterFreezeTime}); 16 | 17 | var canAllocate = WeaponHelpers.IsWeaponAllocationAllowed(isFreezeTime); 18 | 19 | Assert.That(canAllocate, Is.EqualTo(expected)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /RetakesAllocatorTest/WeaponSelectionTests.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Modules.Entities.Constants; 2 | using CounterStrikeSharp.API.Modules.Utils; 3 | using RetakesAllocatorCore; 4 | using RetakesAllocatorCore.Config; 5 | using RetakesAllocatorCore.Db; 6 | using RetakesAllocatorCore.Managers; 7 | using static RetakesAllocatorTest.TestConstants; 8 | 9 | namespace RetakesAllocatorTest; 10 | 11 | public class WeaponSelectionTests : BaseTestFixture 12 | { 13 | [Test] 14 | public async Task SetWeaponPreferenceDirectly() 15 | { 16 | Assert.That( 17 | (await Queries.GetUserSettings(TestSteamId)) 18 | ?.GetWeaponPreference(CsTeam.Terrorist, WeaponAllocationType.FullBuyPrimary), 19 | Is.EqualTo(null)); 20 | 21 | await Queries.SetWeaponPreferenceForUserAsync(TestSteamId, CsTeam.Terrorist, WeaponAllocationType.FullBuyPrimary, 22 | CsItem.Galil); 23 | Assert.That( 24 | (await Queries.GetUserSettings(TestSteamId)) 25 | ?.GetWeaponPreference(CsTeam.Terrorist, WeaponAllocationType.FullBuyPrimary), 26 | Is.EqualTo(CsItem.Galil)); 27 | 28 | await Queries.SetWeaponPreferenceForUserAsync(TestSteamId, CsTeam.Terrorist, WeaponAllocationType.FullBuyPrimary, 29 | CsItem.AWP); 30 | Assert.That( 31 | (await Queries.GetUserSettings(TestSteamId)) 32 | ?.GetWeaponPreference(CsTeam.Terrorist, WeaponAllocationType.FullBuyPrimary), 33 | Is.EqualTo(CsItem.AWP)); 34 | 35 | await Queries.SetWeaponPreferenceForUserAsync(TestSteamId, CsTeam.Terrorist, WeaponAllocationType.PistolRound, 36 | CsItem.Deagle); 37 | Assert.That( 38 | (await Queries.GetUserSettings(TestSteamId)) 39 | ?.GetWeaponPreference(CsTeam.Terrorist, WeaponAllocationType.PistolRound), 40 | Is.EqualTo(CsItem.Deagle)); 41 | 42 | Assert.That( 43 | (await Queries.GetUserSettings(TestSteamId)) 44 | ?.GetWeaponPreference(CsTeam.CounterTerrorist, WeaponAllocationType.HalfBuyPrimary), 45 | Is.EqualTo(null)); 46 | await Queries.SetWeaponPreferenceForUserAsync(TestSteamId, CsTeam.CounterTerrorist, WeaponAllocationType.HalfBuyPrimary, 47 | CsItem.MP9); 48 | Assert.That( 49 | (await Queries.GetUserSettings(TestSteamId)) 50 | ?.GetWeaponPreference(CsTeam.CounterTerrorist, WeaponAllocationType.HalfBuyPrimary), 51 | Is.EqualTo(CsItem.MP9)); 52 | } 53 | 54 | [Test] 55 | [TestCase(RoundType.FullBuy, CsTeam.Terrorist, "galil", CsItem.Galil, "Galil' is now", "Galil' is no longer")] 56 | [TestCase(RoundType.HalfBuy, CsTeam.Terrorist, "galil", null, "Galil' is now;;;at the next FullBuy", 57 | "Galil' is no longer")] 58 | [TestCase(RoundType.FullBuy, CsTeam.Terrorist, "krieg", CsItem.Krieg, "SG553' is now", "SG553' is no longer")] 59 | [TestCase(RoundType.HalfBuy, CsTeam.Terrorist, "mac10", CsItem.Mac10, "Mac10' is now", "Mac10' is no longer")] 60 | [TestCase(RoundType.FullBuy, CsTeam.Terrorist, "mac10", null, "Mac10' is now;;;at the next HalfBuy", 61 | "Mac10' is no longer")] 62 | [TestCase(RoundType.Pistol, CsTeam.CounterTerrorist, "deag", CsItem.Deagle, "Deagle' is now", 63 | "Deagle' is no longer")] 64 | [TestCase(RoundType.FullBuy, CsTeam.CounterTerrorist, "deag", CsItem.Deagle, "Deagle' is now", 65 | "Deagle' is no longer")] 66 | [TestCase(RoundType.HalfBuy, CsTeam.CounterTerrorist, "deag", CsItem.Deagle, "Deagle' is now", 67 | "Deagle' is no longer")] 68 | [TestCase(RoundType.FullBuy, CsTeam.CounterTerrorist, "galil", null, "Galil' is not valid", null)] 69 | [TestCase(RoundType.Pistol, CsTeam.CounterTerrorist, "tec9", null, "Tec9' is not valid", null)] 70 | [TestCase(RoundType.FullBuy, CsTeam.Terrorist, "poop", null, "not found", null)] 71 | [TestCase(RoundType.FullBuy, CsTeam.Terrorist, "galil,T", CsItem.Galil, "Galil' is now", null)] 72 | [TestCase(RoundType.FullBuy, CsTeam.Terrorist, "krieg,T", CsItem.Krieg, "SG553' is now", null)] 73 | [TestCase(RoundType.HalfBuy, CsTeam.Terrorist, "mac10,T", CsItem.Mac10, "Mac10' is now", null)] 74 | [TestCase(RoundType.HalfBuy, CsTeam.None, "mac10,T", null, "Mac10' is now", null)] 75 | [TestCase(RoundType.Pistol, CsTeam.CounterTerrorist, "deag,CT", CsItem.Deagle, "Deagle' is now", null)] 76 | [TestCase(RoundType.FullBuy, CsTeam.CounterTerrorist, "galil,CT", null, "Galil' is not valid", null)] 77 | [TestCase(RoundType.Pistol, CsTeam.CounterTerrorist, "tec9,CT", null, "Tec9' is not valid", null)] 78 | [TestCase(RoundType.FullBuy, CsTeam.Terrorist, "poop,T", null, "not found", null)] 79 | [TestCase(null, CsTeam.Terrorist, "ak", null, "AK47' is now", "AK47' is no longer")] 80 | [TestCase(RoundType.FullBuy, CsTeam.Spectator, "ak", null, "must join a team", "must join a team")] 81 | [TestCase(RoundType.FullBuy, CsTeam.Terrorist, "ak,F", null, "Invalid team", "Invalid team")] 82 | [TestCase(RoundType.FullBuy, CsTeam.Terrorist, "awp", null, "will now get a 'AWP", "no longer receive 'AWP")] 83 | [TestCase(RoundType.Pistol, CsTeam.CounterTerrorist, "awp", null, "will now get a 'AWP", "no longer receive 'AWP")] 84 | public async Task SetWeaponPreferenceCommandSingleArg( 85 | RoundType? roundType, 86 | CsTeam team, 87 | string strArgs, 88 | CsItem? expectedItem, 89 | string message, 90 | string? removeMessage 91 | ) 92 | { 93 | var args = strArgs.Split(","); 94 | 95 | var result = await OnWeaponCommandHelper.HandleAsync(args, TestSteamId, roundType, team, false); 96 | 97 | var messages = message.Split(";;;"); 98 | foreach (var m in messages) 99 | { 100 | Assert.That(result.Item1, Does.Contain(m)); 101 | } 102 | 103 | var selectedItem = result.Item2; 104 | Assert.That(selectedItem, Is.EqualTo(expectedItem)); 105 | 106 | var allocationType = 107 | selectedItem is not null 108 | ? WeaponHelpers.GetWeaponAllocationTypeForWeaponAndRound(roundType, team, selectedItem.Value) 109 | : null; 110 | 111 | var setWeapon = allocationType is not null 112 | ? (await Queries.GetUserSettings(TestSteamId))? 113 | .GetWeaponPreference(team, allocationType.Value) 114 | : null; 115 | Assert.That(setWeapon, Is.EqualTo(expectedItem)); 116 | 117 | if (removeMessage is not null) 118 | { 119 | result = await OnWeaponCommandHelper.HandleAsync(args, TestSteamId, roundType, team, true); 120 | Assert.That(result.Item1, Does.Contain(removeMessage)); 121 | 122 | setWeapon = allocationType is not null 123 | ? (await Queries.GetUserSettings(TestSteamId))?.GetWeaponPreference(team, allocationType.Value) 124 | : null; 125 | Assert.That(setWeapon, Is.EqualTo(null)); 126 | } 127 | } 128 | 129 | [Test] 130 | [TestCase("ak", CsItem.AK47, WeaponSelectionType.PlayerChoice, CsItem.AK47, "AK47' is now")] 131 | [TestCase("ak", CsItem.Galil, WeaponSelectionType.PlayerChoice, null, "not allowed")] 132 | [TestCase("ak", CsItem.AK47, WeaponSelectionType.Default, null, "cannot choose")] 133 | public async Task SetWeaponPreferencesConfig( 134 | string itemName, 135 | CsItem? allowedItem, 136 | WeaponSelectionType weaponSelectionType, 137 | CsItem? expectedItem, 138 | string message 139 | ) 140 | { 141 | var team = CsTeam.Terrorist; 142 | Configs.GetConfigData().AllowedWeaponSelectionTypes = new List {weaponSelectionType}; 143 | Configs.GetConfigData().UsableWeapons = new List { }; 144 | if (allowedItem is not null) 145 | { 146 | Configs.GetConfigData().UsableWeapons.Add(allowedItem.Value); 147 | } 148 | 149 | var args = new List {itemName}; 150 | var result = await OnWeaponCommandHelper.HandleAsync(args, TestSteamId, RoundType.FullBuy, team, false); 151 | 152 | Assert.That(result.Item1, Does.Contain(message)); 153 | Assert.That(result.Item2, Is.EqualTo(expectedItem)); 154 | 155 | var setWeapon = (await Queries.GetUserSettings(TestSteamId)) 156 | ?.GetWeaponPreference(team, WeaponAllocationType.FullBuyPrimary); 157 | Assert.That(setWeapon, Is.EqualTo(expectedItem)); 158 | } 159 | 160 | [Test] 161 | [Retry(3)] 162 | public void RandomWeaponSelection() 163 | { 164 | Configs.OverrideConfigDataForTests(new ConfigData 165 | { 166 | RoundTypePercentages = new() 167 | { 168 | {RoundType.Pistol, 5}, 169 | {RoundType.HalfBuy, 5}, 170 | {RoundType.FullBuy, 90}, 171 | }, 172 | RoundTypeSelection = RoundTypeSelectionOption.Random, 173 | }); 174 | var numPistol = 0; 175 | var numHalfBuy = 0; 176 | var numFullBuy = 0; 177 | for (var i = 0; i < 1000; i++) 178 | { 179 | var randomRoundType = RoundTypeManager.Instance.GetNextRoundType(); 180 | switch (randomRoundType) 181 | { 182 | case RoundType.Pistol: 183 | numPistol++; 184 | break; 185 | case RoundType.HalfBuy: 186 | numHalfBuy++; 187 | break; 188 | case RoundType.FullBuy: 189 | numFullBuy++; 190 | break; 191 | } 192 | } 193 | 194 | // Ranges are very permissive to avoid flakes 195 | Assert.That(numPistol, Is.InRange(20, 80)); 196 | Assert.That(numHalfBuy, Is.InRange(20, 80)); 197 | Assert.That(numFullBuy, Is.InRange(850, 950)); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /cs2-retakes-allocator.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RetakesAllocator", "RetakesAllocator\RetakesAllocator.csproj", "{63CED353-BD36-4715-BBF6-56558D131CE1}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RetakesAllocatorTest", "RetakesAllocatorTest\RetakesAllocatorTest.csproj", "{71FB77CC-26A1-4845-9941-131DBBC52C22}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RetakesAllocatorCore", "RetakesAllocatorCore\RetakesAllocatorCore.csproj", "{076A156F-9D3E-4840-976B-BF9F35CD5048}" 8 | EndProject 9 | Global 10 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 11 | Debug|Any CPU = Debug|Any CPU 12 | Release|Any CPU = Release|Any CPU 13 | EndGlobalSection 14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 15 | {63CED353-BD36-4715-BBF6-56558D131CE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 16 | {63CED353-BD36-4715-BBF6-56558D131CE1}.Debug|Any CPU.Build.0 = Debug|Any CPU 17 | {63CED353-BD36-4715-BBF6-56558D131CE1}.Release|Any CPU.ActiveCfg = Release|Any CPU 18 | {63CED353-BD36-4715-BBF6-56558D131CE1}.Release|Any CPU.Build.0 = Release|Any CPU 19 | {71FB77CC-26A1-4845-9941-131DBBC52C22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {71FB77CC-26A1-4845-9941-131DBBC52C22}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {71FB77CC-26A1-4845-9941-131DBBC52C22}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {71FB77CC-26A1-4845-9941-131DBBC52C22}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {076A156F-9D3E-4840-976B-BF9F35CD5048}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {076A156F-9D3E-4840-976B-BF9F35CD5048}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {076A156F-9D3E-4840-976B-BF9F35CD5048}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {076A156F-9D3E-4840-976B-BF9F35CD5048}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | --------------------------------------------------------------------------------