├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.cake ├── docs ├── ClientInstallation.md ├── README.md └── ServerSelection.md └── src ├── CrowdedMod.sln ├── CrowdedMod ├── Components │ ├── AbstractPagingBehaviour.cs │ ├── MeetingHudPagingBehaviour.cs │ ├── ShapeShifterPagingBehaviour.cs │ └── VitalsPagingBehaviour.cs ├── CrowdedMod.csproj ├── CrowdedModPlugin.cs ├── Net │ ├── CustomRpcCalls.cs │ └── SetColorRpc.cs └── Patches │ ├── CreateGameOptionsPatches.cs │ ├── GenericPatches.cs │ ├── MiniGamePatches.cs │ └── PagingPatches.cs └── nuget.config /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ "push", "pull_request" ] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-20.04 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | with: 12 | submodules: true 13 | 14 | - name: Setup .NET 15 | uses: actions/setup-dotnet@v2 16 | with: 17 | dotnet-version: 8.x 18 | 19 | - name: Run the Cake script 20 | uses: cake-build/cake-action@v1 21 | with: 22 | verbosity: Diagnostic 23 | 24 | - uses: actions/upload-artifact@v4 25 | with: 26 | name: CrowdedMod.dll 27 | path: src/CrowdedMod/bin/Release/net6.0/CrowdedMod.dll 28 | 29 | - uses: softprops/action-gh-release@v1 30 | if: github.ref_type == 'tag' 31 | with: 32 | draft: true 33 | files: src/CrowdedMod/bin/Release/net6.0/CrowdedMod.dll -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # yes 2 | .idea/ 3 | 4 | ## Ignore Visual Studio temporary files, build results, and 5 | ## files generated by popular Visual Studio add-ons. 6 | ## 7 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Aa][Rr][Mm]/ 30 | [Aa][Rr][Mm]64/ 31 | bld/ 32 | [Bb]in/ 33 | [Oo]bj/ 34 | [Ll]og/ 35 | [Ll]ogs/ 36 | 37 | # Visual Studio 2015/2017 cache/options directory 38 | .vs/ 39 | # Uncomment if you have tasks that create the project's static files in wwwroot 40 | #wwwroot/ 41 | 42 | # Visual Studio 2017 auto generated files 43 | Generated\ Files/ 44 | 45 | # MSTest test Results 46 | [Tt]est[Rr]esult*/ 47 | [Bb]uild[Ll]og.* 48 | 49 | # NUnit 50 | *.VisualState.xml 51 | TestResult.xml 52 | nunit-*.xml 53 | 54 | # Build Results of an ATL Project 55 | [Dd]ebugPS/ 56 | [Rr]eleasePS/ 57 | dlldata.c 58 | 59 | # Benchmark Results 60 | BenchmarkDotNet.Artifacts/ 61 | 62 | # .NET Core 63 | project.lock.json 64 | project.fragment.lock.json 65 | artifacts/ 66 | 67 | # StyleCop 68 | StyleCopReport.xml 69 | 70 | # Files built by Visual Studio 71 | *_i.c 72 | *_p.c 73 | *_h.h 74 | *.ilk 75 | *.meta 76 | *.obj 77 | *.iobj 78 | *.pch 79 | *.pdb 80 | *.ipdb 81 | *.pgc 82 | *.pgd 83 | *.rsp 84 | *.sbr 85 | *.tlb 86 | *.tli 87 | *.tlh 88 | *.tmp 89 | *.tmp_proj 90 | *_wpftmp.csproj 91 | *.log 92 | *.vspscc 93 | *.vssscc 94 | .builds 95 | *.pidb 96 | *.svclog 97 | *.scc 98 | 99 | # Chutzpah Test files 100 | _Chutzpah* 101 | 102 | # Visual C++ cache files 103 | ipch/ 104 | *.aps 105 | *.ncb 106 | *.opendb 107 | *.opensdf 108 | *.sdf 109 | *.cachefile 110 | *.VC.db 111 | *.VC.VC.opendb 112 | 113 | # Visual Studio profiler 114 | *.psess 115 | *.vsp 116 | *.vspx 117 | *.sap 118 | 119 | # Visual Studio Trace Files 120 | *.e2e 121 | 122 | # TFS 2012 Local Workspace 123 | $tf/ 124 | 125 | # Guidance Automation Toolkit 126 | *.gpState 127 | 128 | # ReSharper is a .NET coding add-in 129 | _ReSharper*/ 130 | *.[Rr]e[Ss]harper 131 | *.DotSettings.user 132 | 133 | # TeamCity is a build add-in 134 | _TeamCity* 135 | 136 | # DotCover is a Code Coverage Tool 137 | *.dotCover 138 | 139 | # AxoCover is a Code Coverage Tool 140 | .axoCover/* 141 | !.axoCover/settings.json 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | Libs/*.dll 355 | RemovePlayerLimit/RemovePlayerLimit.csproj 356 | 357 | # VSC Stuff 358 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2022 andry08 (github.com/andry08) & CrowdedMods (github.com/CrowdedMods) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Our Discord](https://img.shields.io/discord/787008412482797598?color=7289da&label=DISCORD&style=for-the-badge)](https://discord.gg/XEc7PdDTyn) 2 | 3 | # CrowdedMod 4 | 5 | This mod unlocks the possibility for more than 15 players to join in an Among Us lobby. 6 | 7 | Works on Among Us Steam version from **v2020.12.9s** to **latest** 8 | 9 | ## Why is this mod outdated? / Please support mod X 10 | 11 | ### It's not outdated. 12 | 13 | #### See `#mod-version-support` channel in our [Discord](https://discord.gg/qzx9GzR7Yd) 14 | 15 | ### 16 | 17 | > ### :warning: **WARNING** :warning: 18 | > CrowdedMod does **not** work on InnerSloth servers. You can self-host your own modded server 19 | > with [Impostor](https://github.com/Impostor/Impostor) 20 | 21 | ## Versions 22 | 23 | | Mod version | Game version | BepInEx | Downloads | 24 | |-------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------| 25 | | v2.10.0 | v2024.9.4+ | [BepInEx CoreCLR](https://builds.bepinex.dev/projects/bepinex_be/733/BepInEx-Unity.IL2CPP-win-x86-6.0.0-be.733%2B995f049.zip) | [GitHub](https://github.com/CrowdedMods/CrowdedMod/releases/tag/2.10.0) | 26 | | v2.9.0 | v2022.12.14+ | [BepInEx CoreCLR](https://builds.bepinex.dev/projects/bepinex_be/660/BepInEx-Unity.IL2CPP-win-x86-6.0.0-be.660%2B40bf261.zip) | [GitHub](https://github.com/CrowdedMods/CrowdedMod/releases/tag/2.9.0) | 27 | | v2.8.0 | v2022.10.25+ | [BepInEx CoreCLR](https://builds.bepinex.dev/projects/bepinex_be/660/BepInEx-Unity.IL2CPP-win-x86-6.0.0-be.660%2B40bf261.zip) | [GitHub](https://github.com/CrowdedMods/CrowdedMod/releases/tag/v2.8.0) | 28 | | v2.7.0 | v2022.3.29 | [BepInEx x86](https://builds.bepinex.dev/projects/bepinex_be/562/BepInEx_UnityIL2CPP_x86_7a97bdd_6.0.0-be.562.zip) / [BepInEx CoreCLR](https://builds.bepinex.dev/projects/bepinex_be/660/BepInEx-Unity.IL2CPP-win-x86-6.0.0-be.660%2B40bf261.zip) | [GitHub](https://github.com/CrowdedMods/CrowdedMod/releases/tag/v2.7.0) | 29 | | v2.6.0 | v2021.5.10s | [Reactor BepInEx](https://github.com/NuclearPowered/BepInEx/releases/download/6.0.0-reactor.18%2Bstructfix/BepInEx-6.0.0-reactor.18+structfix.zip) | [CurseForge](https://www.curseforge.com/among-us/all-mods/crowdedmod/files/3310911) | 30 | | v2.5.0 | v2021.4.14s | [Reactor BepInEx](https://github.com/NuclearPowered/BepInEx/releases/download/6.0.0-reactor.18%2Bstructfix/BepInEx-6.0.0-reactor.18+structfix.zip) | [CurseForge](https://www.curseforge.com/among-us/all-mods/crowdedmod/files/3296325) | 31 | | v2.4.0 | v2021.4.12s | [Reactor BepInEx](https://github.com/NuclearPowered/BepInEx/releases/download/6.0.0-reactor.18%2Bstructfix/BepInEx-6.0.0-reactor.18+structfix.zip) | [CurseForge](https://www.curseforge.com/among-us/all-mods/crowdedmod/files/3279698) | 32 | | v2.3.0 | v2021.3.31.3s | [Reactor BepInEx](https://github.com/NuclearPowered/BepInEx/releases/download/6.0.0-reactor.18%2Bstructfix/BepInEx-6.0.0-reactor.18+structfix.zip) | [CurseForge](https://www.curseforge.com/among-us/all-mods/crowdedmod/files/3279689) | 33 | | v2.2.0 | v2021.3.5 | [Reactor BepInEx](https://github.com/NuclearPowered/BepInEx/releases/download/6.0.0-reactor.16/BepInEx-6.0.0-reactor.16.zip) | [CurseForge](https://www.curseforge.com/among-us/all-mods/crowdedmod/files/3261806) | 34 | | v2.1.1 | v2020.12.9s | [BepInEx x86](https://builds.bepis.io/projects/bepinex_be/335/BepInEx_UnityIL2CPP_x86_acedebc_6.0.0-be.335.zip) | [CurseForge](https://www.curseforge.com/among-us/all-mods/crowdedmod/files/3202698) | 35 | 36 | *Versions v2.2.0-v2.6.0 theoretically work with Itch and Epic, but they weren't tested.
37 | 38 | ## Installation 39 | 40 | See: [client installation](https://github.com/CrowdedMods/CrowdedMod/blob/master/docs/ClientInstallation.md) 41 | 42 | ## Credits 43 | 44 | - [andry08/100-player-mod](https://github.com/andry08/100-player-mod) - Original author of this mod (@andry08) 45 | - @NikoCat233 & @Tommy-XL - maintaining a fork for half a year 46 | 47 | -------------------------------------------------------------------------------- /build.cake: -------------------------------------------------------------------------------- 1 | var target = Argument("target", "Build"); 2 | 3 | var buildId = EnvironmentVariable("GITHUB_RUN_NUMBER"); 4 | 5 | var @ref = EnvironmentVariable("GITHUB_REF"); 6 | const string prefix = "refs/tags/"; 7 | var tag = !string.IsNullOrEmpty(@ref) && @ref.StartsWith(prefix) ? @ref.Substring(prefix.Length) : null; 8 | 9 | Task("Build") 10 | .Does(() => 11 | { 12 | var settings = new DotNetBuildSettings 13 | { 14 | Configuration = "Release", 15 | MSBuildSettings = new DotNetMSBuildSettings() 16 | }; 17 | 18 | if (tag != null) 19 | { 20 | settings.MSBuildSettings.Properties["Version"] = new[] { tag }; 21 | } 22 | else if (buildId != null) 23 | { 24 | settings.VersionSuffix = "ci." + buildId; 25 | } 26 | 27 | foreach (var gamePlatform in new[] { "Steam" }) 28 | { 29 | settings.MSBuildSettings.Properties["GamePlatform"] = new[] { gamePlatform }; 30 | DotNetBuild("src", settings); 31 | } 32 | }); 33 | 34 | RunTarget(target); -------------------------------------------------------------------------------- /docs/ClientInstallation.md: -------------------------------------------------------------------------------- 1 | ## Client mod installation 2 | 3 | To make mod work, you need two pieces. Custom server ([Impostor](https://github.com/Impostor/Impostor)) and a 4 | client-side mod (CrowdedMod). 5 |
6 | After installing this mod, you need to 7 | also [add custom server to the client menu](https://github.com/CrowdedMods/CrowdedMod/tree/master/docs/ServerSelection.md). 8 | 9 | 1. Download latest [BepInEx](https://builds.bepinex.dev/projects/bepinex_be) `Unity.IL2CPP-win-x86` from "Artifacts" 10 | section 11 | 2. Extract all files from zipped archive and put them in Among Us directory (where Among Us.exe is) 12 | 3. Download [latest Reactor.dll](https://github.com/NuclearPowered/Reactor/releases) and put it in 13 | `YourAmongUsDirectory/BepInEx/plugins` (you probably have to unzip it first) 14 | 4. Download right CrowdedMod dll from [CurseForge](https://www.curseforge.com/among-us/all-mods/crowdedmod/files) 15 | or [GitHub releases](https://github.com/CrowdedMods/CrowdedMod/releases) and put it in 16 | `YourAmongUsDirectory/BepInEx/plugins` (you probably have to unzip it first) 17 | 5. The first launch will take a while, be patient 18 | 6. Enjoy! 19 | 20 | > To make sure everything works correctly join local game (or any other except Freeplay) and you should see `CrowdedMod` 21 | > right under your ping -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Welcome to the CrowdedMod wiki! 2 | 3 | You should start by checking [client-side mod installation](https://github.com/CrowdedMods/CrowdedMod/tree/master/docs/ClientInstallation.md). -------------------------------------------------------------------------------- /docs/ServerSelection.md: -------------------------------------------------------------------------------- 1 | ## Custom server selection 2 | 3 | To play properly, you need to join a [custom server](https://github.com/Impostor/Impostor). 4 | 5 | ### Note 6 | 7 | If you're using content mods there is a good chance they included a modded server for you. 8 | Just select it in the bottom right corner of the "Find Game" menu. 9 | 10 | ### Manual installation 11 | 12 | Follow instructions on [this site](http://impostor.github.io/Impostor/) -------------------------------------------------------------------------------- /src/CrowdedMod.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30524.135 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdedMod", "CrowdedMod\CrowdedMod.csproj", "{AB79A6CE-60D8-4F2F-9797-7F92A3EFCBF2}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {AB79A6CE-60D8-4F2F-9797-7F92A3EFCBF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {AB79A6CE-60D8-4F2F-9797-7F92A3EFCBF2}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {AB79A6CE-60D8-4F2F-9797-7F92A3EFCBF2}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {AB79A6CE-60D8-4F2F-9797-7F92A3EFCBF2}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {767BDF72-7C0E-490F-8C81-927940B7CCFA} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/CrowdedMod/Components/AbstractPagingBehaviour.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace CrowdedMod.Components; 5 | 6 | // Interface until unhollower implements generic il2cpp (if it's possible) 7 | /// 8 | /// This class is not actually abstract because unhollower does not support it.
9 | /// You need to implement and . 10 | ///
11 | public class AbstractPagingBehaviour : MonoBehaviour 12 | { 13 | public AbstractPagingBehaviour(IntPtr ptr) : base(ptr) 14 | { 15 | } 16 | 17 | public const string PAGE_INDEX_GAME_OBJECT_NAME = "CrowdedMod_PageIndex"; 18 | 19 | private int _page; 20 | 21 | public virtual int MaxPerPage => 15; 22 | // public virtual IEnumerable Targets { get; } 23 | 24 | public virtual int PageIndex 25 | { 26 | get => _page; 27 | set 28 | { 29 | _page = value; 30 | OnPageChanged(); 31 | } 32 | } 33 | 34 | public virtual int MaxPageIndex => throw new NotImplementedException(); 35 | // public virtual int MaxPages => Targets.Count() / MaxPerPage; 36 | 37 | public virtual void OnPageChanged() => throw new NotImplementedException(); 38 | 39 | public virtual void Start() => OnPageChanged(); 40 | 41 | public virtual void Update() 42 | { 43 | if (Input.GetKeyDown(KeyCode.UpArrow) || Input.GetKeyDown(KeyCode.LeftArrow) || Input.mouseScrollDelta.y > 0f) 44 | Cycle(false); 45 | else if (Input.GetKeyDown(KeyCode.DownArrow) || Input.GetKeyDown(KeyCode.RightArrow) || 46 | Input.mouseScrollDelta.y < 0f) 47 | Cycle(true); 48 | } 49 | 50 | /// 51 | /// Loops around if you go over the limits.
52 | /// Attempting to go up a page while on the first page will take you to the last page and vice versa. 53 | ///
54 | public virtual void Cycle(bool increment) 55 | { 56 | var change = increment ? 1 : -1; 57 | PageIndex = Mathf.Clamp(PageIndex + change, 0, MaxPageIndex); 58 | } 59 | } -------------------------------------------------------------------------------- /src/CrowdedMod/Components/MeetingHudPagingBehaviour.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Il2CppInterop.Runtime.Attributes; 5 | using Reactor.Utilities.Attributes; 6 | using UnityEngine; 7 | 8 | namespace CrowdedMod.Components; 9 | 10 | [RegisterInIl2Cpp] 11 | public class MeetingHudPagingBehaviour : AbstractPagingBehaviour 12 | { 13 | public MeetingHudPagingBehaviour(IntPtr ptr) : base(ptr) 14 | { 15 | } 16 | 17 | internal MeetingHud meetingHud = null!; 18 | 19 | [HideFromIl2Cpp] public IEnumerable Targets => meetingHud.playerStates.OrderBy(p => p.AmDead); 20 | public override int MaxPageIndex => (Targets.Count() - 1) / MaxPerPage; 21 | 22 | public override void Start() => OnPageChanged(); 23 | 24 | public override void Update() 25 | { 26 | base.Update(); 27 | 28 | if (meetingHud.state is MeetingHud.VoteStates.Animating or MeetingHud.VoteStates.Proceeding || 29 | meetingHud.TimerText.text.Contains($" ({PageIndex + 1}/{MaxPageIndex + 1})")) 30 | return; 31 | 32 | meetingHud.TimerText.text += $" ({PageIndex + 1}/{MaxPageIndex + 1})"; 33 | } 34 | 35 | public override void OnPageChanged() 36 | { 37 | var i = 0; 38 | 39 | foreach (var button in Targets) 40 | { 41 | if (i >= PageIndex * MaxPerPage && i < (PageIndex + 1) * MaxPerPage) 42 | { 43 | button.gameObject.SetActive(true); 44 | 45 | var relativeIndex = i % MaxPerPage; 46 | var row = relativeIndex / 3; 47 | var col = relativeIndex % 3; 48 | var buttonTransform = button.transform; 49 | buttonTransform.localPosition = meetingHud.VoteOrigin + 50 | new Vector3( 51 | meetingHud.VoteButtonOffsets.x * col, 52 | meetingHud.VoteButtonOffsets.y * row, 53 | buttonTransform.localPosition.z 54 | ); 55 | } 56 | else 57 | { 58 | button.gameObject.SetActive(false); 59 | } 60 | 61 | i++; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/CrowdedMod/Components/ShapeShifterPagingBehaviour.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Il2CppInterop.Runtime.Attributes; 5 | using Reactor.Utilities.Attributes; 6 | using TMPro; 7 | using UnityEngine; 8 | 9 | namespace CrowdedMod.Components; 10 | 11 | [RegisterInIl2Cpp] 12 | public class ShapeShifterPagingBehaviour : AbstractPagingBehaviour 13 | { 14 | public ShapeShifterPagingBehaviour(IntPtr ptr) : base(ptr) 15 | { 16 | } 17 | 18 | public ShapeshifterMinigame shapeshifterMinigame = null!; 19 | [HideFromIl2Cpp] public IEnumerable Targets => shapeshifterMinigame.potentialVictims.ToArray(); 20 | 21 | public override int MaxPageIndex => (Targets.Count() - 1) / MaxPerPage; 22 | private TextMeshPro PageText = null!; 23 | 24 | public override void Start() 25 | { 26 | PageText = Instantiate(HudManager.Instance.KillButton.cooldownTimerText, shapeshifterMinigame.transform); 27 | PageText.name = PAGE_INDEX_GAME_OBJECT_NAME; 28 | PageText.enableWordWrapping = false; 29 | PageText.gameObject.SetActive(true); 30 | PageText.transform.localPosition = new Vector3(4.1f, -2.36f, -1f); 31 | PageText.transform.localScale *= 0.5f; 32 | OnPageChanged(); 33 | } 34 | 35 | public override void OnPageChanged() 36 | { 37 | PageText.text = $"({PageIndex + 1}/{MaxPageIndex + 1})"; 38 | var i = 0; 39 | 40 | foreach (var panel in Targets) 41 | { 42 | if (i >= PageIndex * MaxPerPage && i < (PageIndex + 1) * MaxPerPage) 43 | { 44 | panel.gameObject.SetActive(true); 45 | 46 | var relativeIndex = i % MaxPerPage; 47 | var row = relativeIndex / 3; 48 | var col = relativeIndex % 3; 49 | var buttonTransform = panel.transform; 50 | buttonTransform.localPosition = new Vector3( 51 | shapeshifterMinigame.XStart + shapeshifterMinigame.XOffset * col, 52 | shapeshifterMinigame.YStart + shapeshifterMinigame.YOffset * row, 53 | buttonTransform.localPosition.z 54 | ); 55 | } 56 | else 57 | { 58 | panel.gameObject.SetActive(false); 59 | } 60 | 61 | i++; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/CrowdedMod/Components/VitalsPagingBehaviour.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Il2CppInterop.Runtime.Attributes; 5 | using TMPro; 6 | using Reactor.Utilities.Attributes; 7 | using UnityEngine; 8 | 9 | namespace CrowdedMod.Components; 10 | 11 | [RegisterInIl2Cpp] 12 | public class VitalsPagingBehaviour : AbstractPagingBehaviour 13 | { 14 | public VitalsPagingBehaviour(IntPtr ptr) : base(ptr) 15 | { 16 | } 17 | 18 | public VitalsMinigame vitalsMinigame = null!; 19 | 20 | [HideFromIl2Cpp] public IEnumerable Targets => vitalsMinigame.vitals.ToArray(); 21 | public override int MaxPageIndex => (Targets.Count() - 1) / MaxPerPage; 22 | private TextMeshPro PageText = null!; 23 | 24 | public override void Start() 25 | { 26 | PageText = Instantiate(HudManager.Instance.KillButton.cooldownTimerText, vitalsMinigame.transform); 27 | PageText.name = PAGE_INDEX_GAME_OBJECT_NAME; 28 | PageText.enableWordWrapping = false; 29 | PageText.gameObject.SetActive(true); 30 | PageText.transform.localPosition = new Vector3(2.7f, -2f, -1f); 31 | PageText.transform.localScale *= 0.5f; 32 | OnPageChanged(); 33 | } 34 | 35 | public override void OnPageChanged() 36 | { 37 | if (PlayerTask.PlayerHasTaskOfType(PlayerControl.LocalPlayer)) 38 | return; 39 | 40 | PageText.text = $"({PageIndex + 1}/{MaxPageIndex + 1})"; 41 | var i = 0; 42 | 43 | foreach (var panel in Targets) 44 | { 45 | if (i >= PageIndex * MaxPerPage && i < (PageIndex + 1) * MaxPerPage) 46 | { 47 | panel.gameObject.SetActive(true); 48 | var relativeIndex = i % MaxPerPage; 49 | var row = relativeIndex / 3; 50 | var col = relativeIndex % 3; 51 | var panelTransform = panel.transform; 52 | panelTransform.localPosition = new Vector3( 53 | vitalsMinigame.XStart + vitalsMinigame.XOffset * col, 54 | vitalsMinigame.YStart + vitalsMinigame.YOffset * row, 55 | panelTransform.localPosition.z 56 | ); 57 | } 58 | else 59 | panel.gameObject.SetActive(false); 60 | 61 | i++; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/CrowdedMod/CrowdedMod.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 2.10.0 6 | CrowdedMods, andry08 7 | 8 | latest 9 | embedded 10 | enable 11 | 12 | 13 | Steam 14 | 2024.9.4 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/CrowdedMod/CrowdedModPlugin.cs: -------------------------------------------------------------------------------- 1 | using BepInEx; 2 | using BepInEx.Unity.IL2CPP; 3 | using HarmonyLib; 4 | using Reactor; 5 | using Reactor.Networking; 6 | using Reactor.Networking.Attributes; 7 | using Reactor.Utilities; 8 | using System.Linq; 9 | 10 | namespace CrowdedMod; 11 | 12 | [BepInAutoPlugin("xyz.crowdedmods.crowdedmod")] 13 | [BepInProcess("Among Us.exe")] 14 | [BepInDependency(ReactorPlugin.Id)] 15 | [ReactorModFlags(ModFlags.RequireOnAllClients)] 16 | [BepInDependency("gg.reactor.debugger", BepInDependency.DependencyFlags.SoftDependency)] 17 | public partial class CrowdedModPlugin : BasePlugin 18 | { 19 | public const int MaxPlayers = 254; // allegedly. should stick to 127 tho 20 | public const int MaxImpostors = MaxPlayers / 2; 21 | 22 | public static bool ForceDisableFreeColor { get; set; } = false; 23 | 24 | private Harmony Harmony { get; } = new(Id); 25 | 26 | public override void Load() 27 | { 28 | ReactorCredits.Register(ReactorCredits.AlwaysShow); 29 | 30 | Harmony.PatchAll(); 31 | 32 | RemoveVanillaServer(); 33 | } 34 | 35 | public static void RemoveVanillaServer() 36 | { 37 | var sm = ServerManager.Instance; 38 | var curRegions = sm.AvailableRegions; 39 | sm.AvailableRegions = curRegions.Where(region => !IsVanillaServer(region)).ToArray(); 40 | 41 | var defaultRegion = ServerManager.DefaultRegions; 42 | ServerManager.DefaultRegions = defaultRegion.Where(region => !IsVanillaServer(region)).ToArray(); 43 | 44 | if (IsVanillaServer(sm.CurrentRegion)) 45 | { 46 | var region = defaultRegion.FirstOrDefault(); 47 | sm.SetRegion(region); 48 | } 49 | } 50 | 51 | private static bool IsVanillaServer(IRegionInfo regionInfo) 52 | => regionInfo != null && 53 | regionInfo.TranslateName is 54 | StringNames.ServerAS or 55 | StringNames.ServerEU or 56 | StringNames.ServerNA; 57 | } -------------------------------------------------------------------------------- /src/CrowdedMod/Net/CustomRpcCalls.cs: -------------------------------------------------------------------------------- 1 | namespace CrowdedMod.Net; 2 | 3 | public enum CustomRpcCalls 4 | { 5 | SetColor 6 | } -------------------------------------------------------------------------------- /src/CrowdedMod/Net/SetColorRpc.cs: -------------------------------------------------------------------------------- 1 | using Hazel; 2 | using Reactor.Networking.Attributes; 3 | using Reactor.Networking.Rpc; 4 | 5 | namespace CrowdedMod.Net; 6 | 7 | [RegisterCustomRpc((uint)CustomRpcCalls.SetColor)] 8 | public class SetColorRpc : PlayerCustomRpc 9 | { 10 | public SetColorRpc(CrowdedModPlugin plugin, uint id) : base(plugin, id) 11 | { 12 | } 13 | 14 | public override RpcLocalHandling LocalHandling => RpcLocalHandling.After; 15 | 16 | public override void Write(MessageWriter writer, byte data) 17 | { 18 | writer.Write(data); 19 | } 20 | 21 | public override byte Read(MessageReader reader) 22 | { 23 | return reader.ReadByte(); 24 | } 25 | 26 | public override void Handle(PlayerControl player, byte data) 27 | { 28 | player.SetColor(data); 29 | } 30 | } -------------------------------------------------------------------------------- /src/CrowdedMod/Patches/CreateGameOptionsPatches.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using AmongUs.GameOptions; 3 | using HarmonyLib; 4 | using Reactor.Utilities.Extensions; 5 | using TMPro; 6 | using UnityEngine; 7 | 8 | namespace CrowdedMod.Patches; 9 | 10 | internal static class CreateGameOptionsPatches 11 | { 12 | private static bool HasManyImpostors(this IGameOptions options) => 13 | options.GameMode is GameModes.Normal or GameModes.NormalFools; 14 | 15 | private static bool IsHost(this CreateOptionsPicker picker) => picker.mode == SettingsMode.Host; 16 | 17 | [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.Awake))] 18 | public static class CreateOptionsPicker_Awake 19 | { 20 | public static void Postfix(CreateOptionsPicker __instance) 21 | { 22 | if (!__instance.IsHost()) return; 23 | 24 | { 25 | var firstButtonRenderer = __instance.MaxPlayerButtons[0]; 26 | firstButtonRenderer.GetComponentInChildren().text = "-"; 27 | firstButtonRenderer.enabled = false; 28 | 29 | var firstButtonButton = firstButtonRenderer.GetComponent(); 30 | firstButtonButton.OnClick.RemoveAllListeners(); 31 | firstButtonButton.OnClick.AddListener((Action)(() => 32 | { 33 | for (var i = 1; i < 11; i++) 34 | { 35 | var playerButton = __instance.MaxPlayerButtons[i]; 36 | 37 | var tmp = playerButton.GetComponentInChildren(); 38 | var newValue = Mathf.Max(byte.Parse(tmp.text) - 10, byte.Parse(playerButton.name) - 2); 39 | tmp.text = newValue.ToString(); 40 | } 41 | 42 | __instance.UpdateMaxPlayersButtons(__instance.GetTargetOptions()); 43 | })); 44 | firstButtonRenderer.Destroy(); 45 | 46 | var lastButtonRenderer = __instance.MaxPlayerButtons[^1]; 47 | lastButtonRenderer.GetComponentInChildren().text = "+"; 48 | lastButtonRenderer.enabled = false; 49 | 50 | var lastButtonButton = lastButtonRenderer.GetComponent(); 51 | lastButtonButton.OnClick.RemoveAllListeners(); 52 | lastButtonButton.OnClick.AddListener((Action)(() => 53 | { 54 | for (var i = 1; i < 11; i++) 55 | { 56 | var playerButton = __instance.MaxPlayerButtons[i]; 57 | 58 | var tmp = playerButton.GetComponentInChildren(); 59 | var newValue = Mathf.Min(byte.Parse(tmp.text) + 10, 60 | CrowdedModPlugin.MaxPlayers - 14 + byte.Parse(playerButton.name)); 61 | tmp.text = newValue.ToString(); 62 | } 63 | 64 | __instance.UpdateMaxPlayersButtons(__instance.GetTargetOptions()); 65 | })); 66 | lastButtonRenderer.Destroy(); 67 | 68 | for (var i = 1; i < 11; i++) 69 | { 70 | var playerButton = __instance.MaxPlayerButtons[i].GetComponent(); 71 | var text = playerButton.GetComponentInChildren(); 72 | 73 | playerButton.OnClick.RemoveAllListeners(); 74 | playerButton.OnClick.AddListener((Action)(() => 75 | { 76 | var maxPlayers = byte.Parse(text.text); 77 | var maxImp = Mathf.Min(__instance.GetTargetOptions().NumImpostors, maxPlayers / 2); 78 | __instance.GetTargetOptions().SetInt(Int32OptionNames.NumImpostors, maxImp); 79 | __instance.ImpostorButtons[1].TextMesh.text = maxImp.ToString(); 80 | __instance.SetMaxPlayersButtons(maxPlayers); 81 | })); 82 | } 83 | 84 | foreach (var button in __instance.MaxPlayerButtons) 85 | { 86 | button.enabled = button.GetComponentInChildren().text == __instance.GetTargetOptions().MaxPlayers.ToString(); 87 | } 88 | } 89 | 90 | var secondButton = __instance.ImpostorButtons[1]; 91 | secondButton.SpriteRenderer.enabled = false; 92 | secondButton.transform.FindChild("ConsoleHighlight").gameObject.Destroy(); 93 | secondButton.PassiveButton.OnClick.RemoveAllListeners(); 94 | secondButton.BoxCollider.size = new Vector2(0f, 0f); 95 | 96 | var secondButtonText = secondButton.TextMesh; 97 | secondButtonText.text = __instance.GetTargetOptions().NumImpostors.ToString(); 98 | 99 | var firstButton = __instance.ImpostorButtons[0]; 100 | firstButton.SpriteRenderer.enabled = false; 101 | firstButton.TextMesh.text = "-"; 102 | 103 | var firstPassiveButton = firstButton.PassiveButton; 104 | firstPassiveButton.OnClick.RemoveAllListeners(); 105 | firstPassiveButton.OnClick.AddListener((Action)(() => { 106 | var newVal = Mathf.Clamp( 107 | byte.Parse(secondButtonText.text) - 1, 108 | 1, 109 | __instance.GetTargetOptions().MaxPlayers / 2 110 | ); 111 | __instance.SetImpostorButtons(newVal); 112 | secondButtonText.text = newVal.ToString(); 113 | })); 114 | 115 | var thirdButton = __instance.ImpostorButtons[2]; 116 | thirdButton.SpriteRenderer.enabled = false; 117 | thirdButton.TextMesh.text = "+"; 118 | 119 | var thirdPassiveButton = thirdButton.PassiveButton; 120 | thirdPassiveButton.OnClick.RemoveAllListeners(); 121 | thirdPassiveButton.OnClick.AddListener((Action)(() => { 122 | var newVal = Mathf.Clamp( 123 | byte.Parse(secondButtonText.text) + 1, 124 | 1, 125 | __instance.GetTargetOptions().MaxPlayers / 2 126 | ); 127 | __instance.SetImpostorButtons(newVal); 128 | secondButtonText.text = newVal.ToString(); 129 | })); 130 | } 131 | } 132 | 133 | [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.UpdateMaxPlayersButtons))] 134 | public static class CreateOptionsPicker_UpdateMaxPlayersButtons 135 | { 136 | public static bool Prefix(CreateOptionsPicker __instance, [HarmonyArgument(0)] IGameOptions opts) 137 | { 138 | if (__instance.CrewArea) 139 | { 140 | __instance.CrewArea.SetCrewSize(opts.MaxPlayers, opts.NumImpostors); 141 | } 142 | 143 | var selectedAsString = opts.MaxPlayers.ToString(); 144 | for (var i = 1; i < __instance.MaxPlayerButtons.Count - 1; i++) 145 | { 146 | __instance.MaxPlayerButtons[i].enabled = __instance.MaxPlayerButtons[i].GetComponentInChildren().text == selectedAsString; 147 | } 148 | 149 | return false; 150 | } 151 | } 152 | 153 | [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.UpdateImpostorsButtons))] 154 | public static class CreateOptionsPicker_UpdateImpostorsButtons 155 | { 156 | public static bool Prefix(CreateOptionsPicker __instance) 157 | { 158 | var hasMany = __instance.GetTargetOptions().HasManyImpostors(); 159 | 160 | foreach (var button in __instance.ImpostorButtons) 161 | { 162 | button.SetOptionEnabled(hasMany); 163 | } 164 | 165 | __instance.ImpostorText.color = hasMany ? Color.white : Palette.DisabledGrey; 166 | 167 | return false; 168 | } 169 | } 170 | 171 | [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.Refresh))] 172 | public static class CreateOptionsPicker_Refresh 173 | { 174 | public static bool Prefix(CreateOptionsPicker __instance) 175 | { 176 | var options = __instance.GetTargetOptions(); 177 | 178 | __instance.UpdateImpostorsButtons(options.NumImpostors); 179 | __instance.UpdateMaxPlayersButtons(options); 180 | __instance.UpdateLanguageButton((uint)options.Keywords); 181 | __instance.MapMenu.UpdateMapButtons(options.MapId); 182 | var modeName = GameModesHelpers.ModeToName[GameOptionsManager.Instance.CurrentGameOptions.GameMode]; 183 | __instance.GameModeText.text = TranslationController.Instance.GetString(modeName); 184 | 185 | return false; 186 | } 187 | } 188 | } -------------------------------------------------------------------------------- /src/CrowdedMod/Patches/GenericPatches.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using AmongUs.GameOptions; 3 | using CrowdedMod.Net; 4 | using HarmonyLib; 5 | using Hazel; 6 | using Il2CppInterop.Runtime; 7 | using Il2CppInterop.Runtime.InteropTypes.Arrays; 8 | using Il2CppSystem.Reflection; 9 | using InnerNet; 10 | using Reactor.Networking.Rpc; 11 | using Reactor.Utilities; 12 | using UnityEngine; 13 | 14 | namespace CrowdedMod.Patches; 15 | 16 | internal static class GenericPatches 17 | { 18 | private static bool ShouldDisableColorPatch => CrowdedModPlugin.ForceDisableFreeColor || 19 | GameData.Instance == null || 20 | GameData.Instance.PlayerCount <= Palette.PlayerColors.Length; 21 | 22 | [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdCheckColor))] 23 | public static class PlayerControlCmdCheckColorPatch 24 | { 25 | public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] byte colorId) 26 | { 27 | if (ShouldDisableColorPatch) 28 | { 29 | return true; 30 | } 31 | 32 | Rpc.Instance.Send(__instance, colorId); 33 | return false; 34 | } 35 | } 36 | 37 | [HarmonyPatch(typeof(PlayerTab), nameof(PlayerTab.Update))] 38 | public static class PlayerTabIsSelectedItemEquippedPatch 39 | { 40 | public static void Postfix(PlayerTab __instance) 41 | { 42 | if (ShouldDisableColorPatch) 43 | { 44 | return; 45 | } 46 | 47 | __instance.currentColorIsEquipped = false; 48 | } 49 | } 50 | 51 | [HarmonyPatch(typeof(PlayerTab), nameof(PlayerTab.UpdateAvailableColors))] 52 | public static class PlayerTabUpdateAvailableColorsPatch 53 | { 54 | public static bool Prefix(PlayerTab __instance) 55 | { 56 | if (ShouldDisableColorPatch) 57 | { 58 | return true; 59 | } 60 | 61 | __instance.AvailableColors.Clear(); 62 | for (var i = 0; i < Palette.PlayerColors.Count; i++) 63 | { 64 | if (!PlayerControl.LocalPlayer || PlayerControl.LocalPlayer.CurrentOutfit.ColorId != i) 65 | { 66 | __instance.AvailableColors.Add(i); 67 | } 68 | } 69 | 70 | return false; 71 | } 72 | } 73 | 74 | // I did not find a use of this method, but still patching for future updates 75 | // maxExpectedPlayers is unknown, looks like server code tbh 76 | [HarmonyPatch(typeof(GameOptionsData), nameof(GameOptionsData.AreInvalid))] 77 | public static class InvalidOptionsPatches 78 | { 79 | public static bool Prefix(GameOptionsData __instance, [HarmonyArgument(0)] int maxExpectedPlayers) 80 | { 81 | return __instance.MaxPlayers > maxExpectedPlayers || 82 | __instance.NumImpostors < 1 || 83 | __instance.NumImpostors + 1 > maxExpectedPlayers / 2 || 84 | __instance.KillDistance is < 0 or > 2 || 85 | __instance.PlayerSpeedMod is <= 0f or > 3f; 86 | } 87 | } 88 | 89 | [HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.Update))] 90 | public static class GameStartManagerUpdatePatch 91 | { 92 | private static string fixDummyCounterColor = string.Empty; 93 | 94 | public static void Prefix(GameStartManager __instance) 95 | { 96 | if (GameData.Instance == null || __instance.LastPlayerCount == GameData.Instance.PlayerCount) 97 | { 98 | return; 99 | } 100 | 101 | if (__instance.LastPlayerCount > __instance.MinPlayers) 102 | { 103 | fixDummyCounterColor = ""; 104 | } 105 | else if (__instance.LastPlayerCount == __instance.MinPlayers) 106 | { 107 | fixDummyCounterColor = ""; 108 | } 109 | else 110 | { 111 | fixDummyCounterColor = ""; 112 | } 113 | } 114 | 115 | public static void Postfix(GameStartManager __instance) 116 | { 117 | if (string.IsNullOrEmpty(fixDummyCounterColor) || 118 | GameData.Instance == null || 119 | GameManager.Instance == null || 120 | GameManager.Instance.LogicOptions == null) 121 | { 122 | return; 123 | } 124 | 125 | __instance.PlayerCounter.text = 126 | $"{fixDummyCounterColor}{GameData.Instance.PlayerCount}/{GameManager.Instance.LogicOptions.MaxPlayers}"; 127 | fixDummyCounterColor = string.Empty; 128 | } 129 | } 130 | 131 | [HarmonyPatch(typeof(InnerNetServer), nameof(InnerNetServer.HandleNewGameJoin))] 132 | public static class InnerNetSerer_HandleNewGameJoin 133 | { 134 | public static bool Prefix(InnerNetServer __instance, [HarmonyArgument(0)] InnerNetServer.Player client) 135 | { 136 | if (__instance.Clients.Count is < 15 or >= CrowdedModPlugin.MaxPlayers) return true; 137 | 138 | __instance.Clients.Add(client); 139 | 140 | client.LimboState = LimboStates.PreSpawn; 141 | if (__instance.HostId == -1) 142 | { 143 | __instance.HostId = __instance.Clients.ToArray()[0].Id; 144 | } 145 | 146 | if (__instance.HostId == client.Id) 147 | { 148 | client.LimboState = LimboStates.NotLimbo; 149 | } 150 | 151 | var writer = MessageWriter.Get(SendOption.Reliable); 152 | try 153 | { 154 | __instance.WriteJoinedMessage(client, writer, true); 155 | client.Connection.Send(writer); 156 | __instance.BroadcastJoinMessage(client, writer); 157 | } 158 | catch (Il2CppException exception) 159 | { 160 | Debug.LogError("[CM] InnerNetServer::HandleNewGameJoin MessageWriter 2 Exception: " + 161 | exception.Message); 162 | // Debug.LogException(exception, __instance); 163 | } 164 | finally 165 | { 166 | writer.Recycle(); 167 | } 168 | 169 | return false; 170 | } 171 | } 172 | 173 | private static void TryAdjustOptionsRecommendations(GameOptionsManager manager) 174 | { 175 | const int MaxPlayers = CrowdedModPlugin.MaxPlayers; 176 | var type = manager.GetGameOptions(); 177 | var options = manager.GameHostOptions.Cast(); 178 | 179 | var maxRecommendation = ((Il2CppStructArray)Enumerable.Repeat(MaxPlayers, MaxPlayers + 1).ToArray()) 180 | .Cast(); 181 | var minRecommendation = ((Il2CppStructArray)Enumerable.Repeat(4, MaxPlayers + 1).ToArray()) 182 | .Cast(); 183 | var killRecommendation = ((Il2CppStructArray)Enumerable.Repeat(0, MaxPlayers + 1).ToArray()) 184 | .Cast(); 185 | 186 | 187 | const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static; 188 | // all these fields are currently static, but we're doing a forward compat 189 | // static fields ignore object param so non-null instance is ok 190 | type.GetField("RecommendedImpostors", flags)?.SetValue(options, maxRecommendation); 191 | type.GetField("MaxImpostors", flags)?.SetValue(options, maxRecommendation); 192 | type.GetField("RecommendedKillCooldown", flags)?.SetValue(options, killRecommendation); 193 | type.GetField("MinPlayers", flags)?.SetValue(options, minRecommendation); 194 | } 195 | 196 | [HarmonyPatch(typeof(GameOptionsManager), nameof(GameOptionsManager.GameHostOptions), MethodType.Setter)] 197 | public static class GameOptionsManager_set_GameHostOptions 198 | { 199 | 200 | public static void Postfix(GameOptionsManager __instance) 201 | { 202 | try 203 | { 204 | TryAdjustOptionsRecommendations(__instance); 205 | } 206 | catch (System.Exception e) 207 | { 208 | Logger.Error($"Failed to adjust options recommendations: {e}"); 209 | } 210 | } 211 | } 212 | 213 | [HarmonyPatch(typeof(GameOptionsManager), nameof(GameOptionsManager.SwitchGameMode))] 214 | public static class GameOptionsManager_SwitchGameMode 215 | { 216 | public static void Postfix(GameOptionsManager __instance) 217 | { 218 | try 219 | { 220 | TryAdjustOptionsRecommendations(__instance); 221 | } 222 | catch (System.Exception e) 223 | { 224 | Logger.Error($"Failed to adjust options recommendations: {e}"); 225 | } 226 | } 227 | } 228 | 229 | [HarmonyPatch(typeof(GameManager), nameof(GameManager.Awake))] 230 | public static class GameManager_Awake 231 | { 232 | public static void Postfix(GameManager __instance) 233 | { 234 | foreach (var category in __instance.GameSettingsList.AllCategories) 235 | { 236 | foreach (var option in category.AllGameSettings) 237 | { 238 | if (option is IntGameSetting intOption && option.Title == StringNames.GameNumImpostors) 239 | { 240 | intOption.ValidRange = new IntRange(1, CrowdedModPlugin.MaxImpostors); 241 | return; 242 | } 243 | } 244 | } 245 | } 246 | } 247 | 248 | [HarmonyPatch(typeof(MainMenuManager), nameof(MainMenuManager.Start))] 249 | public static class RemoveVanillaServerPatch 250 | { 251 | public static void Postfix() 252 | { 253 | CrowdedModPlugin.RemoveVanillaServer(); 254 | } 255 | } 256 | } -------------------------------------------------------------------------------- /src/CrowdedMod/Patches/MiniGamePatches.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Il2CppInterop.Runtime.InteropTypes.Arrays; 3 | 4 | namespace CrowdedMod.Patches; 5 | 6 | internal static class MiniGamePatches 7 | { 8 | [HarmonyPatch(typeof(SecurityLogger), nameof(SecurityLogger.Awake))] 9 | public static class SecurityLoggerPatch 10 | { 11 | public static void Postfix(SecurityLogger __instance) 12 | { 13 | __instance.Timers = new Il2CppStructArray(CrowdedModPlugin.MaxPlayers); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/CrowdedMod/Patches/PagingPatches.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using CrowdedMod.Components; 3 | 4 | namespace CrowdedMod.Patches; 5 | 6 | internal static class PagingPatches 7 | { 8 | [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))] 9 | public static class MeetingHudStartPatch 10 | { 11 | public static void Postfix(MeetingHud __instance) 12 | { 13 | __instance.gameObject.AddComponent().meetingHud = __instance; 14 | } 15 | } 16 | 17 | [HarmonyPatch(typeof(ShapeshifterMinigame), nameof(ShapeshifterMinigame.Begin))] 18 | public static class ShapeshifterMinigameBeginPatch 19 | { 20 | public static void Postfix(ShapeshifterMinigame __instance) 21 | { 22 | __instance.gameObject.AddComponent().shapeshifterMinigame = __instance; 23 | } 24 | } 25 | 26 | [HarmonyPatch(typeof(VitalsMinigame), nameof(VitalsMinigame.Begin))] 27 | public static class VitalsMinigameBeginPatch 28 | { 29 | public static void Postfix(VitalsMinigame __instance) 30 | { 31 | __instance.gameObject.AddComponent().vitalsMinigame = __instance; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/nuget.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | --------------------------------------------------------------------------------