├── .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 | [](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 |
--------------------------------------------------------------------------------