diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d38bb89 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,84 @@ +[*.cs] + +# IDE0022: Use block body for method +csharp_style_expression_bodied_methods = false:silent +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent + +# CS8604: Possible null reference argument. +dotnet_diagnostic.CS8604.severity = silent + +# CS8602: Dereference of a possibly null reference. +dotnet_diagnostic.CS8602.severity = none + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_collection_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion diff --git a/BetterAmongUs.csproj b/BetterAmongUs.csproj new file mode 100644 index 0000000..7fe3fb7 --- /dev/null +++ b/BetterAmongUs.csproj @@ -0,0 +1,37 @@ + + + + net6.0 + enable + enable + Library + preview + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + diff --git a/BetterAmongUs.sln b/BetterAmongUs.sln new file mode 100644 index 0000000..910995f --- /dev/null +++ b/BetterAmongUs.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34511.84 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BetterAmongUs", "BetterAmongUs.csproj", "{41193AE4-8181-49F8-9DA4-F4B7B275A0EA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {41193AE4-8181-49F8-9DA4-F4B7B275A0EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41193AE4-8181-49F8-9DA4-F4B7B275A0EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41193AE4-8181-49F8-9DA4-F4B7B275A0EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41193AE4-8181-49F8-9DA4-F4B7B275A0EA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D51C3994-D13F-43E5-9B5E-E029EC534208} + EndGlobalSection +EndGlobal diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Modules/AntiCheat.cs b/Modules/AntiCheat.cs new file mode 100644 index 0000000..f443e10 --- /dev/null +++ b/Modules/AntiCheat.cs @@ -0,0 +1,369 @@ +using AmongUs.GameOptions; +using Hazel; +using InnerNet; + +namespace BetterAmongUs; + +class AntiCheat +{ + public static Dictionary PlayerData = []; // HashPuid, FriendCode + public static Dictionary SickoData = []; // HashPuid, FriendCode + public static Dictionary AUMData = []; // HashPuid, FriendCode + private static bool IsEnabled = true; + + public static void PauseAntiCheat() + { + float time = 2f; + if (IsEnabled) + { + IsEnabled = false; + + _ = new LateTask(() => + { + IsEnabled = true; + }, time, "PauseAntiCheat"); + } + } + + // Handle RPC before anti cheat detection + public static void HandleRPCBeforeCheck(PlayerControl player, byte callId, MessageReader reader) + { + if (player == PlayerControl.LocalPlayer || PlayerControl.LocalPlayer == null || player == null || !IsEnabled) return; + + if (callId is unchecked((byte)CustomRPC.Sicko) && Main.AntiCheat.Value) + { + if (reader.BytesRemaining == 0) + { + var flag = SickoData.ContainsKey(Utils.GetHashPuid(player)); + + if (reader.BytesRemaining == 0 && !flag) + { + AmongUsClient.Instance.ReportPlayer(player.GetClientId(), ReportReasons.Cheating_Hacking); + BAUNotificationManager.NotifyCheat(player, $"Sicko Menu", newText: "Has been detected with a cheat client"); + PlayerData[Utils.GetHashPuid(player)] = player.FriendCode; + SickoData[Utils.GetHashPuid(player)] = player.FriendCode; + BAUDataManager.SaveCheatData(Utils.GetHashPuid(player), player.Data.FriendCode, player.Data.PlayerName, "cheatData", "Sicko Menu RPC"); + BAUDataManager.SaveCheatData(Utils.GetHashPuid(player), player.Data.FriendCode, player.Data.PlayerName, "sickoData", "Sicko Menu RPC"); + } + } + + return; + } + + if (callId is unchecked((byte)CustomRPC.AUM) && Main.AntiCheat.Value) + { + try + { + var aumid = reader.ReadByte(); + + if (aumid == player.PlayerId) + { + var flag = AUMData.ContainsKey(Utils.GetHashPuid(player)); + + if (!flag) + { + AmongUsClient.Instance.ReportPlayer(player.GetClientId(), ReportReasons.Cheating_Hacking); + BAUNotificationManager.NotifyCheat(player, $"AUM", newText: "Has been detected with a cheat client"); + PlayerData[Utils.GetHashPuid(player)] = player.FriendCode; + AUMData[Utils.GetHashPuid(player)] = player.FriendCode; + BAUDataManager.SaveCheatData(Utils.GetHashPuid(player), player.Data.FriendCode, player.Data.PlayerName, "cheatData", "AUM RPC"); + BAUDataManager.SaveCheatData(Utils.GetHashPuid(player), player.Data.FriendCode, player.Data.PlayerName, "aumData", "AUM RPC"); + } + } + } + catch { } + + return; + } + + if (callId is unchecked((byte)CustomRPC.AUMChat)) + { + try + { + var nameString = reader.ReadString(); + var msgString = reader.ReadString(); + var colorId = reader.ReadInt32(); + + Utils.AddChatPrivate($"{msgString}", overrideName: $"AUM Chat - {nameString}"); + + PlayerControl AUMPlayer = Main.AllPlayerControls.First(pc => pc.Data.PlayerName == nameString && pc.CurrentOutfit.ColorId == colorId); + + if (AUMPlayer == null || !Main.AntiCheat.Value) return; + + var flag = string.IsNullOrEmpty(nameString) && string.IsNullOrEmpty(msgString); + var flag2 = AUMData.ContainsKey(Utils.GetHashPuid(AUMPlayer)); + + if (!flag && !flag2) + { + AmongUsClient.Instance.ReportPlayer(player.GetClientId(), ReportReasons.Cheating_Hacking); + BAUNotificationManager.NotifyCheat(AUMPlayer, $"AUM", newText: "Has been detected with a cheat client"); + PlayerData[Utils.GetHashPuid(AUMPlayer)] = AUMPlayer.FriendCode; + AUMData[Utils.GetHashPuid(AUMPlayer)] = AUMPlayer.FriendCode; + BAUDataManager.SaveCheatData(Utils.GetHashPuid(player), player.Data.FriendCode, player.Data.PlayerName, "cheatData", "AUM Chat RPC"); + BAUDataManager.SaveCheatData(Utils.GetHashPuid(player), player.Data.FriendCode, player.Data.PlayerName, "aumData", "AUM Chat RPC"); + } + } + catch { } + + return; + } + } + + // Check and notify for invalid rpcs + public static void CheckRPC(PlayerControl player, byte callId, MessageReader reader) + { + try + { + if (PlayerControl.LocalPlayer == null || player == null || player == PlayerControl.LocalPlayer || reader == null || !IsEnabled || !Main.AntiCheat.Value) return; + + RoleTypes? Role = player?.Data?.RoleType; + Role ??= RoleTypes.Crewmate; + bool IsImpostor = player.IsImpostorTeam(); + bool IsCrewmate = !player.IsImpostorTeam(); + string hashPuid = Utils.GetHashPuid(player); + + if (callId is (byte)RpcCalls.EnterVent or (byte)RpcCalls.ExitVent) + { + if ((IsCrewmate && Role != RoleTypes.Engineer) || player.Data.IsDead) + BAUNotificationManager.NotifyCheat(player, $"Invalid Action RPC: {Enum.GetName((RpcCalls)callId)}"); + + return; + } + + if (callId is (byte)RpcCalls.CheckProtect or (byte)RpcCalls.ProtectPlayer) + { + if (Role is not RoleTypes.GuardianAngel) + BAUNotificationManager.NotifyCheat(player, $"Invalid Action RPC: {Enum.GetName((RpcCalls)callId)}"); + + return; + } + + if (callId is (byte)RpcCalls.CheckMurder or (byte)RpcCalls.MurderPlayer) + { + PlayerControl target = reader.ReadNetObject(); + + if (IsCrewmate || !player.IsAlive() || !target.IsAlive() || target.IsImpostorTeam()) + BAUNotificationManager.NotifyCheat(player, $"Invalid Action RPC: {Enum.GetName((RpcCalls)callId)}"); + + return; + } + + if (callId is (byte)RpcCalls.SendChat or (byte)RpcCalls.SendQuickChat) + { + if (player.IsAlive() && GameStates.InGame && !GameStates.IsLobby && !GameStates.IsMeeting && !GameStates.IsExilling && GameStates.IsInTask) + BAUNotificationManager.NotifyCheat(player, $"Invalid Action RPC: {Enum.GetName((RpcCalls)callId)}"); + + return; + } + + if (callId is (byte)RpcCalls.SetLevel) + { + if (reader.BytesRemaining > 0) + { + uint level = reader.ReadPackedUInt32(); + if (level >= 200) + { + BAUNotificationManager.NotifyCheat(player, $"Invalid Level: {level}"); + } + } + + return; + } + + if (IsCrewmate) + { + if (GameStates.IsInGamePlay) + { + if (callId is (byte)RpcCalls.CloseDoorsOfType) + { + byte Type = reader.ReadByte(); + BAUNotificationManager.NotifyCheat(player, $"Invalid Action RPC: {Enum.GetName((RpcCalls)callId)} - {Enum.GetName((SystemTypes)callId)}"); + } + if (callId is (byte)RpcCalls.UpdateSystem) + { + byte Type = reader.ReadByte(); + if (Type is (byte)SystemTypes.Reactor + or (byte)SystemTypes.Laboratory + or (byte)SystemTypes.Comms + or (byte)SystemTypes.LifeSupp + or (byte)SystemTypes.MushroomMixupSabotage + or (byte)SystemTypes.HeliSabotage) + { + BAUNotificationManager.NotifyCheat(player, $"Invalid Action RPC: {Enum.GetName((RpcCalls)callId)} - {Enum.GetName((SystemTypes)callId)}"); + } + } + } + + return; + } + + if (IsImpostor) + { + if (callId is (byte)RpcCalls.CompleteTask or (byte)RpcCalls.BootFromVent) + { + BAUNotificationManager.NotifyCheat(player, $"Invalid Action RPC: {Enum.GetName((RpcCalls)callId)}"); + } + + return; + } + + if (callId is (byte)RpcCalls.Pet or (byte)RpcCalls.CancelPet) + { + if (player?.CurrentOutfit?.PetId == "pet_EmptyPet") + { + BAUNotificationManager.NotifyCheat(player, $"Invalid Action RPC: {Enum.GetName((RpcCalls)callId)}"); + } + + return; + } + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + } + } + + // Check notify and cancel out request for invalid rpcs + public static bool CheckCancelRPC(PlayerControl player, byte callId, MessageReader reader) + { + try + { + if (PlayerControl.LocalPlayer == null || player == null || player == PlayerControl.LocalPlayer || reader == null || !IsEnabled || !Main.AntiCheat.Value) return true; + + RoleTypes Role = player.Data.RoleType; + bool IsImpostor = player.IsImpostorTeam(); + bool IsCrewmate = !player.IsImpostorTeam(); + + if (TrustedRPCs(callId) != true) + { + BAUNotificationManager.NotifyCheat(player, $"Untrusted RPC received: {callId}"); + return false; + } + + if (callId is (byte)RpcCalls.CheckMurder or (byte)RpcCalls.MurderPlayer) + { + PlayerControl target = reader.ReadNetObject(); + + if (!target.IsAlive() || target.IsImpostorTeam()) + { + BAUNotificationManager.NotifyCheat(player, $"Invalid Action RPC: {Enum.GetName((RpcCalls)callId)}"); + return false; + } + } + + if (callId is (byte)RpcCalls.CheckShapeshift or (byte)RpcCalls.Shapeshift) + { + var flag = reader.ReadBoolean(); + + if (Role is not RoleTypes.Shapeshifter || !player.IsAlive()) + { + BAUNotificationManager.NotifyCheat(player, $"Invalid Action RPC: {Enum.GetName((RpcCalls)callId)}"); + return false; + } + + if (!flag && !player.IsInVent()) + { + BAUNotificationManager.NotifyCheat(player, $"Invalid Action RPC: {Enum.GetName((RpcCalls)callId)}"); + return false; + } + } + + if (callId is (byte)RpcCalls.CheckVanish or (byte)RpcCalls.CheckAppear or (byte)RpcCalls.StartVanish or (byte)RpcCalls.StartAppear) + { + if (Role is not RoleTypes.Phantom || player.Data.IsDead) + { + BAUNotificationManager.NotifyCheat(player, $"Invalid Action RPC: {Enum.GetName((RpcCalls)callId)}"); + return false; + } + } + + if (!player.IsHost()) + { + if (callId is (byte)RpcCalls.SetTasks + or (byte)RpcCalls.ExtendLobbyTimer + or (byte)RpcCalls.CloseMeeting) + { + BAUNotificationManager.NotifyCheat(player, $"Invalid Host RPC: {Enum.GetName((RpcCalls)callId)}"); + return false; + } + } + + if (player.DataIsCollected() == true) + { + if (callId is (byte)RpcCalls.SetName or (byte)RpcCalls.CheckName or (byte)RpcCalls.SetLevel) + { + BAUNotificationManager.NotifyCheat(player, $"Invalid Set RPC: {Enum.GetName((RpcCalls)callId)}"); + return false; + } + } + + if (GameStates.IsInGamePlay) + { + if (callId is (byte)RpcCalls.SetColor + or (byte)RpcCalls.SetHat + or (byte)RpcCalls.SetSkin + or (byte)RpcCalls.SetVisor + or (byte)RpcCalls.SetPet + or (byte)RpcCalls.SetNamePlate) + { + BAUNotificationManager.NotifyCheat(player, $"Invalid Set RPC: {Enum.GetName((RpcCalls)callId)}"); + return false; + } + } + + if (GameStates.IsInGame && GameStates.IsLobby) + { + if (callId is (byte)RpcCalls.StartMeeting + or (byte)RpcCalls.ReportDeadBody + or (byte)RpcCalls.CloseMeeting + or (byte)RpcCalls.Exiled + or (byte)RpcCalls.CastVote + or (byte)RpcCalls.ClearVote + or (byte)RpcCalls.SetRole + or (byte)RpcCalls.ClimbLadder + or (byte)RpcCalls.UsePlatform + or (byte)RpcCalls.UseZipline + or (byte)RpcCalls.CompleteTask + or (byte)RpcCalls.BootFromVent + or (byte)RpcCalls.EnterVent + or (byte)RpcCalls.ExitVent + or (byte)RpcCalls.CloseDoorsOfType + or (byte)RpcCalls.MurderPlayer + or (byte)RpcCalls.CheckMurder + or (byte)RpcCalls.Shapeshift + or (byte)RpcCalls.RejectShapeshift + or (byte)RpcCalls.CheckShapeshift + or (byte)RpcCalls.CheckProtect + or (byte)RpcCalls.ProtectPlayer + or (byte)RpcCalls.StartAppear + or (byte)RpcCalls.StartVanish + or (byte)RpcCalls.CheckAppear + or (byte)RpcCalls.CheckVanish) + { + BAUNotificationManager.NotifyCheat(player, $"Invalid Lobby RPC: {Enum.GetName((RpcCalls)callId)}"); + return false; + } + } + + return true; + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + return true; + } + } + + // Check if RPC is known + private static bool TrustedRPCs(int RPCId) + { + foreach (RpcCalls rpc in Enum.GetValues(typeof(RpcCalls))) + if ((byte)rpc == RPCId || unchecked((byte)rpc) == RPCId || unchecked((byte)(short)rpc) == RPCId) + return true; + foreach (CustomRPC rpc in Enum.GetValues(typeof(CustomRPC))) + if ((byte)rpc == RPCId || unchecked((byte)rpc) == RPCId || unchecked((byte)(short)rpc) == RPCId) + return true; + + return false; + } +} diff --git a/Modules/BAUDataManager.cs b/Modules/BAUDataManager.cs new file mode 100644 index 0000000..b9d26fb --- /dev/null +++ b/Modules/BAUDataManager.cs @@ -0,0 +1,188 @@ +using System.Text.Json; + +namespace BetterAmongUs; + +class BAUDataManager +{ + public static string GetFilePath(string name) + { + return Path.Combine(Main.GetDataPathToAmongUs(), $"{name}.json"); + } + + public static void SetUp() + { + string filePath = GetFilePath("BetterData"); + + if (!File.Exists(filePath)) + { + // Initialize with predefined categories + var initialData = new Dictionary>> + { + { "Data", new Dictionary>() }, // Default category + { "cheatData", new Dictionary>() }, + { "sickoData", new Dictionary>() }, + { "aumData", new Dictionary>() } + }; + + string json = JsonSerializer.Serialize(initialData, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(filePath, json); + } + else + { + // Load the existing JSON data + string json = File.ReadAllText(filePath); + var jsonData = JsonSerializer.Deserialize>>>(json) ?? new Dictionary>>(); + + // Check and add missing categories + if (!jsonData.ContainsKey("Data")) + { + jsonData["Data"] = new Dictionary>(); + } + if (!jsonData.ContainsKey("cheatData")) + { + jsonData["cheatData"] = new Dictionary>(); + } + if (!jsonData.ContainsKey("sickoData")) + { + jsonData["sickoData"] = new Dictionary>(); + } + if (!jsonData.ContainsKey("aumData")) + { + jsonData["aumData"] = new Dictionary>(); + } + + // Write the updated JSON data back to the file + json = JsonSerializer.Serialize(jsonData, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(filePath, json); + } + } + + public static void Save(string name, string dataToSave, string category = "Data") + { + string filePath = GetFilePath("BetterData"); + + string json = File.ReadAllText(filePath); + var jsonData = JsonSerializer.Deserialize>>(json) ?? new Dictionary>(); + + if (!jsonData.ContainsKey(category)) + { + jsonData[category] = new Dictionary(); + } + + jsonData[category][name] = dataToSave; + + json = JsonSerializer.Serialize(jsonData, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(filePath, json); + } + + public static string Load(string name, string category = "Data") + { + string filePath = GetFilePath("BetterData"); + + string json = File.ReadAllText(filePath); + var jsonData = JsonSerializer.Deserialize>>(json); + + if (jsonData != null && jsonData.ContainsKey(category) && jsonData[category].ContainsKey(name)) + { + return jsonData[category][name]; + } + + return string.Empty; + } + + public static void SaveCheatData(string puid, string friendCode, string name, string category = "cheatData", string reason = "None") + { + string filePath = GetFilePath("BetterData"); + + string json = File.ReadAllText(filePath); + var jsonData = JsonSerializer.Deserialize>>>(json) ?? new Dictionary>>(); + + if (!jsonData.ContainsKey(category)) + { + jsonData[category] = new Dictionary>(); + } + + if (!jsonData[category].ContainsKey(name)) + { + jsonData[category][name] = new Dictionary(); + } + + jsonData[category][name]["FriendCode"] = friendCode; + jsonData[category][name]["HashPUID"] = puid; + jsonData[category][name]["Reason"] = reason; + + json = JsonSerializer.Serialize(jsonData, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(filePath, json); + } + + public static void ClearCheatData() + { + string filePath = GetFilePath("BetterData"); + string json = File.ReadAllText(filePath); + var jsonData = JsonSerializer.Deserialize>>>(json) + ?? new Dictionary>>(); + + // Clear in-save data + if (jsonData != null) + { + if (jsonData.ContainsKey("cheatData")) + { + jsonData["cheatData"].Clear(); + } + + if (jsonData.ContainsKey("sickoData")) + { + jsonData["sickoData"].Clear(); + } + + if (jsonData.ContainsKey("aumData")) + { + jsonData["aumData"].Clear(); + } + + json = JsonSerializer.Serialize(jsonData, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(filePath, json); + } + + // Clear in-memory data + AntiCheat.PlayerData.Clear(); + AntiCheat.SickoData.Clear(); + AntiCheat.AUMData.Clear(); + + Logger.LogCheat("Cleared cheat memory and data"); + } + + public static void LoadCheatData() + { + string filePath = GetFilePath("BetterData"); + string json = File.ReadAllText(filePath); + var jsonData = JsonSerializer.Deserialize>>>(json); + + if (jsonData != null) + { + if (jsonData.ContainsKey("cheatData")) + { + foreach (var item in jsonData["cheatData"]) + { + AntiCheat.PlayerData[item.Value["HashPUID"]] = item.Value["FriendCode"]; + } + } + + if (jsonData.ContainsKey("sickoData")) + { + foreach (var item in jsonData["sickoData"]) + { + AntiCheat.SickoData[item.Value["HashPUID"]] = item.Value["FriendCode"]; + } + } + + if (jsonData.ContainsKey("aumData")) + { + foreach (var item in jsonData["aumData"]) + { + AntiCheat.AUMData[item.Value["HashPUID"]] = item.Value["FriendCode"]; + } + } + } + } +} diff --git a/Modules/BAUNotificationManager.cs b/Modules/BAUNotificationManager.cs new file mode 100644 index 0000000..d906021 --- /dev/null +++ b/Modules/BAUNotificationManager.cs @@ -0,0 +1,102 @@ +using TMPro; +using UnityEngine; + +namespace BetterAmongUs; + +class BAUNotificationManager +{ + public static GameObject BAUNotificationManagerObj; + private static Dictionary NotifyQueue = []; + private static float showTime = 0f; + private static Camera localCamera; + private static bool Notifying = false; + + public static void Notify(string text, float Time = 5f) + { + if (BAUNotificationManagerObj != null) + { + if (Notifying) + { + if (text == BAUNotificationManagerObj.transform.Find("Sizer/ChatText (TMP)").GetComponent().text) + return; + NotifyQueue[text] = Time; + return; + } + + showTime = Time; + BAUNotificationManagerObj.SetActive(true); + BAUNotificationManagerObj.transform.Find("Sizer/ChatText (TMP)").GetComponent().text = text; + SoundManager.Instance.PlaySound(DestroyableSingleton.Instance.TaskCompleteSound, false, 1f); + Notifying = true; + } + } + + public static void NotifyCheat(PlayerControl player, string reason, string newText = "", bool kickPlayer = true) + { + string text = $"Player: {player?.CurrentOutfit.PlayerName} Has been detected doing an unauthorized action: {reason}"; + if (newText != "") + text = $"Player: {player?.CurrentOutfit.PlayerName} " + newText + $": {reason}"; + + if (!AntiCheat.PlayerData.ContainsKey(Utils.GetHashPuid(player))) + { + AntiCheat.PlayerData[Utils.GetHashPuid(player)] = player.Data.FriendCode; + BAUDataManager.SaveCheatData(Utils.GetHashPuid(player), player.Data.FriendCode, player.Data.PlayerName, "cheatData", reason); + Notify(text, Time: 8f); + } + + if (GameStates.IsHost && kickPlayer) + { + player.RpcSetName($"{player.Data.PlayerName} Has been banned by Anti-Cheat, Reason: {reason}"); + AmongUsClient.Instance.KickPlayer(player.GetClientId(), true); + } + + Logger.LogCheat(Utils.GetRawText(text)); + } + + public static void Update() + { + if (BAUNotificationManagerObj != null) + { + if (!localCamera) + { + if (DestroyableSingleton.InstanceExists) + { + localCamera = DestroyableSingleton.Instance.GetComponentInChildren(); + } + else + { + localCamera = Camera.main; + } + } + + BAUNotificationManagerObj.transform.position = AspectPosition.ComputeWorldPosition(localCamera, AspectPosition.EdgeAlignments.Bottom, new Vector3(-1.3f, 0.7f, localCamera.nearClipPlane + 0.1f)); + + showTime -= Time.deltaTime; + if (showTime <= 0f) + { + BAUNotificationManagerObj.transform.Find("Sizer/ChatText (TMP)").GetComponent().text = ""; + BAUNotificationManagerObj.SetActive(false); + Notifying = false; + + CheckNotifyQueue(); + } + + if (!GameStates.IsInGame) + { + BAUNotificationManagerObj.SetActive(false); + showTime = 0f; + } + } + } + + private static void CheckNotifyQueue() + { + if (NotifyQueue.Any()) + { + var key = NotifyQueue.Keys.First(); + var value = NotifyQueue[key]; + Notify(key, value); + NotifyQueue.Remove(key); + } + } +} diff --git a/Modules/BetterHostManager.cs b/Modules/BetterHostManager.cs new file mode 100644 index 0000000..6d20326 --- /dev/null +++ b/Modules/BetterHostManager.cs @@ -0,0 +1,99 @@ +using Hazel; +using System.Text; +using System.Text.RegularExpressions; + +namespace BetterAmongUs; + +class BetterHostManager +{ + public static bool CheckRPCAsHost(PlayerControl player, byte callId, MessageReader reader) + { + + + return true; + } + + // Set player names for better host + public static void SetPlayersInfoAsHost(PlayerControl player, bool forMeeting = false, bool force = false, bool IsBetterHost = true) + { + if (player == null || !player.DataIsCollected()) return; + + if (!Main.BetterHost.Value) + { + if (player.Data.PlayerName != player.CurrentOutfit.PlayerName && !player.IsInShapeshift() || force) + { + player.RpcSetName(player.Data.PlayerName); + Logger.Log($"Reset {player.Data.PlayerName} name", "RPC"); + } + return; + } + + if (!IsBetterHost && !force) return; + + string friendCode = player.Data.FriendCode; + + string pattern = @"^[a-zA-Z0-9#]+$"; + string hashtagPattern = @"^#[0-9]{4}$"; + string friendCodeColor = (Regex.Replace(friendCode, hashtagPattern, string.Empty).Length is > 10 or < 5 || !Regex.IsMatch(friendCode, pattern) || !Regex.IsMatch(friendCode, hashtagPattern)) ? "#00f7ff" : "#ff0000"; + + foreach (PlayerControl target in Main.AllPlayerControls) + { + if (target == null || target == PlayerControl.LocalPlayer || target.GetIsBetterUser()) continue; + + string NewName = player.CurrentOutfit.PlayerName; + string BottomTag = ""; + + StringBuilder sbTopTag = new StringBuilder(); + StringBuilder sbTopInfo = new StringBuilder(); + + if (GameStates.IsLobby) + { + NewName = player.Data.PlayerName; + + if (player.IsDev()) + sbTopTag.Append("(Dev)+++"); + + if (player == PlayerControl.LocalPlayer) + sbTopTag.Append($"Better Host+++"); + else if (player.GetIsBetterUser()) + sbTopTag.Append("Better User+++"); + + BottomTag += $"{friendCode}"; + } + else if (GameStates.IsInGame && !GameStates.IsLobby) + { + string RoleHexColor = player.IsImpostorTeam() ? "#ff1919" : "#8cffff"; + string Role = $"{player.GetRoleName()}"; + if (!player.IsImpostorTeam() && !target.IsImpostorTeam()) + { + if (target.IsAlive() && player != target) + { + Role = ""; + } + } + + sbTopTag.Append($"{Role}+++"); + } + + for (int i = 0; i < sbTopTag.ToString().Split("+++").Length; i++) + { + if (!string.IsNullOrEmpty(sbTopTag.ToString().Split("+++")[i])) + { + if (i < sbTopTag.ToString().Split("+++").Length) + { + sbTopInfo.Append(sbTopTag.ToString().Split("+++")[i]); + } + if (i != sbTopTag.ToString().Split("+++").Length - 2) + { + sbTopInfo.Append(" - "); + } + } + } + + NewName = $"{sbTopInfo}\n{NewName}\n{BottomTag}"; + + player.RpcSetNamePrivate(NewName, target); + Logger.Log($"Set {player.Data.PlayerName} name to {NewName.Replace("\n", "-")} for {target.Data.PlayerName}", "RPC"); + } + } +} diff --git a/Modules/ClientOptionItem.cs b/Modules/ClientOptionItem.cs new file mode 100644 index 0000000..52a1826 --- /dev/null +++ b/Modules/ClientOptionItem.cs @@ -0,0 +1,154 @@ +using BepInEx.Configuration; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace BetterAmongUs; + +// Code from: https://github.com/tukasa0001/TownOfHost/pull/1265 +// Code from: https://github.com/0xDrMoe/TownofHost-Enhanced +public class ClientOptionItem +{ + public ConfigEntry Config; + public ToggleButtonBehaviour ToggleButton; + + public static SpriteRenderer CustomBackground; + private static int numOptions = 0; + + private ClientOptionItem( + string name, + ConfigEntry config, + OptionsMenuBehaviour optionsMenuBehaviour, + Action additionalOnClickAction = null, + Func toggleCheck = null, + bool IsToggle = true) + { + try + { + Config = config; + + var mouseMoveToggle = optionsMenuBehaviour.DisableMouseMovement; + + // 1つ目のボタンの生成時に背景も生成 + if (CustomBackground == null) + { + numOptions = 0; + CustomBackground = Object.Instantiate(optionsMenuBehaviour.Background, optionsMenuBehaviour.transform); + CustomBackground.name = "CustomBackground"; + CustomBackground.transform.localScale = new(0.9f, 0.9f, 1f); + CustomBackground.transform.localPosition += Vector3.back * 8; + CustomBackground.gameObject.SetActive(false); + + var closeButton = Object.Instantiate(mouseMoveToggle, CustomBackground.transform); + closeButton.transform.localPosition = new(1.3f, -2.3f, -6f); + closeButton.name = "Back"; + closeButton.Text.text = "Back"; + closeButton.Background.color = Palette.DisabledGrey; + var closePassiveButton = closeButton.GetComponent(); + closePassiveButton.OnClick = new(); + closePassiveButton.OnClick.AddListener(new Action(() => + { + CustomBackground.gameObject.SetActive(false); + })); + + UiElement[] selectableButtons = optionsMenuBehaviour.ControllerSelectable.ToArray(); + PassiveButton leaveButton = null; + PassiveButton returnButton = null; + foreach (var button in selectableButtons) + { + if (button == null) continue; + + if (button.name == "LeaveGameButton") + leaveButton = button.GetComponent(); + else if (button.name == "ReturnToGameButton") + returnButton = button.GetComponent(); + } + var generalTab = mouseMoveToggle.transform.parent.parent.parent; + + var modOptionsButton = Object.Instantiate(mouseMoveToggle, generalTab); + modOptionsButton.transform.localPosition = leaveButton?.transform?.localPosition ?? new(0f, -2.4f, 1f); + modOptionsButton.name = "Better Options"; + modOptionsButton.Text.text = "Better Options"; + modOptionsButton.Background.color = new Color32(0, 150, 0, byte.MaxValue); + var modOptionsPassiveButton = modOptionsButton.GetComponent(); + modOptionsPassiveButton.OnClick = new(); + modOptionsPassiveButton.OnClick.AddListener(new Action(() => + { + CustomBackground.gameObject.SetActive(true); + })); + + if (leaveButton != null) + leaveButton.transform.localPosition = new(-1.35f, -2.411f, -1f); + if (returnButton != null) + returnButton.transform.localPosition = new(1.35f, -2.411f, -1f); + } + + // ボタン生成 + ToggleButton = Object.Instantiate(mouseMoveToggle, CustomBackground.transform); + ToggleButton.transform.localPosition = new Vector3( + // 現在のオプション数を基に位置を計算 + numOptions % 2 == 0 ? -1.3f : 1.3f, + 2.2f - (0.5f * (numOptions / 2)), + -6f); + ToggleButton.name = name; + ToggleButton.Text.text = name; + ToggleButton.Text.text += (Config != null && Config.Value) ? ": On" : ": Off"; + + var passiveButton = ToggleButton.GetComponent(); + passiveButton.OnClick = new(); + + if (IsToggle == false) + { + ToggleButton.Text.text = name; + ToggleButton.Rollover?.ChangeOutColor(new(0, 150, 0, byte.MaxValue)); + ToggleButton.Text.color = new(1f, 1f, 1f, 1f); + + passiveButton.OnClick.AddListener(new Action(() => + { + additionalOnClickAction?.Invoke(); + })); + + return; + } + + passiveButton.OnClick.AddListener(new Action(() => + { + if (toggleCheck() == false) + { + return; + } + + if (config != null) config.Value = !config.Value; + UpdateToggle(); + additionalOnClickAction?.Invoke(); + })); + UpdateToggle(); + } + finally { numOptions++; } + } + + public static ClientOptionItem Create( + string name, + ConfigEntry config, + OptionsMenuBehaviour optionsMenuBehaviour, + Action additionalOnClickAction = null, + Func toggleCheck = null, + bool IsToggle = true) + { + toggleCheck ??= () => true; + + return new ClientOptionItem(name, config, optionsMenuBehaviour, additionalOnClickAction, toggleCheck, IsToggle); + } + + public void UpdateToggle() + { + if (ToggleButton == null) return; + + var color = (Config != null && Config.Value) ? new Color32(0, 150, 0, byte.MaxValue) : new Color32(77, 77, 77, byte.MaxValue); + var textColor = (Config != null && Config.Value) ? new Color(1f, 1f, 1f, 1f) : new Color(1f, 1f, 1f, 0.5f); + ToggleButton.Background.color = color; + ToggleButton.Rollover?.ChangeOutColor(color); + ToggleButton.Text.color = textColor; + ToggleButton.Text.text = ToggleButton.name; + ToggleButton.Text.text += (Config != null && Config.Value) ? ": On" : ": Off"; + } +} \ No newline at end of file diff --git a/Modules/DebugMenu.cs b/Modules/DebugMenu.cs new file mode 100644 index 0000000..3ff28c4 --- /dev/null +++ b/Modules/DebugMenu.cs @@ -0,0 +1,235 @@ +using UnityEngine; +using Il2CppInterop.Runtime.Attributes; +using BetterAmongUs.Resources; + +namespace BetterAmongUs; + +public class DebugMenu : MonoBehaviour +{ + public static bool RevealRoles = false; + + + [HideFromIl2Cpp] + public DragWindow Window { get; } + public bool WindowEnabled { get; set; } = true; + + private List tabs = new List(); + private List tabActions = new List(); + private Dictionary> + tabContents + = new Dictionary>(); + + private string selectedTab = null; + + public void Start() + { + InitializeMenu(); + } + + private void InitializeMenu() + { + string Main = "Main"; + string HostOnly = "Host Only"; + string Misc = "Misc"; + AddTab(Main); + AddTab(HostOnly); + AddTab(Misc); + + AddButtonToTab(Main, "Reveal All Roles", () => + { + RevealRoles = !RevealRoles; + }); + + AddButtonToTab(Main, "Trigger Anti-Cheat", () => + { + if (GameStates.IsInGame || GameStates.IsLobby || GameStates.IsFreePlay) + { + BAUNotificationManager.NotifyCheat(PlayerControl.LocalPlayer, "Debug Test", kickPlayer: false); + } + }, true); + + AddButtonToTab(Main, "Clear Anti-Cheat Data", () => + { + BAUDataManager.ClearCheatData(); + }); + + AddButtonToTab(HostOnly, "Sync All Names", () => + { + RPC.SyncAllNames(); + }); + + // Automatically select the first tab + if (tabs.Count > 0) + { + selectedTab = tabs[0]; + } + + WindowEnabled = false; + } + + public void Update() + { + if (Input.GetKeyDown(KeyCode.F1)) + Toggle(); + + if (!GameStates.IsInGamePlay) + RevealRoles = false; + } + + public DebugMenu(IntPtr ptr) : base(ptr) + { + Window = new DragWindow(new Rect(20, 20, 400, 300), "Debug Menu", () => + { + GUILayout.BeginVertical(); + try + { + GUIStyle headerStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 18, + fontStyle = FontStyle.Bold, + alignment = TextAnchor.MiddleCenter, + normal = { textColor = Color.white } + }; + + GUILayout.Label("Debug Menu - Press F1 To Hide", headerStyle); + GUILayout.Space(10); + + if (tabs.Count > 0) + { + // Display tabs horizontally + GUILayout.BeginHorizontal(); + foreach (var tab in tabs) + { + // Highlight the selected tab with color change + GUI.backgroundColor = tab == selectedTab ? new Color(0.2f, 0.2f, 0.2f) : GUI.backgroundColor; + if (GUILayout.Button(tab)) + { + selectedTab = tab; + } + // Reset the background color after drawing the button + GUI.backgroundColor = Color.white; + } + GUILayout.EndHorizontal(); + + // Display content of selected tab + if (!string.IsNullOrEmpty(selectedTab) && tabContents.ContainsKey(selectedTab)) + { + var contents = tabContents[selectedTab]; + + // Display contents for selected tab + GUILayout.BeginVertical(); + int buttonCount = 0; + bool isNewRowStarted = false; + + foreach (var (isText, content, action, startNewRow, centerText, colorHex) in contents) + { + if (isText) + { + if (isNewRowStarted) + { + GUILayout.EndHorizontal(); + isNewRowStarted = false; + } + GUIStyle style = new GUIStyle(GUI.skin.label); + + // Set text color if specified + if (!string.IsNullOrEmpty(colorHex) && ColorUtility.TryParseHtmlString(colorHex, out Color color)) + { + style.normal.textColor = color; + } + + if (centerText) + { + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.Label(content, style); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + } + else + { + GUILayout.Label(content, style); + } + buttonCount = 0; // Reset button count after text + } + else + { + if (startNewRow || buttonCount % 3 == 0) + { + if (isNewRowStarted) + { + GUILayout.EndHorizontal(); // End previous row if any + } + GUILayout.BeginHorizontal(); // Start a new row + isNewRowStarted = true; + } + + if (GUILayout.Button(content)) + { + action?.Invoke(); + } + + buttonCount++; + } + } + + // End the last row if not empty + if (isNewRowStarted) + { + GUILayout.EndHorizontal(); + } + + GUILayout.EndVertical(); + } + } + } + finally + { + GUILayout.EndVertical(); + } + }); + } + + + public void AddTab(string tabName) + { + tabs.Add(tabName); + tabActions.Add(() => { /* Default action, can be replaced later */ }); + tabContents[tabName] = new List<(bool isText, string content, Action action, bool startNewRow, bool centerText, string colorHex)>(); + AddNewRowToTab(tabName); + } + + public void AddButtonToTab(string tabName, string buttonLabel, Action action, bool startNewRow = false) + { + if (tabContents.ContainsKey(tabName)) + { + tabContents[tabName].Add((false, buttonLabel, action, startNewRow, false, null)); + } + } + + public void AddTextToTab(string tabName, string text, bool centerText = false, string colorHex = null) + { + if (tabContents.ContainsKey(tabName)) + { + tabContents[tabName].Add((true, text, null, false, centerText, colorHex)); + } + } + + public void AddNewRowToTab(string tabName) + { + AddTextToTab(tabName, ""); // Adding an empty text item to create a new row + } + + public void OnGUI() + { + if (WindowEnabled) + Window.OnGUI(); + } + + public void Toggle() + { + if (!GameStates.IsDev) return; + + WindowEnabled = !WindowEnabled; + } +} diff --git a/Modules/Debugger.cs b/Modules/Debugger.cs new file mode 100644 index 0000000..0179a18 --- /dev/null +++ b/Modules/Debugger.cs @@ -0,0 +1,35 @@ + +namespace BetterAmongUs; + +class Logger +{ + public static void Log(string info, string tag = "Log") + { + string mark = $"{DateTime.Now:HH:mm} [BetterLog][{tag}]"; + string logFilePath = Path.Combine(Environment.CurrentDirectory, "better-log.txt"); + string newLine = $"{mark}: {info}"; + File.AppendAllText(logFilePath, newLine + Environment.NewLine); + Main.Logger.LogInfo($"[{tag}] {info}"); + } + public static void LogCheat(string info, string tag = "AntiCheat") => Log(info, tag); + public static void Error(string info, string tag = "Error") => Log(info, tag); + public static void Test() => Log("------------------> TEST <------------------", "TEST"); + // Log in game join msg + public static void InGame(string info) + { + if (DestroyableSingleton._instance) DestroyableSingleton.Instance.Notifier.AddDisconnectMessage(info); + Log(info, "InGame"); + } + + // ------------- Private Log ------------- + // Logs that can only be accessed when dumped + + public static void LogPrivate(string info, string tag = "Log") + { + string mark = $"{DateTime.Now:HH:mm} [BetterLog][PrivateLog][{tag}]"; + string logFilePath = Path.Combine(Environment.CurrentDirectory, "better-log.txt"); + string newLine = $"{mark}: " + Encryptor.Encrypt($"{info}"); + File.AppendAllText(logFilePath, newLine + Environment.NewLine); + } +} + diff --git a/Modules/Encryptor.cs b/Modules/Encryptor.cs new file mode 100644 index 0000000..fb1af8b --- /dev/null +++ b/Modules/Encryptor.cs @@ -0,0 +1,39 @@ +using System.Security.Cryptography; +using System.Text; + +namespace BetterAmongUs; + +class Encryptor +{ + private static readonly byte[] Key = Encoding.UTF8.GetBytes("0123456789ABCDEF0123456789ABCDEF"); + private static readonly byte[] IV = Encoding.UTF8.GetBytes("ABCDEF0123456789"); + + public static string Encrypt(string input) + { + using (Aes aes = Aes.Create()) + { + aes.Key = Key; + aes.IV = IV; + + using MemoryStream memoryStream = new(); + using CryptoStream cryptoStream = new(memoryStream, aes.CreateEncryptor(), CryptoStreamMode.Write); + using (StreamWriter streamWriter = new(cryptoStream)) + { + streamWriter.Write(input); + } + return Convert.ToBase64String(memoryStream.ToArray()); + } + } + + public static string Decrypt(string input) + { + using Aes aes = Aes.Create(); + aes.Key = Key; + aes.IV = IV; + + using MemoryStream memoryStream = new(Convert.FromBase64String(input)); + using CryptoStream cryptoStream = new(memoryStream, aes.CreateDecryptor(), CryptoStreamMode.Read); + using StreamReader streamReader = new(cryptoStream); + return streamReader.ReadToEnd(); + } +} diff --git a/Modules/EventLogManager.cs b/Modules/EventLogManager.cs new file mode 100644 index 0000000..3e86be0 --- /dev/null +++ b/Modules/EventLogManager.cs @@ -0,0 +1,41 @@ +using HarmonyLib; + +namespace BetterAmongUs; + +class EventLogManager +{ + [HarmonyPatch(typeof(PlayerControl))] + class PlayerControlPatch + { + [HarmonyPatch(nameof(PlayerControl.MurderPlayer))] + [HarmonyPostfix] + public static void MurderPlayer__Postfix(PlayerControl __instance, [HarmonyArgument(0)] PlayerControl target) => + Logger.LogPrivate($"{__instance.Data.PlayerName} Has killed {target.Data.PlayerName} as {Utils.GetRoleName(__instance.Data.RoleType)}", "EventLog"); + [HarmonyPatch(nameof(PlayerControl.Shapeshift))] + [HarmonyPostfix] + public static void Shapeshift_Postfix(PlayerControl __instance, [HarmonyArgument(0)] PlayerControl target, [HarmonyArgument(1)] bool animate) + { + if (__instance != target) + Logger.LogPrivate($"{__instance.Data.PlayerName} Has Shapeshifted into {target.Data.PlayerName}, did animate: {animate}", "EventLog"); + else + Logger.LogPrivate($"{__instance.Data.PlayerName} Has Un-Shapeshifted, did animate: {animate}", "EventLog"); + } + } + + [HarmonyPatch(typeof(PlayerPhysics))] + class PlayerPhysicsPatch + { + [HarmonyPatch(nameof(PlayerPhysics.BootFromVent))] + [HarmonyPostfix] + public static void BootFromVent_Postfix(PlayerPhysics __instance, [HarmonyArgument(0)] int ventId) => + Logger.LogPrivate($"{__instance.myPlayer.Data.PlayerName} Has been booted from vent: {ventId}, as {Utils.GetRoleName(__instance.myPlayer.Data.RoleType)}", "EventLog"); + [HarmonyPatch(nameof(PlayerPhysics.CoEnterVent))] + [HarmonyPostfix] + public static void CoEnterVent_Postfix(PlayerPhysics __instance, [HarmonyArgument(0)] int ventId) => + Logger.LogPrivate($"{__instance.myPlayer.Data.PlayerName} Has entered vent: {ventId}, as {Utils.GetRoleName(__instance.myPlayer.Data.RoleType)}", "EventLog"); + [HarmonyPatch(nameof(PlayerPhysics.CoExitVent))] + [HarmonyPostfix] + public static void CoExitVent_Postfix(PlayerPhysics __instance, [HarmonyArgument(0)] int ventId) => + Logger.LogPrivate($"{__instance.myPlayer.Data.PlayerName} Has exit vent: {ventId}, as {Utils.GetRoleName(__instance.myPlayer.Data.RoleType)}", "EventLog"); + } +} diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs new file mode 100644 index 0000000..02f249f --- /dev/null +++ b/Modules/ExtendedPlayerControl.cs @@ -0,0 +1,145 @@ +using AmongUs.GameOptions; +using InnerNet; +using TMPro; +using UnityEngine; + +namespace BetterAmongUs; + +static class ExtendedPlayerControl +{ + // Get players client + public static ClientData? GetClient(this PlayerControl player) + { + try + { + var client = AmongUsClient.Instance.allClients.ToArray().FirstOrDefault(cd => cd.Character.PlayerId == player.PlayerId); + return client; + } + catch + { + return null; + } + } + // Get players client id + public static int GetClientId(this PlayerControl player) + { + if (player == null) return -1; + var client = player.GetClient(); + return client == null ? -1 : client.Id; + } + // Set players over head text + public static void SetPlayerTextInfo(this PlayerControl player, string text, bool isBottom = false, bool isInfo = false) + { + if (player == null) return; + GameObject playerbase = player.gameObject; + string InfoType = isBottom ? "InfoText_B_TMP" : "InfoText_T_TMP"; + + if (isInfo) + { + InfoType = "InfoText_Info_TMP"; + GameObject TopText = GameObject.Find($"{playerbase.transform.name}/Names/NameText_TMP/InfoText_T_TMP"); + if (TopText != null) + { + if (TopText.GetComponent()?.text.Replace("", string.Empty).Replace("", string.Empty).Length < 1) + { + text = "" + text + ""; + } + } + } + + text = "" + text + ""; + GameObject TextObj = GameObject.Find($"{player?.gameObject.transform.name}/Names/NameText_TMP/{InfoType}"); + if (TextObj != null) + { + TextObj.GetComponent().text = text; + } + } + // Reset players over head text + public static void ResetAllPlayerTextInfo(this PlayerControl player) + { + if (player == null) return; + player.SetPlayerTextInfo("", isInfo: true); + player.SetPlayerTextInfo(""); + player.SetPlayerTextInfo("", isBottom: true); + } + // Check if players character has been created and received from the Host + public static bool DataIsCollected(this PlayerControl player) + { + if (player == null) return false; + + if (player.gameObject.transform.Find("Names/NameText_TMP").GetComponent().text + is "???" or "Player" or "Loading" or null + || player.Data == null + || string.IsNullOrEmpty(player.Data.Puid) + || player.Data.PlayerLevel == uint.MaxValue + || player.CurrentOutfit == null + || player.CurrentOutfit.ColorId == -1) + { + return false; + } + return true; + } + + // RPCs + public static void RpcSendHostChat(this PlayerControl player, string text, string title = "(System Message)", bool sendToBetterUser = true) + { + if (player == null) return; + RPC.SendHostChatToPlayer(player, text, title, sendToBetterUser); + } + public static void RpcSetNamePrivate(this PlayerControl player, string name, PlayerControl target) + { + if (player == null || target == null) return; + RPC.SetNamePrivate(player, name, target); + } + public static void RpcExile(this PlayerControl player) + { + if (player == null) return; + RPC.ExileAsync(player); + } + + public static bool IsInRoomSelect(this PlayerControl player) + { + if (player == null) return false; + return GameStates.AirshipIsActive && Vector2.Distance(player.GetTruePosition(), new(-25, 40)) < 5f; + } + + // Get vent Id that the player is in. + public static int GetPlayerVentId(this PlayerControl player) + { + if(player == null) return -1; + + if (!(ShipStatus.Instance.Systems.TryGetValue(SystemTypes.Ventilation, out var systemType) && + systemType.TryCast() is VentilationSystem ventilationSystem)) + return 0; + + return ventilationSystem.PlayersInsideVents.TryGetValue(player.PlayerId, out var playerIdVentId) ? playerIdVentId : -1; + } + + public static Vector2 GetCustomPosition(this PlayerControl player) => new(player.transform.position.x, player.transform.position.y); + + // Check if player is a dev + public static bool IsDev(this PlayerControl player) => player != null && Main.DevUser.Contains(Utils.GetHashPuid(player)); + // Check if player is alive + public static bool IsAlive(this PlayerControl player) => player?.Data != null && !player.Data.IsDead; + // Check if player is in a vent + public static bool IsInVent(this PlayerControl player) => player != null && (player.inVent || player.walkingToVent || player.MyPhysics?.Animations?.IsPlayingEnterVentAnimation() == true); + // Check if player role name + public static string GetRoleName(this PlayerControl player) => player?.Data != null && Main.GetRoleName.TryGetValue((int)player.Data.RoleType, out var roleName) ? roleName : string.Empty; + // Check if player is Shapeshifting + public static bool IsInShapeshift(this PlayerControl player) => player != null && (player.shapeshiftTargetPlayerId > -1 || player.shapeshifting); + // Check if player is on imposter team + public static bool IsImpostorTeam(this PlayerControl player) => player?.Data != null && (player.Data.RoleType is RoleTypes.Impostor or RoleTypes.ImpostorGhost or RoleTypes.Shapeshifter or RoleTypes.Phantom); + // Check if player is a imposter teammate + public static bool IsImpostorTeammate(this PlayerControl player) => + player != null && PlayerControl.LocalPlayer != null && + ((player == PlayerControl.LocalPlayer && PlayerControl.LocalPlayer.IsImpostorTeam()) || + (PlayerControl.LocalPlayer.IsImpostorTeam() && player.IsImpostorTeam())); + // Check if player is in the Anti-Cheat list + public static bool IsCheater(this PlayerControl player) => + player != null && (AntiCheat.PlayerData.ContainsKey(Utils.GetHashPuid(player)) || + AntiCheat.SickoData.ContainsKey(Utils.GetHashPuid(player)) || + AntiCheat.AUMData.ContainsKey(Utils.GetHashPuid(player))); + // Check if player is the host + public static bool IsHost(this PlayerControl player) => player?.Data != null && GameData.Instance?.GetHost()?.Puid == player.Data.Puid; + +} diff --git a/Modules/ExtendedPlayerInfo.cs b/Modules/ExtendedPlayerInfo.cs new file mode 100644 index 0000000..b2d5932 --- /dev/null +++ b/Modules/ExtendedPlayerInfo.cs @@ -0,0 +1,21 @@ + +namespace BetterAmongUs; + +static class ExtendedPlayerInfo +{ + private static Dictionary IsBetterUser = new Dictionary(); + + public static bool GetIsBetterUser(this PlayerControl player) + { + if (IsBetterUser.ContainsKey(player)) + return IsBetterUser[player]; + else + return false; + } + + public static void SetIsBetterUser(this PlayerControl player, bool value) + { + IsBetterUser[player] = value; + } +} + diff --git a/Modules/FileChecker.cs b/Modules/FileChecker.cs new file mode 100644 index 0000000..e5690b5 --- /dev/null +++ b/Modules/FileChecker.cs @@ -0,0 +1,188 @@ +using System.Collections.ObjectModel; +using UnityEngine; + +namespace BetterAmongUs; + +class FileChecker +{ + private static bool Enabled = true; + private static readonly ReadOnlyCollection UnsupportedBepInExMods = new(new List { "TOHE", "YuAntiCheat" }); // Put BepInEx BepInPlugin name, not dll name here lol. + private static readonly ReadOnlyCollection BannedBepInExMods = new(new List { "MalumMenu", "MalumMenu-Yu", "MalumMenuYu", "AUnlocker" }); // Put BepInEx BepInPlugin name, not dll name here lol. + private static readonly ReadOnlyCollection KeyWordsInVersionInfo = new(new List { "Malum", "Sicko", "AUM" }); // Banned words for version text + public static string UnauthorizedReason = string.Empty; + public static List CheatTags = []; // For API report + public static bool HasTrySpoofFriendCode = false; + public static bool HasUnauthorizedFile = false; + public static bool HasShownPopUp = false; + + // Set up if unauthorized files have been found. + public static void UpdateUnauthorizedFiles() + { + if (Enabled == false) return; + + GameObject PlayButton = GameObject.Find("Main Buttons/PlayButton"); + + // Disable play button + if (PlayButton != null) + { + if (EOSManager.Instance.userId == null || HasTrySpoofFriendCode) + { + PlayButton.GetComponent().enabled = false; + GameObject.Find("Main Buttons/PlayButton/Inactive").GetComponent().color = Color.gray; + } + else + { + PlayButton.GetComponent().enabled = true; + GameObject.Find("Main Buttons/PlayButton/Inactive").GetComponent().color = new Color(1.0f, 1.0f, 1.0f); + } + } + + // Unauthorized file or ban detected. + if (HasUnauthorizedFile) + { + GameObject playOnlineButton = GameObject.Find("PlayOnlineButton"); + + if (playOnlineButton != null) + { + PassiveButton PassiveButtonComponent = playOnlineButton.GetComponent(); + PlayOnlineButtonSprite PlayOnlineButtonSpriteComponent = playOnlineButton.GetComponent(); + + if (PassiveButtonComponent != null) + { + if (PassiveButtonComponent != null) + PassiveButtonComponent.enabled = false; + + PlayOnlineButtonSpriteComponent?.SetGreyscale(); + } + } + + GameObject SignInStatus = GameObject.Find("SignInStatus"); + + if (SignInStatus != null) + { + SignInStatusComponent SignInStatusCom = SignInStatus.GetComponent(); + SignInStatusCom?.SetOffline(); + + GameObject.Find("Account_CTA")?.SetActive(false); + GameObject.Find("AccountTab/GameHeader/LeftSide/FriendCode")?.SetActive(false); + if (GameObject.Find("Stats_CTA") != null) GameObject.Find("Stats_CTA").transform.position = new Vector2(1.7741f, -0.2442f); + } + + SoundManager.instance?.ChangeMusicVolume(0); + return; + } + + if (EOSManager.Instance.editAccountUsername.gameObject.active || EOSManager.Instance.askToMergeAccount.gameObject.active) + { + HasTrySpoofFriendCode = true; + } + } + + private static readonly string UnauthorizedTextDetectedMsg = "Unauthorized {0} Detected"; + private static readonly string UnauthorizedFileMsg = "Unauthorized Game Files Detected"; + private static readonly string OnlineMsg = "Online access has been temporarily disabled!"; + private static readonly string UnsupportedBepInExModMsg = "Unsupported BepInEx Mod Detected"; + private static readonly string BannedBepInExModMsg = "Unauthorized BepInEx Mod Detected"; + + // Check if there's any unauthorized files. + public static bool CheckIfUnauthorizedFiles() + { + if (Enabled == false) return false; + + // Get user info for later use with API. + string ClientUserName = GameObject.Find("AccountTab")?.GetComponent()?.userName.text; + string ClientFriendCode = EOSManager.Instance.friendCode; + string ClientPUIDHash = Utils.GetHashPuid(EOSManager.Instance.ProductUserId); + + if (GameStates.IsDev) + { + Enabled = false; + return false; + } + + // Check for Banned BepInEx Mods + foreach (var bannedMod in BannedBepInExMods) + { + if (IsBepInExModLoaded(bannedMod)) + { + if (!HasUnauthorizedFile) UnauthorizedReason = BannedBepInExModMsg; + if (!CheatTags.Contains($"{bannedMod}-BepInEx")) CheatTags.Add($"{bannedMod}-BepInEx"); + HasUnauthorizedFile = true; + } + } + + // Check for Unsupported BepInEx Mods + foreach (var unsupportedMod in UnsupportedBepInExMods) + { + if (IsBepInExModLoaded(unsupportedMod)) + { + if (!HasUnauthorizedFile) UnauthorizedReason = UnsupportedBepInExModMsg; + HasUnauthorizedFile = true; + } + } + + // Check for version.dll + if (File.Exists(Path.Combine(Environment.CurrentDirectory, "version.dll"))) + { + string versiondll = "'version.dll'"; + if (!HasUnauthorizedFile) UnauthorizedReason = string.Format(UnauthorizedTextDetectedMsg, versiondll); + if (!CheatTags.Contains("version.dll")) CheatTags.Add("version.dll"); + HasUnauthorizedFile = true; + } + + // Check for banned words in VersionInfo display. Aka check cheat developers ego + foreach (var WordInVersionInfo in KeyWordsInVersionInfo) + { + if (UnityEngine.Object.FindFirstObjectByType().text.text.ToLower().Contains(WordInVersionInfo.ToLower())) + { + if (!HasUnauthorizedFile) UnauthorizedReason = UnauthorizedFileMsg; + if (!CheatTags.Contains($"{WordInVersionInfo}-VersionInfo")) CheatTags.Add($"{WordInVersionInfo}-VersionInfo"); + HasUnauthorizedFile = true; + } + } + + // Check for Sicko leftover files + if (File.Exists(Path.Combine(Environment.CurrentDirectory, "sicko-settings.json")) || + File.Exists(Path.Combine(Environment.CurrentDirectory, "sicko-log.txt")) || + File.Exists(Path.Combine(Environment.CurrentDirectory, "sicko-prev-log.txt")) || + File.Exists(Path.Combine(Environment.CurrentDirectory, "sicko-config"))) + { + if (!HasUnauthorizedFile) UnauthorizedReason = UnauthorizedFileMsg; + if (!CheatTags.Contains("Sicko-Menu-Files")) CheatTags.Add("Sicko-Menu-Files"); + HasUnauthorizedFile = true; + } + + // Check for AUM leftover files + if (File.Exists(Path.Combine(Environment.CurrentDirectory, "settings.json")) || + File.Exists(Path.Combine(Environment.CurrentDirectory, "aum-log.txt")) || + File.Exists(Path.Combine(Environment.CurrentDirectory, "aum-prev-log.txt"))) + { + if (!HasUnauthorizedFile) UnauthorizedReason = UnauthorizedFileMsg; + if (!CheatTags.Contains("AUM-Menu-Files")) CheatTags.Add("AUM-Menu-Files"); + HasUnauthorizedFile = true; + } + + // ----------- Unused Until API support is added! ----------- + + // Combine Player information and Tags + string tagsAsString = string.Join(" - ", CheatTags); + string playerInfo = $"{ClientUserName}.{ClientFriendCode}.{ClientPUIDHash} - {tagsAsString}"; // If Detection goes off send this information to the API database! + + // ---------------------------------------------------------- + + return HasUnauthorizedFile; + } + + // Get all loaded BepInEx mods and check if one is on the ban list. + private static bool IsBepInExModLoaded(string modName) + { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (assembly.FullName.Contains(modName)) + { + return true; + } + } + return false; + } +} diff --git a/Modules/GameState.cs b/Modules/GameState.cs new file mode 100644 index 0000000..c88d014 --- /dev/null +++ b/Modules/GameState.cs @@ -0,0 +1,51 @@ +using AmongUs.GameOptions; +using UnityEngine; + +namespace BetterAmongUs; + +// Code from: https://github.com/0xDrMoe/TownofHost-Enhanced + +public static class GameStates +{ + /**********Check Game Status***********/ + public static bool IsDev => Main.DevUser.Contains(Utils.GetHashPuid(EOSManager.Instance.ProductUserId)); + public static bool InGame => Main.AllPlayerControls.Any(); + public static bool IsNormalGame => GameOptionsManager.Instance.CurrentGameOptions.GameMode is GameModes.Normal or GameModes.NormalFools; + public static bool IsHideNSeek => GameOptionsManager.Instance.CurrentGameOptions.GameMode is GameModes.HideNSeek or GameModes.SeekFools; + public static bool SkeldIsActive => (MapNames)GameOptionsManager.Instance.CurrentGameOptions.MapId == MapNames.Skeld; + public static bool MiraHQIsActive => (MapNames)GameOptionsManager.Instance.CurrentGameOptions.MapId == MapNames.Mira; + public static bool PolusIsActive => (MapNames)GameOptionsManager.Instance.CurrentGameOptions.MapId == MapNames.Polus; + public static bool DleksIsActive => (MapNames)GameOptionsManager.Instance.CurrentGameOptions.MapId == MapNames.Dleks; + public static bool AirshipIsActive => (MapNames)GameOptionsManager.Instance.CurrentGameOptions.MapId == MapNames.Airship; + public static bool FungleIsActive => (MapNames)GameOptionsManager.Instance.CurrentGameOptions.MapId == MapNames.Fungle; + public static bool IsInGame => InGame; + public static bool IsLobby => AmongUsClient.Instance?.GameState == InnerNet.InnerNetClient.GameStates.Joined; + public static bool IsInIntro => IntroCutscene.Instance != null; + public static bool IsInGamePlay => ((InGame && !IsLobby && !IsInIntro) || IsFreePlay); + public static bool IsEnded => AmongUsClient.Instance?.GameState == InnerNet.InnerNetClient.GameStates.Ended; + public static bool IsNotJoined => AmongUsClient.Instance?.GameState == InnerNet.InnerNetClient.GameStates.NotJoined; + public static bool IsOnlineGame => AmongUsClient.Instance?.NetworkMode == NetworkModes.OnlineGame; + public static bool IsVanillaServer + { + get + { + if (!IsOnlineGame) return false; + + string region = ServerManager.Instance.CurrentRegion.Name; + return (region == "North America" || region == "Europe" || region == "Asia"); + } + } + public static bool IsLocalGame => AmongUsClient.Instance?.NetworkMode == NetworkModes.LocalGame; + public static bool IsFreePlay => AmongUsClient.Instance?.NetworkMode == NetworkModes.FreePlay; + public static bool IsInTask => InGame && MeetingHud.Instance == null; + public static bool IsMeeting => InGame && MeetingHud.Instance != null; + public static bool IsVoting => IsMeeting && MeetingHud.Instance?.state is MeetingHud.VoteStates.Voted or MeetingHud.VoteStates.NotVoted; + public static bool IsProceeding => IsMeeting && MeetingHud.Instance?.state is MeetingHud.VoteStates.Proceeding; + public static bool IsExilling => ExileController.Instance != null && !(AirshipIsActive && Minigame.Instance != null && Minigame.Instance.isActiveAndEnabled); + public static bool IsCountDown => GameStartManager.InstanceExists && GameStartManager.Instance.startState == GameStartManager.StartingStates.Countdown; + /**********TOP ZOOM.cs***********/ + public static bool IsShip => ShipStatus.Instance != null; + public static bool IsHost => AmongUsClient.Instance.AmHost; + public static bool IsCanMove => PlayerControl.LocalPlayer?.CanMove is true; + public static bool IsDead => PlayerControl.LocalPlayer?.Data?.IsDead is true; +} diff --git a/Modules/LateTask.cs b/Modules/LateTask.cs new file mode 100644 index 0000000..7321d00 --- /dev/null +++ b/Modules/LateTask.cs @@ -0,0 +1,55 @@ +// Code from: https://github.com/0xDrMoe/TownofHost-Enhanced + +using System.Linq; + +namespace BetterAmongUs; + +class LateTask +{ + public string name; + public float timer; + public bool shouldLog; + public Action action; + public static List Tasks = []; + public bool Run(float deltaTime) + { + timer -= deltaTime; + if (timer <= 0) + { + action(); + + if (shouldLog == true) + Logger.Log($"{name} has finished", "LateTask"); + return true; + } + return false; + } + public LateTask(Action action, float time, string name = "No Name Task", bool shoudLog = true) + { + this.action = action; + this.timer = time; + this.name = name; + this.shouldLog = shoudLog; + Tasks.Add(this); + } + public static void Update(float deltaTime) + { + var TasksToRemove = new List(); + foreach (var task in Tasks.ToArray()) + { + try + { + if (task.Run(deltaTime)) + { + TasksToRemove.Add(task); + } + } + catch (Exception ex) + { + TasksToRemove.Add(task); + Logger.Error(ex.ToString()); + } + } + TasksToRemove.ForEach(task => Tasks.Remove(task)); + } +} \ No newline at end of file diff --git a/Modules/RPC.cs b/Modules/RPC.cs new file mode 100644 index 0000000..c7541fd --- /dev/null +++ b/Modules/RPC.cs @@ -0,0 +1,161 @@ +using BetterAmongUs.Patches; +using HarmonyLib; +using Hazel; +using static UnityEngine.GraphicsBuffer; + +namespace BetterAmongUs; + +enum CustomRPC : int +{ + // Cheat RPC's + Sicko = 420, + AUM = 42069, + AUMChat = 101, + + // TOHE + VersionCheck = 80, + RequestRetryVersionCheck = 81, + + //Better Among Us + BetterCheck = 150, + AddChat, +} + +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))] +internal class RPCHandlerPatch +{ + public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] byte callId, [HarmonyArgument(1)] MessageReader reader) + { + AntiCheat.HandleRPCBeforeCheck(__instance, callId, reader); + + if (AntiCheat.CheckCancelRPC(__instance, callId, reader) != true) + { + Logger.LogCheat($"RPC canceled by Anti-Cheat: {Enum.GetName((RpcCalls)callId)}{Enum.GetName((CustomRPC)callId)} - {callId}"); + return false; + } + + if (GameStates.IsHost && BetterHostManager.CheckRPCAsHost(__instance, callId, reader) != true) + { + Logger.LogCheat($"RPC canceled by Host: {Enum.GetName((RpcCalls)callId)}{Enum.GetName((CustomRPC)callId)} - {callId}"); + return false; + } + + return true; + } + public static void Postfix(PlayerControl __instance, [HarmonyArgument(0)] byte callId, [HarmonyArgument(1)] MessageReader reader) + { + AntiCheat.CheckRPC(__instance, callId, reader); + RPC.HandleRPC(__instance, callId, reader); + } +} + +internal static class RPC +{ + public static void SetNamePrivate(PlayerControl player, string name, PlayerControl target) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(player.NetId, (byte)RpcCalls.SetName, SendOption.Reliable, target.GetClientId()); + writer.Write(player.NetId); + writer.Write(name); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + + public static void SyncAllNames(bool isForMeeting = false, bool force = false, bool isBetterHost = true) + { + if (!GameStates.IsHost) return; + + foreach (PlayerControl player in Main.AllPlayerControls) + BetterHostManager.SetPlayersInfoAsHost(player, isForMeeting, force, isBetterHost); + } + + public static void ExileAsync(PlayerControl player) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(player.NetId, (byte)RpcCalls.Exiled, SendOption.Reliable, -1); + AmongUsClient.Instance.FinishRpcImmediately(writer); + player.Exiled(); + } + + public static async Task WaitForChatCooldown() + { + while (3f - HudManager.Instance.Chat.timeSinceLastMessage > 0f) + { + await Task.Delay(100); + } + } + + public static async void SendHostChatToPlayer(PlayerControl player, string text, string? title = "", bool sendToBetterUser = true) + { + if (!GameStates.IsHost) return; + + if (player.GetIsBetterUser()) + { + if (sendToBetterUser) + { + MessageWriter messageWriter2 = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, unchecked((byte)CustomRPC.AddChat), SendOption.None, player.GetClientId()); + messageWriter2.Write(text); + messageWriter2.Write(title); + AmongUsClient.Instance.FinishRpcImmediately(messageWriter2); + } + + return; + } + + return; + + await WaitForChatCooldown(); + + PlayerControl asPlayer = Main.AllPlayerControls.Where(pc => pc.IsAlive()).OrderBy(pc => pc == PlayerControl.LocalPlayer ? 0 : 1).First(); + + var oldName = asPlayer.CurrentOutfit.PlayerName; + if (title == "") + title = oldName; + + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(asPlayer.NetId, (byte)RpcCalls.SetName, SendOption.None, player.GetClientId()); + writer.Write(asPlayer.Data.NetId); + writer.Write(title); + AmongUsClient.Instance.FinishRpcImmediately(writer); + + MessageWriter messageWriter = AmongUsClient.Instance.StartRpcImmediately(asPlayer.NetId, (byte)RpcCalls.SendChat, SendOption.None, player.GetClientId()); + messageWriter.Write(text); + AmongUsClient.Instance.FinishRpcImmediately(messageWriter); + + MessageWriter writer2 = AmongUsClient.Instance.StartRpcImmediately(asPlayer.NetId, (byte)RpcCalls.SetName, SendOption.None, player.GetClientId()); + writer2.Write(asPlayer.Data.NetId); + writer2.Write(oldName); + AmongUsClient.Instance.FinishRpcImmediately(writer2); + + SyncAllNames(); + } + + public static void HandleRPC(PlayerControl player, byte callId, MessageReader reader) + { + if (PlayerControl.LocalPlayer == null || player == null || player == PlayerControl.LocalPlayer || player.Data == null) return; + + switch (callId) + { + case (byte)CustomRPC.BetterCheck: + if (reader.ReadByte() == player.NetId) + player.SetIsBetterUser(true); + break; + case (byte)CustomRPC.AddChat: + Utils.AddChatPrivate(reader.ReadString(), reader.ReadString()); + break; + case (byte)CustomRPC.VersionCheck: + if (player.IsHost()) + { + var BAU = "♻BetterAmongUs♻"; + Utils.DisconnectSelf($"{BAU} does not support TOHE"); + } + break; + case (byte)RpcCalls.SendChat: + if (player.IsHost() && player != PlayerControl.LocalPlayer) + { + if (reader.ReadString().ToLower() == "/allow") + { + CommandsPatch.Permission = player; + } + } + break; + } + } +} + diff --git a/Modules/Utils.cs b/Modules/Utils.cs new file mode 100644 index 0000000..9b8a98b --- /dev/null +++ b/Modules/Utils.cs @@ -0,0 +1,229 @@ +using AmongUs.GameOptions; +using InnerNet; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using UnityEngine; + +namespace BetterAmongUs; + +public static class Utils +{ + // Get player by client id + public static ClientData? ClientFromId(int id) => AmongUsClient.Instance.allClients.ToArray().FirstOrDefault(cd => cd.Id == id) ?? null; + // Get player from player id + public static PlayerControl? PlayerFromId(int playerId) => Main.AllPlayerControls.FirstOrDefault(player => player.PlayerId == playerId) ?? null; + // Get player from client id + public static PlayerControl? PlayerFromClientId(int clientId) => Main.AllPlayerControls.FirstOrDefault(player => player.GetClientId() == clientId) ?? null; + // Get player from net id + public static PlayerControl? PlayerFromNetId(uint netId) => Main.AllPlayerControls.FirstOrDefault(player => player.NetId == netId) ?? null; + // Add msg to chat + public static void AddChatPrivate(string text, string overrideName = "", bool setRight = false) + { + ChatController chat = HudManager.Instance.Chat; + NetworkedPlayerInfo data = PlayerControl.LocalPlayer.Data; + ChatBubble pooledBubble = chat.GetPooledBubble(); + string MsgName = "(System Message)"; + if (overrideName != "") + MsgName = overrideName; + try + { + pooledBubble.transform.SetParent(chat.scroller.Inner); + pooledBubble.transform.localScale = Vector3.one; + pooledBubble.Background.color = new Color(0.05f, 0.05f, 0.05f, 1f); + pooledBubble.SetCosmetics(data); + pooledBubble.gameObject.transform.Find("PoolablePlayer").gameObject.SetActive(false); + pooledBubble.ColorBlindName.gameObject.SetActive(false); + if (!setRight) + { + pooledBubble.SetLeft(); + pooledBubble.gameObject.transform.Find("NameText (TMP)").transform.localPosition += new Vector3(-0.7f, 0f); + pooledBubble.gameObject.transform.Find("ChatText (TMP)").transform.localPosition += new Vector3(-0.7f, 0f); + } + else + { + pooledBubble.SetRight(); + } + chat.SetChatBubbleName(pooledBubble, data, data.IsDead, false, PlayerNameColor.Get(data), null); + pooledBubble.SetText(text); + pooledBubble.AlignChildren(); + chat.AlignAllBubbles(); + pooledBubble.NameText.text = MsgName; + if (!chat.IsOpenOrOpening && chat.notificationRoutine == null) + { + chat.notificationRoutine = chat.StartCoroutine(chat.BounceDot()); + } + SoundManager.Instance.PlaySound(chat.messageSound, false, 1f, null).pitch = 0.5f + (float)data.PlayerId / 15f; + } + catch (Exception ex) + { + chat.chatBubblePool.Reclaim(pooledBubble); + Logger.Error(ex.ToString()); + throw; + } + } + // Get players HashPuid + public static string GetHashPuid(PlayerControl player) + { + if (player?.Data?.Puid == null) return ""; + + string puid = player.Data.Puid; + + using SHA256 sha256 = SHA256.Create(); + byte[] sha256Bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(puid)); + string sha256Hash = BitConverter.ToString(sha256Bytes).Replace("-", "").ToLower(); + return sha256Hash.Substring(0, 5) + sha256Hash.Substring(sha256Hash.Length - 4); + } + // Get HashPuid from puid + public static string GetHashPuid(string puid) + { + using SHA256 sha256 = SHA256.Create(); + byte[] sha256Bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(puid)); + string sha256Hash = BitConverter.ToString(sha256Bytes).Replace("-", "").ToLower(); + return sha256Hash.Substring(0, 5) + sha256Hash.Substring(sha256Hash.Length - 4); + } + // Remove Html Tags Template + public static string RemoveHtmlTagsTemplate(string str) => Regex.Replace(str, "", ""); + // Get raw text + public static string GetRawText(string text) + { + text = Regex.Replace(text, "<[^>]*>", ""); + text = Regex.Replace(text, "{[^}]*}", ""); + return text; + } + // Get name for role + public static string GetRoleName(RoleTypes role) => Main.GetRoleName[(int)role]; + public static string Color32ToHex(Color32 color) => $"#{color.r:X2}{color.g:X2}{color.b:X2}{255:X2}"; + // Disconnect client + public static void DisconnectSelf(string reason) + { + AmongUsClient.Instance.ExitGame(0); + _ = new LateTask(() => + { + SceneChanger.ChangeScene("MainMenu"); + _ = new LateTask(() => + { + var lines = "----------------------------------------------------------------------------------------------"; + DisconnectPopup.Instance._textArea.enableWordWrapping = false; + DisconnectPopup.Instance.ShowCustom($"{lines}\n\n\n{reason}\n\n\n{lines}"); + }, 0.1f, "DisconnectSelf 2"); + }, 0.2f, "DisconnectSelf 1"); + } + + public static Dictionary CachedSprites = []; + + public static Sprite? LoadSprite(string path, float pixelsPerUnit = 1f) + { + try + { + if (CachedSprites.TryGetValue(path + pixelsPerUnit, out var sprite)) + return sprite; + + var texture = LoadTextureFromResources(path); + if (texture == null) + return null; + + sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), pixelsPerUnit); + sprite.hideFlags |= HideFlags.HideAndDontSave | HideFlags.DontSaveInEditor; + + Logger.Log($"Successfully loaded sprite from {path}"); + return CachedSprites[path + pixelsPerUnit] = sprite; + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + return null; + } + } + + public static Texture2D? LoadTextureFromResources(string path) + { + try + { + var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(path); + if (stream == null) + return null; + + var texture = new Texture2D(1, 1, TextureFormat.ARGB32, false); + using (var ms = new MemoryStream()) + { + stream.CopyTo(ms); + if (!ImageConversion.LoadImage(texture, ms.ToArray(), false)) + return null; + } + + Logger.Log($"Successfully loaded texture from {path}"); + return texture; + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + return null; + } + } + + // Get platform name + public static string GetPlatformName(PlayerControl player, bool useTag = false) + { + if (player == null) return string.Empty; + if (player.GetClient() == null) return string.Empty; + + string PlatformName = string.Empty; + string Tag = string.Empty; + + Platforms platform = player.GetClient().PlatformData.Platform; + + switch (platform) + { + case Platforms.StandaloneSteamPC: + PlatformName = "Steam"; + Tag = "PC"; + break; + case Platforms.StandaloneEpicPC: + PlatformName = "Epic Games"; + Tag = "PC"; + break; + case Platforms.StandaloneWin10: + PlatformName = "Microsoft Store"; + Tag = "PC"; + break; + case Platforms.StandaloneMac: + PlatformName = "Mac OS"; + Tag = "PC"; + break; + case Platforms.StandaloneItch: + PlatformName = "Itch.io"; + Tag = "PC"; + break; + case Platforms.Xbox: + PlatformName = "Xbox"; + Tag = "Console"; + break; + case Platforms.Playstation: + PlatformName = "Playstation"; + Tag = "Console"; + break; + case Platforms.Switch: + PlatformName = "Switch"; + Tag = "Console"; + break; + case Platforms.Android: + PlatformName = "Android"; + Tag = "Mobile"; + break; + case Platforms.IPhone: + PlatformName = "IPhone"; + Tag = "Mobile"; + break; + case Platforms.Unknown: + PlatformName = "None"; + break; + } + + if (useTag == false) + return PlatformName; + + return $"{Tag}: {PlatformName}"; + } +} \ No newline at end of file diff --git a/Modules/Zoom.cs b/Modules/Zoom.cs new file mode 100644 index 0000000..e3bab1c --- /dev/null +++ b/Modules/Zoom.cs @@ -0,0 +1,96 @@ +using HarmonyLib; +using UnityEngine; + +namespace BetterAmongUs; + +// Code from: https://github.com/0xDrMoe/TownofHost-Enhanced +[HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))] +public static class Zoom +{ + private static bool resetButtons = false; + + public static void Postfix() + { + if ((GameStates.IsShip && !GameStates.IsMeeting && GameStates.IsCanMove && PlayerControl.LocalPlayer.Data.IsDead) || + (GameStates.IsLobby && GameStates.IsCanMove)) + { + if (Camera.main.orthographicSize > 3.0f) + resetButtons = true; + + if (Input.mouseScrollDelta.y > 0 && Camera.main.orthographicSize > 3.0f) + { + SetZoomSize(times: false); + } + else if (Input.mouseScrollDelta.y < 0 && (GameStates.IsDead || GameStates.IsFreePlay || GameStates.IsLobby) && + Camera.main.orthographicSize < 18.0f) + { + SetZoomSize(times: true); + } + + Flag.NewFlag("Zoom"); + } + else + { + Flag.Run(() => SetZoomSize(reset: true), "Zoom"); + } + } + + private static void SetZoomSize(bool times = false, bool reset = false) + { + float size = times ? 1.5f : 1 / 1.5f; + + if (reset) + { + Camera.main.orthographicSize = 3.0f; + HudManager.Instance.UICamera.orthographicSize = 3.0f; + HudManager.Instance.Chat.transform.localScale = Vector3.one; + if (GameStates.IsMeeting) + MeetingHud.Instance.transform.localScale = Vector3.one; + } + else + { + Camera.main.orthographicSize *= size; + HudManager.Instance.UICamera.orthographicSize *= size; + } + + DestroyableSingleton.Instance?.ShadowQuad?.gameObject?.SetActive((reset || Camera.main.orthographicSize == 3.0f) && PlayerControl.LocalPlayer.IsAlive()); + + if (resetButtons) + { + ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen); + resetButtons = false; + } + } + + public static void OnFixedUpdate() => + DestroyableSingleton.Instance?.ShadowQuad?.gameObject?.SetActive((Camera.main.orthographicSize == 3.0f) && PlayerControl.LocalPlayer.IsAlive()); +} + +public static class Flag +{ + private static readonly List oneTimeList = new(); + private static readonly List firstRunList = new(); + + public static void Run(Action action, string type, bool firstrun = false) + { + if (oneTimeList.Contains(type) || (firstrun && !firstRunList.Contains(type))) + { + if (!firstRunList.Contains(type)) + firstRunList.Add(type); + + oneTimeList.Remove(type); + action(); + } + } + + public static void NewFlag(string type) + { + if (!oneTimeList.Contains(type)) + oneTimeList.Add(type); + } + + public static void DeleteFlag(string type) + { + oneTimeList.Remove(type); + } +} \ No newline at end of file diff --git a/Patches/ButtonsPatch.cs b/Patches/ButtonsPatch.cs new file mode 100644 index 0000000..9a5bbda --- /dev/null +++ b/Patches/ButtonsPatch.cs @@ -0,0 +1,48 @@ +using BetterAmongUs.Role; +using HarmonyLib; +using UnityEngine; + +namespace BetterAmongUs.Patches; + +internal class ButtonsPatch +{ + // Handle EFC + [HarmonyPatch(typeof(SabotageButton))] + internal class SabotageButtonPatch + { + [HarmonyPatch(nameof(SabotageButton.DoClick))] + [HarmonyPrefix] + public static bool DoClick_Prefix(SabotageButton __instance) + { + if (PlayerControl.LocalPlayer.Data.Role.IsImpostor && GameManager.Instance.SabotagesEnabled()) + { + DestroyableSingleton.Instance.ToggleMapVisible(new MapOptions + { + Mode = MapOptions.Modes.Sabotage + }); + } + + return false; + } + [HarmonyPatch(nameof(SabotageButton.Refresh))] + [HarmonyPrefix] + public static bool Refresh_Prefix(SabotageButton __instance) + { + if (GameManager.Instance == null || PlayerControl.LocalPlayer == null) + { + __instance.ToggleVisible(false); + __instance.SetDisabled(); + return false; + } + if (!GameManager.Instance.SabotagesEnabled() || PlayerControl.LocalPlayer.petting) + { + __instance.ToggleVisible(PlayerControl.LocalPlayer.Data.Role.IsImpostor && GameManager.Instance.SabotagesEnabled()); + __instance.SetDisabled(); + return false; + } + __instance.SetEnabled(); + + return false; + } + } +} diff --git a/Patches/ChatNotificationPatch.cs b/Patches/ChatNotificationPatch.cs new file mode 100644 index 0000000..e6fb7fa --- /dev/null +++ b/Patches/ChatNotificationPatch.cs @@ -0,0 +1,40 @@ +using HarmonyLib; +using TMPro; +using UnityEngine; + +namespace BetterAmongUs.Patches; + +[HarmonyPatch(typeof(ChatNotification))] +public class ChatNotificationPatch +{ + [HarmonyPatch(nameof(ChatNotification.Awake))] + [HarmonyPostfix] + public static void Awake_Postfix(ChatNotification __instance) + { + if (__instance.gameObject.name == "ChatNotification" && BAUNotificationManager.BAUNotificationManagerObj == null) + { + GameObject ChatNotifications = __instance.gameObject; + if (ChatNotifications != null) + { + GameObject BAUNotification = UnityEngine.Object.Instantiate(ChatNotifications); + BAUNotification.name = "BAUNotification"; + UnityEngine.Object.Destroy(BAUNotification.GetComponent()); + UnityEngine.Object.Destroy(GameObject.Find($"{BAUNotification.name}/Sizer/PoolablePlayer")); + UnityEngine.Object.Destroy(GameObject.Find($"{BAUNotification.name}/Sizer/ColorText")); + BAUNotification.GetComponent().DistanceFromEdge = new Vector3(-1.57f, 5.3f, -15f); + GameObject.Find($"{BAUNotification.name}/Sizer/NameText").transform.localPosition = new Vector3(-3.3192f, -0.0105f); + GameObject.Find($"{BAUNotification.name}/Sizer/NameText").GetComponent().text = "System Notification"; + UnityEngine.Object.DontDestroyOnLoad(BAUNotification); + BAUNotificationManager.BAUNotificationManagerObj = BAUNotification; + BAUNotification.SetActive(false); + } + } + } + [HarmonyPatch(nameof(ChatNotification.Update))] + [HarmonyPostfix] + public static void Update_Postfix(ChatNotification __instance) + { + __instance.GetComponent().DistanceFromEdge = new Vector3(-2.8f, 0.3f, -40f); + __instance.transform.localScale = new Vector3(0.45f, 0.42f); + } +} diff --git a/Patches/ChatPatch.cs b/Patches/ChatPatch.cs new file mode 100644 index 0000000..b1d3447 --- /dev/null +++ b/Patches/ChatPatch.cs @@ -0,0 +1,229 @@ +using AmongUs.Data; +using HarmonyLib; +using System.Text; +using TMPro; +using UnityEngine; + +namespace BetterAmongUs.Patches; + +// History and dark mode from: https://github.com/0xDrMoe/TownofHost-Enhanced + +class ChatPatch +{ + public static List ChatHistory = []; + public static int CurrentHistorySelection = -1; + + [HarmonyPatch(typeof(ChatController))] + class ChatControllerPatch + { + [HarmonyPatch(nameof(ChatController.Update))] + [HarmonyPostfix] + public static void Update_Postfix(ChatController __instance) + { + for (int i = 0; i < __instance.scroller.Inner.gameObject.transform.childCount; i++) + { + GameObject chatItem = __instance.scroller.Inner.transform.GetChild(i).gameObject; + + chatItem.transform.Find("Background").GetComponent().color = new Color(0.05f, 0.05f, 0.05f, 1f); + chatItem.transform.Find("ChatText (TMP)").GetComponent().color = new Color(1f, 1f, 1f, 1f); + } + + // Free chat color + __instance.freeChatField.background.color = new Color32(40, 40, 40, byte.MaxValue); + __instance.freeChatField.textArea.compoText.Color(Color.white); + __instance.freeChatField.textArea.outputText.color = Color.white; + + // Quick chat color + __instance.quickChatField.background.color = new Color32(40, 40, 40, byte.MaxValue); + __instance.quickChatField.text.color = Color.white; + + if ((Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) && Input.GetKeyDown(KeyCode.X)) + { + ClipboardHelper.PutClipboardString(__instance.freeChatField.textArea.text); + __instance.freeChatField.textArea.SetText(""); + } + + if (Input.GetKeyDown(KeyCode.UpArrow) && ChatHistory.Any()) + { + CurrentHistorySelection = Mathf.Clamp(--CurrentHistorySelection, 0, ChatHistory.Count - 1); + __instance.freeChatField.textArea.SetText(ChatHistory[CurrentHistorySelection]); + } + + if (Input.GetKeyDown(KeyCode.DownArrow) && ChatHistory.Any()) + { + CurrentHistorySelection++; + if (CurrentHistorySelection < ChatHistory.Count) + __instance.freeChatField.textArea.SetText(ChatHistory[CurrentHistorySelection]); + else __instance.freeChatField.textArea.SetText(""); + } + } + + [HarmonyPatch(nameof(ChatController.AddChat))] + [HarmonyPrefix] + public static bool AddChat_Prefix(ChatController __instance, [HarmonyArgument(0)] PlayerControl sourcePlayer, [HarmonyArgument(1)] string chatText) + { + bool censor = false; + if (!sourcePlayer || !PlayerControl.LocalPlayer) + { + return false; + } + NetworkedPlayerInfo data = PlayerControl.LocalPlayer.Data; + NetworkedPlayerInfo data2 = sourcePlayer.Data; + if (data2 == null || data == null || (data2.IsDead && !data.IsDead)) + { + return false; + } + ChatBubble pooledBubble = __instance.GetPooledBubble(); + try + { + pooledBubble.transform.SetParent(__instance.scroller.Inner); + pooledBubble.transform.localScale = Vector3.one; + bool flag = sourcePlayer == PlayerControl.LocalPlayer; + if (flag) + { + pooledBubble.SetRight(); + } + else + { + pooledBubble.SetLeft(); + } + bool didVote = MeetingHud.Instance && MeetingHud.Instance.DidVote(sourcePlayer.PlayerId); + pooledBubble.Background.color = new Color(0.05f, 0.05f, 0.05f, 1f); + pooledBubble.SetCosmetics(data2); + __instance.SetChatBubbleName(pooledBubble, data2, data2.IsDead, didVote, PlayerNameColor.Get(data2), null); + if (censor && DataManager.Settings.Multiplayer.CensorChat) + { + chatText = BlockedWords.CensorWords(chatText, false); + } + pooledBubble.SetText(chatText); + pooledBubble.AlignChildren(); + __instance.AlignAllBubbles(); + if (!__instance.IsOpenOrOpening && __instance.notificationRoutine == null) + { + __instance.notificationRoutine = __instance.StartCoroutine(__instance.BounceDot()); + } + if (!flag) + { + SoundManager.Instance.PlaySound(__instance.messageSound, false, 1f, null).pitch = 0.5f + (float)sourcePlayer.PlayerId / 15f; + __instance.chatNotification.SetUp(sourcePlayer, chatText); + } + + StringBuilder sbTag = new StringBuilder(); + StringBuilder sbInfo = new StringBuilder(); + + string hashPuid = Utils.GetHashPuid(sourcePlayer); + string friendCode = sourcePlayer.Data.FriendCode; + string playerName = sourcePlayer.Data.PlayerName; + string RoleHexColor = sourcePlayer.IsImpostorTeam() ? "#ff1919" : "#8cffff"; + string Role = $"{sourcePlayer.GetRoleName()}+++"; + + if (GameStates.IsLobby && !GameStates.IsFreePlay) + { + Role = ""; + + if (sourcePlayer.IsDev()) + sbTag.Append("Dev+++"); + + if ((sourcePlayer == PlayerControl.LocalPlayer || sourcePlayer.GetIsBetterUser() == true) && GameStates.IsLobby && !GameStates.IsFreePlay) + sbTag.Append("Better User+++"); + + if (!string.IsNullOrEmpty(hashPuid) && AntiCheat.SickoData.ContainsKey(hashPuid) || !string.IsNullOrEmpty(friendCode) && AntiCheat.SickoData.ContainsValue(friendCode)) + sbTag.Append("Sicko User+++"); + else if (!string.IsNullOrEmpty(hashPuid) && AntiCheat.AUMData.ContainsKey(hashPuid) || !string.IsNullOrEmpty(friendCode) && AntiCheat.AUMData.ContainsValue(friendCode)) + sbTag.Append("AUM User+++"); + else if (!string.IsNullOrEmpty(hashPuid) && AntiCheat.PlayerData.ContainsKey(hashPuid) || !string.IsNullOrEmpty(friendCode) && AntiCheat.PlayerData.ContainsValue(friendCode)) + sbTag.Append("Known Cheater+++"); + } + + if (!sourcePlayer.IsImpostorTeammate()) + { + if (PlayerControl.LocalPlayer.IsAlive() && sourcePlayer != PlayerControl.LocalPlayer) + { + Role = ""; + } + } + + sbTag.Append(Role); + + sbInfo.Append(""); + for (int i = 0; i < sbTag.ToString().Split("+++").Length; i++) + { + if (!string.IsNullOrEmpty(sbTag.ToString().Split("+++")[i])) + { + if (i < sbTag.ToString().Split("+++").Length) + { + sbInfo.Append(sbTag.ToString().Split("+++")[i]); + } + if (i != sbTag.ToString().Split("+++").Length - 2) + { + sbInfo.Append(" - "); + } + } + } + sbInfo.Append(""); + + if (flag) + { + playerName = $"{sbInfo} " + playerName; + } + else + { + playerName += $" {sbInfo}"; + } + + pooledBubble.NameText.SetText(playerName); + } + catch + { + } + + return false; + } + } + + [HarmonyPatch(typeof(FreeChatInputField))] + class FreeChatInputFieldPatch + { + [HarmonyPatch(nameof(FreeChatInputField.Awake))] + [HarmonyPostfix] + public static void Awake_Postfix(FreeChatInputField __instance) + { + __instance.textArea.allowAllCharacters = true; + __instance.textArea.AllowSymbols = true; + __instance.textArea.AllowPaste = true; + __instance.textArea.AllowEmail = true; + __instance.textArea.characterLimit = 120; + __instance.charCountText.text = "0/120"; + } + [HarmonyPatch(nameof(FreeChatInputField.UpdateCharCount))] + [HarmonyPostfix] + public static void UpdateCharCount_Postfix(FreeChatInputField __instance) + { + int length = __instance.textArea.text.Length; + __instance.charCountText.text = string.Format("{0}/120", length); + __instance.charCountText.color = GetCharColor(length, UnityEngine.Color.white); + } + } + + private static UnityEngine.Color GetCharColor(int length, UnityEngine.Color color) + { + + switch (length) + { + case int n when n > 119: + if (ColorUtility.TryParseHtmlString("#ff0000", out UnityEngine.Color newColor1)) + color = newColor1; + break; + case int n when n > 74: + if (ColorUtility.TryParseHtmlString("#ffff00", out UnityEngine.Color newColor3)) + color = newColor3; + break; + default: + if (ColorUtility.TryParseHtmlString("#00f04c", out UnityEngine.Color newColor4)) + color = newColor4; + break; + } + + return color; + } +} diff --git a/Patches/ClientOptionsPatch.cs b/Patches/ClientOptionsPatch.cs new file mode 100644 index 0000000..e21f26a --- /dev/null +++ b/Patches/ClientOptionsPatch.cs @@ -0,0 +1,106 @@ +using HarmonyLib; +using UnityEngine; + +namespace BetterAmongUs.Patches; + +// Code from: https://github.com/tukasa0001/TownOfHost/pull/1265 +// Code from: https://github.com/0xDrMoe/TownofHost-Enhanced +[HarmonyPatch(typeof(OptionsMenuBehaviour))] +public static class OptionsMenuBehaviourPatch +{ + private static ClientOptionItem AntiCheat; + private static ClientOptionItem BetterHost; + private static ClientOptionItem BetterRoleAlgorithma; + private static ClientOptionItem LobbyPlayerInfo; + private static ClientOptionItem DisableLobbyTheme; + private static ClientOptionItem UnlockFPS; + private static ClientOptionItem ShowFPS; + private static ClientOptionItem OpenSaveData; + + [HarmonyPatch(nameof(OptionsMenuBehaviour.Start))] + [HarmonyPrefix] + public static void Start_Postfix(OptionsMenuBehaviour __instance) + { + static bool toggleCheckInGame(string buttonName) + { + bool flag = GameStates.IsInGame && !GameStates.IsLobby || GameStates.IsFreePlay; + if (flag) + BAUNotificationManager.Notify($"Unable to toggle {buttonName} while in game!", 2.5f); + + return flag; + } + + if (__instance.DisableMouseMovement == null) return; + + if (AntiCheat == null || AntiCheat.ToggleButton == null) + { + AntiCheat = ClientOptionItem.Create("Anti-Cheat", Main.AntiCheat, __instance); + } + + if (BetterHost == null || BetterHost.ToggleButton == null) + { + BetterHost = ClientOptionItem.Create("Better Host", Main.BetterHost, __instance, BetterHostButtonToggle, () => !toggleCheckInGame("Better Host")); + static void BetterHostButtonToggle() => RPC.SyncAllNames(force: true); + } + + if (BetterRoleAlgorithma == null || BetterRoleAlgorithma.ToggleButton == null) + { + BetterRoleAlgorithma = ClientOptionItem.Create("Better Role Algorithma", Main.BetterRoleAlgorithma, __instance, toggleCheck: () => !toggleCheckInGame("Better Role Algorithma")); + } + + if (LobbyPlayerInfo == null || LobbyPlayerInfo.ToggleButton == null) + { + LobbyPlayerInfo = ClientOptionItem.Create("Show Lobby Info", Main.LobbyPlayerInfo, __instance); + } + + if (DisableLobbyTheme == null || DisableLobbyTheme.ToggleButton == null) + { + DisableLobbyTheme = ClientOptionItem.Create("Disable Lobby Theme", Main.DisableLobbyTheme, __instance, DisableLobbyThemeButtonToggle); + static void DisableLobbyThemeButtonToggle() + { + if (GameStates.IsLobby && !Main.DisableLobbyTheme.Value) + { + SoundManager.instance.CrossFadeSound("MapTheme", LobbyBehaviour.Instance.MapTheme, 0.5f, 1.5f); + } + } + } + + if (UnlockFPS == null || UnlockFPS.ToggleButton == null) + { + UnlockFPS = ClientOptionItem.Create("UnlockFPS", Main.UnlockFPS, __instance, UnlockFPSButtonToggle); + static void UnlockFPSButtonToggle() + { + Application.targetFrameRate = Main.UnlockFPS.Value ? 165 : 60; + } + } + + if (ShowFPS == null || ShowFPS.ToggleButton == null) + { + ShowFPS = ClientOptionItem.Create("ShowFPS", Main.ShowFPS, __instance); + } + + if (OpenSaveData == null || OpenSaveData.ToggleButton == null) + { + DisableLobbyTheme = ClientOptionItem.Create("Open Save Data", null, __instance, OpenSaveDataButtonToggle, IsToggle: false); + static void OpenSaveDataButtonToggle() + { + if (File.Exists(BAUDataManager.GetFilePath("BetterData"))) + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo() + { + FileName = BAUDataManager.GetFilePath("BetterData"), + UseShellExecute = true, + Verb = "open" + }); + } + } + } + } + + [HarmonyPatch(nameof(OptionsMenuBehaviour.Close))] + [HarmonyPrefix] + public static void Close_Postfix() + { + ClientOptionItem.CustomBackground?.gameObject.SetActive(false); + } +} \ No newline at end of file diff --git a/Patches/ClientPatch.cs b/Patches/ClientPatch.cs new file mode 100644 index 0000000..f8da436 --- /dev/null +++ b/Patches/ClientPatch.cs @@ -0,0 +1,28 @@ +using BepInEx.Logging; +using Cpp2IL.Core.Logging; +using GooglePlayGames.OurUtils; +using HarmonyLib; +using UnityEngine; + +namespace BetterAmongUs.Patches; + +public class ClientPatch +{ + [HarmonyPatch(typeof(CosmeticsLayer))] + public class CosmeticsLayerPatch + { + [HarmonyPatch(nameof(CosmeticsLayer.SetColorBlindColor))] + [HarmonyPrefix] + public static bool SetColorBlindColor_Prefix(CosmeticsLayer __instance, [HarmonyArgument(0)] int color) + { + if (__instance.colorBlindText == null || !__instance.showColorBlindText) + { + return false; + } + __instance.colorBlindText.text = __instance.GetColorBlindText(); + __instance.colorBlindText.color = Palette.PlayerColors[color]; + __instance.colorBlindText.transform.localPosition = new Vector3(0f, -1.5f, 0.4999f); + return false; + } + } +} diff --git a/Patches/CommandsPatch.cs b/Patches/CommandsPatch.cs new file mode 100644 index 0000000..98ce0bd --- /dev/null +++ b/Patches/CommandsPatch.cs @@ -0,0 +1,623 @@ +using AmongUs.Data; +using AmongUs.GameOptions; +using BetterAmongUs.Role; +using HarmonyLib; +using Hazel; +using Il2CppSystem.Text; +using TMPro; +using UnityEngine; + +namespace BetterAmongUs.Patches; + +[HarmonyPatch(typeof(ChatController))] +class CommandsPatch +{ + private static PlayerControl? cmdTarget = null; + private static bool HasPermission = Permission != null; + public static PlayerControl? Permission = null; + + // List of helper text when a command is being typed out + // First word is command, {} are arguments, first --- is command description, second --- is for help command. + public static string[] CommandListHelper = + { + "help---Get help with commands", + "commands---Get a list of all available commands", + "dump---Dump at the entire log to the user's desktop", + "player {id}---Get a Players information---{player id}", + "players---Get all Player information", + "whisper {id} {msg}---Privately send an message to a player---{player id}", + "w {id} {msg}---Privately send an message to a player---{player id} {text}", + "setprefix {prefix}---Set command prefix", + "name {name}---Set player name - Host Only/Host Permission---{text}", + "kick {id}---Kick a player from the game - Host Only---{player id}", + "ban {id}---Ban a player from the game - Host Only---{player id}", + "endgame {reason}---Force end the game - Host Only---{player id}", + }; + public static string[] DebugCommandListHelper = + { + "getposition---Get current player position", + "role {role}---Set your role for the next game - Host Only - DeBug", + "setrole {id} {role}---Set another players role for the next game - Host Only - DeBug", + "syncallnames---Sync all players names for better host - Host Only - DeBug", + }; + + + // Run code for specific commands + private static void HandleCommand(ChatController __instance, string[] command) + { + bool checkDebugCommand = false; + string subArgs = command.Length > 1 ? command[1].ToLower().Trim() : ""; + string subArgs2 = command.Length > 2 ? command[2].ToLower().Trim() : ""; + bool flag = subArgs.Length > 0; + bool flag2 = subArgs2.Length > 0; + string error = "Error:"; + StringBuilder sb = new StringBuilder(); + + // Commands + switch (command[0][1..].ToLower().Trim()) + { + case "help": + Utils.AddChatPrivate( + "Welcome to ♻BetterAmongUs♻ This mod enhances your gameplay experience with a variety of exciting features.\n" + + "Explore the pause menu to access more options and better settings tailored to your needs.\n" + + "For a full list of available commands, use the `/commands` command.\n\n" + + "Our features include: \n" + + "- Built-in Client-Sided Anti-Cheat: Enjoy a fair game with our anti-cheat system that detects and prevents unauthorized actions.\n" + + "- Host Enhancements: Gain additional control as a host with improved options and settings.\n" + + "- Better Options: Customize your game with a range of new and improved settings.\n" + + "- Commands: Utilize a variety of commands to manage and enhance your gameplay.\n" + + "- Client Improvements: Experience smoother and more efficient gameplay with our client-side enhancements.\n" + + "Stay tuned for more exciting features and improvements coming your way!" + ); + break; + case "commands": + string[] allCommands = CommandListHelper; + string[] allDebugCommands = DebugCommandListHelper; + string list; + var open = "┌──────── "; + var mid = "├ "; + var close = "└──────── "; + list = "Command List\n" + open; + for (int i = 0; i < allCommands.Length; i++) + { + if (i < allCommands.Length) + { + list += $"\n{mid}{Main.CommandPrefix.Value}{allCommands[i].Split(' ')[0].Split("---")[0]} {allCommands[i].Split("---")[1]}."; + } + } + if (GameStates.IsDev) + { + list += "\n" + close + "\n"; + list += "Debug Command List\n" + open; + for (int i = 0; i < allDebugCommands.Length; i++) + { + if (i < allDebugCommands.Length) + { + list += $"\n{mid}{Main.CommandPrefix.Value}{allDebugCommands[i].Split(' ')[0].Split("---")[0]} {allDebugCommands[i].Split("---")[1]}."; + } + } + } + list += "\n" + close; + Utils.AddChatPrivate(list); + break; + case "dump": + if (GameStates.IsInGamePlay && !GameStates.IsDev) return; + + string logFilePath = Path.Combine(Environment.CurrentDirectory, "better-log.txt"); + string log = File.ReadAllText(logFilePath); + string newLog = string.Empty; + string[] logArray = log.Split(new[] { Environment.NewLine }, StringSplitOptions.None); + foreach (string text in logArray) + { + if (text.Contains("[PrivateLog]")) + { + newLog += text.Split(':')[0] + ":" + text.Split(':')[1].Replace("[PrivateLog]", "") + ": " + Encryptor.Decrypt(text.Split(':')[2][1..]) + "\n"; + } + else + { + newLog += text + "\n"; + } + } + + string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); + string logFolderPath = Path.Combine(desktopPath, "BetterAmongUsLog"); + if (!Directory.Exists(logFolderPath)) + { + Directory.CreateDirectory(logFolderPath); + } + string logFileName = "log-" + DateTime.Now.ToString("yyyy.MM.dd-HH.mm.ss") + ".log"; + string newLogFilePath = Path.Combine(logFolderPath, logFileName); + File.WriteAllText(newLogFilePath, newLog); + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo() + { + FileName = logFolderPath, + UseShellExecute = true, + Verb = "open" + }); + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo() + { + FileName = newLogFilePath, + UseShellExecute = true, + Verb = "open" + }); + break; + case "player": + if (HandlePlayerArgument(command, subArgs) == true) + { + var player = cmdTarget; + var hexColor = Utils.Color32ToHex(Palette.PlayerColors[player.CurrentOutfit.ColorId]); + var format1 = "┌ •"; + var format2 = "├ •"; + var format3 = "└ •"; + sb.Append($"{player?.Data?.PlayerName} Info:\n"); + sb.Append($"{format1} ID: {player?.Data?.PlayerId}\n"); + sb.Append($"{format2} HashPUID: {Utils.GetHashPuid($"{player?.Data?.Puid}")}\n"); + sb.Append($"{format2} Platform: {Utils.GetPlatformName(player)}\n"); + sb.Append($"{format3} FriendCode: {player?.Data?.FriendCode}"); + Utils.AddChatPrivate(sb.ToString()); + } + break; + case "players": + foreach (PlayerControl player in Main.AllPlayerControls.Where(player => !player.isDummy)) + { + var hexColor = Utils.Color32ToHex(Palette.PlayerColors[player.CurrentOutfit.ColorId]); + sb.Append($"{player?.Data?.PlayerName} Info:\n"); + sb.Append($"{player?.Data?.PlayerId} - "); + sb.Append($"{Utils.GetHashPuid($"{player?.Data?.Puid}")} - "); + sb.Append($"{Utils.GetPlatformName(player)} - "); + sb.Append($"{player?.Data?.FriendCode}"); + sb.Append("\n\n"); + } + Utils.AddChatPrivate(sb.ToString()); + break; + case "whisper" or "w": + if (HandlePlayerArgument(command, subArgs) == true) + { + if (UseCommandInGame() == true) + { + var player = cmdTarget; + var hexColor = Utils.Color32ToHex(Palette.PlayerColors[player.CurrentOutfit.ColorId]); + + MessageWriter messageWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)RpcCalls.SendChat, SendOption.None, player.GetClientId()); + messageWriter.Write(string.Join(" ", command[2..].ToArray())); + AmongUsClient.Instance.FinishRpcImmediately(messageWriter); + Utils.AddChatPrivate($"{string.Join(" ", command[2..].ToArray())}", overrideName: $"Sent Private Message to {player.Data.PlayerName}"); + } + } + break; + case "setprefix": + Main.CommandPrefix.Value = subArgs; + break; + case "name": + if (GameStates.IsHost) + { + PlayerControl.LocalPlayer.RpcSetName(command[1]); + _ = new LateTask(() => + { + RPC.SyncAllNames(); + }, 1f, $"Command {Main.CommandPrefix}name"); + } + else if (command.Length > 1 && !string.IsNullOrWhiteSpace(command[1]) && System.Text.RegularExpressions.Regex.IsMatch(command[1], @"^[a-zA-Z0-9]+$")) + { + if (HandleHasPermission(command) == true) + { + PlayerControl.LocalPlayer.CmdCheckName(command[1]); + } + } + else + { + Utils.AddChatPrivate($"{error}\nInvalid Name!"); + return; + } + + PlayerControl.LocalPlayer.Data.PlayerName = command[1]; + DataManager.Player.customization.Name = command[1]; + Utils.AddChatPrivate($"Player name has been sent to: {command[1]}"); + break; + case "kick" or "ban": + if (HandlePlayerArgument(command, subArgs) == true) + { + if (HandleIsHost(command) == true) + { + var player = cmdTarget; + var cmdFlag = command[0][1..].ToLower().Trim() != "kick"; + AmongUsClient.Instance.KickPlayer(player.GetClientId(), cmdFlag); + } + } + break; + case "endgame": + if (HandleIsHost(command) == true) + { + if (!GameStates.IsLobby && !GameStates.IsFreePlay) + { + if (subArgs == "") + { + foreach (PlayerControl player in Main.AllPlayerControls) + { + player.roleAssigned = false; + player.RpcSetRole(RoleTypes.Crewmate, true); + } + GameManager.Instance.RpcEndGame(GameOverReason.ImpostorByKill, false); + return; + } + else if (subArgs is "impostor" or "1") + { + GameManager.Instance.RpcEndGame(GameOverReason.ImpostorByKill, false); + } + else if (subArgs is "crewmate" or "2") + { + GameManager.Instance.RpcEndGame(GameOverReason.HumansByTask, false); + } + } + } + break; + default: + if (GameStates.IsDev) + { + checkDebugCommand = true; + break; + } + + Utils.AddChatPrivate("Invalid Command!"); + break; + } + + // DeBug Commands + if (checkDebugCommand) + { + switch (command[0][1..].ToLower().Trim()) + { + case "getposition": + Utils.AddChatPrivate($"Current position: {PlayerControl.LocalPlayer.GetTruePosition()}"); + break; + case "role" or "setrole": + if (HandleIsHost(command) == true) + { + if (!Main.BetterRoleAlgorithma.Value) + { + Utils.AddChatPrivate($"{error}\nBetter Role Algorithma in better options must be turned on to use this command"); + return; + } + if (command[0][1..].ToLower().Trim() == "role") + { + var player = PlayerControl.LocalPlayer; + if (subArgs == "") + { + Utils.AddChatPrivate($"Set role to Random for the next game!"); + RoleManagerPatch.SetPlayerRole.Remove(player); + return; + } + + RoleTypes role; + switch (subArgs) + { + case "impostor" or "1": + role = RoleTypes.Impostor; + break; + case "shapeshifter" or "2": + role = RoleTypes.Shapeshifter; + break; + case "phantom" or "3": + role = RoleTypes.Phantom; + break; + case "crewmate" or "4": + role = RoleTypes.Crewmate; + break; + case "engineer" or "5": + role = RoleTypes.Engineer; + break; + case "scientist" or "6": + role = RoleTypes.Scientist; + break; + case "tracker" or "7": + role = RoleTypes.Tracker; + break; + default: + Utils.AddChatPrivate($"{error}\nInvalid RoleType!"); + return; + } + string RoleHexColor = role is RoleTypes.Impostor or RoleTypes.ImpostorGhost or RoleTypes.Shapeshifter or RoleTypes.Phantom ? "#a60d0d" : "#63bfbf"; + Utils.AddChatPrivate($"Set role to {Main.GetRoleName[(int)role]} for the next game!"); + RoleManagerPatch.SetPlayerRole[player] = role; + } + else if (HandlePlayerArgument(command, subArgs) == true) + { + var player = cmdTarget; + if (player == PlayerControl.LocalPlayer) + { + Utils.AddChatPrivate($"{error}\nUnable to use /SetRole for self, use /Role!"); + return; + } + var hexColor = Utils.Color32ToHex(Palette.PlayerColors[player.CurrentOutfit.ColorId]); + if (subArgs2 == "") + { + Utils.AddChatPrivate($"Set {player?.Data?.PlayerName} role to Random for the next game!"); + RoleManagerPatch.SetPlayerRole.Remove(player); + return; + } + RoleTypes role; + switch (subArgs2) + { + case "impostor" or "1": + role = RoleTypes.Impostor; + break; + case "shapeshifter" or "2": + role = RoleTypes.Shapeshifter; + break; + case "phantom" or "3": + role = RoleTypes.Phantom; + break; + case "crewmate" or "4": + role = RoleTypes.Crewmate; + break; + case "engineer" or "5": + role = RoleTypes.Engineer; + break; + case "scientist" or "6": + role = RoleTypes.Scientist; + break; + case "tracker" or "7": + role = RoleTypes.Tracker; + break; + default: + Utils.AddChatPrivate($"{error}\nInvalid RoleType!"); + return; + } + string RoleHexColor = role is RoleTypes.Impostor or RoleTypes.ImpostorGhost or RoleTypes.Shapeshifter or RoleTypes.Phantom ? "#a60d0d" : "#63bfbf"; + Utils.AddChatPrivate($"Set {player?.Data?.PlayerName} role to {Main.GetRoleName[(int)role]} for the next game!"); + RoleManagerPatch.SetPlayerRole[player] = role; + } + } + break; + case "syncallnames": + if (HandleIsHost(command) == true) + { + RPC.SyncAllNames(); + } + break; + default: + Utils.AddChatPrivate("Invalid Command!"); + break; + } + } + + cmdTarget = null; + } + + // Condition when a player is allowed to run a specific command + private static bool UseCommandInGame() + { + bool flag = GameStates.IsInGame && GameStates.IsMeeting || GameStates.IsExilling || GameStates.IsLobby || GameStates.IsFreePlay || !PlayerControl.LocalPlayer.IsAlive(); + if (!flag) + { + Utils.AddChatPrivate("Unable To Use Command While In Game!"); + } + return flag; + } + + // Handle player ID arguments in commands + private static bool HandlePlayerArgument(string[] command, string subArg) + { + string error = "Error:"; + + bool flag = subArg.Where(char.IsDigit).ToArray().Any(); + bool flag2 = command.Length > 2; + bool flag3 = false; + + if (flag) + flag3 = Main.AllPlayerControls.ToArray().Any(player => !player.isDummy && player.Data.PlayerId == int.Parse(subArg.Where(char.IsDigit).ToArray())); + + if (flag3 == true) + { + cmdTarget = Utils.PlayerFromId(int.Parse(subArg)); + return true; + } + else + { + if (!flag) + { + if (!flag2) + { + Utils.AddChatPrivate($"{error}\n{command[0][1..]} " + "{Player ID}"); + } + else + { + Utils.AddChatPrivate($"{error}\n{command[0][1..]} {command[1]} " + "{Player ID}"); + } + } + else + { + Utils.AddChatPrivate($"{error}\nPlayer not found"); + } + + return false; + } + } + + // Handle host only commands + private static bool HandleIsHost(string[] command) + { + string error = "Error:"; + + bool flag = PlayerControl.LocalPlayer.IsHost(); + + if (!flag) + { + Utils.AddChatPrivate($"{error}\n{command[0]} Is only available as host!"); + } + + return flag; + } + + // Handle if player has permission from the host to run command + private static bool HandleHasPermission(string[] command) + { + string error = "Error:"; + + if (HasPermission != true && !GameStates.IsHost && !GameStates.IsDev) + { + Utils.AddChatPrivate($"{error}\n{command[0]} Is only available with the host permission!\nask the host to type /allow in chat to get permissions"); + return false; + } + return true; + } + + // Check if command is typed when sending chat message + [HarmonyPatch(nameof(ChatController.SendChat))] + [HarmonyPrefix] + public static bool SendChat_Prefix(ChatController __instance) + { + string text = __instance.freeChatField.textArea.text; + + if (string.IsNullOrEmpty(text) || text.Length <= 1 || text[0].ToString() != Main.CommandPrefix.Value || 3f - __instance.timeSinceLastMessage > 0f) + { + if (GameStates.InGame && !GameStates.IsLobby && !GameStates.IsFreePlay && !GameStates.IsMeeting && !GameStates.IsExilling && PlayerControl.LocalPlayer.IsAlive()) + return false; + + if (ChatPatch.ChatHistory.Count == 0 || ChatPatch.ChatHistory[^1] != text) ChatPatch.ChatHistory.Add(text); + ChatPatch.CurrentHistorySelection = ChatPatch.ChatHistory.Count; + return true; + } + + string[] command = text.Split(' '); + + HandleCommand(__instance, command); + + if (ChatPatch.ChatHistory.Count == 0 || ChatPatch.ChatHistory[^1] != text) ChatPatch.ChatHistory.Add(text); + ChatPatch.CurrentHistorySelection = ChatPatch.ChatHistory.Count; + + __instance.timeSinceLastMessage = 0f; + __instance.freeChatField.Clear(); + __instance.quickChatMenu.Clear(); + __instance.quickChatField.Clear(); + return false; + } + + // Set up command helper + private static GameObject commandText; + private static GameObject commandInfo; + private static RandomNameGenerator NameRNG; + [HarmonyPatch(nameof(ChatController.Toggle))] + [HarmonyPostfix] + public static void Awake_Postfix(ChatController __instance) + { + if (commandText == null) + { + var TextArea = __instance.freeChatField.textArea.gameObject; + GameObject CommandDisplay = UnityEngine.Object.Instantiate(TextArea, TextArea.transform.parent.transform); + CommandDisplay.transform.SetSiblingIndex(TextArea.transform.GetSiblingIndex() + 1); + CommandDisplay.transform.DestroyChildren(); + CommandDisplay.name = "CommandArea"; + CommandDisplay.GetComponent().color = new Color(1f, 1f, 1f, 0.5f); + commandText = CommandDisplay; + } + + if (commandInfo == null) + { + var TextArea = __instance.freeChatField.textArea.gameObject; + GameObject CommandInformation = UnityEngine.Object.Instantiate(TextArea, TextArea.transform.parent.transform); + CommandInformation.transform.SetSiblingIndex(TextArea.transform.GetSiblingIndex() + 1); + CommandInformation.transform.DestroyChildren(); + CommandInformation.transform.localPosition = new Vector3(CommandInformation.transform.localPosition.x, 0.45f); + CommandInformation.name = "CommandInfoText"; + CommandInformation.GetComponent().color = Color.yellow; + CommandInformation.GetComponent().outlineColor = new Color(0f, 0f, 0f, 1f); + CommandInformation.GetComponent().outlineWidth = 0.2f; + CommandInformation.GetComponent().characterWidthAdjustment = 1.5f; + CommandInformation.GetComponent().enableWordWrapping = false; + commandInfo = CommandInformation; + } + + if (NameRNG == null) + { + RandomNameGenerator rng = __instance.gameObject.AddComponent(); + NameRNG = rng; + } + } + + // Command helper + [HarmonyPatch(nameof(ChatController.Update))] + [HarmonyPostfix] + public static void Update_Postfix(ChatController __instance) + { + string text = __instance.freeChatField.textArea.text; + + if (commandText != null && commandInfo != null) + { + // Check if the first character is the command prefix + if (text.Length > 0 && text[0].ToString() == Main.CommandPrefix.Value) + { + // Get the typed command without the prefix + string typedCommand = text.Substring(1); + + // Find the closest matching command + string closestCommand = GetClosestCommand(typedCommand.Split(' ')[0]); + string CommandInfo = GetClosestCommand(typedCommand.Split(' ')[0]); + + // Check for character mismatches + if (!string.IsNullOrEmpty(closestCommand) && !string.IsNullOrEmpty(CommandInfo) && IsMatch(typedCommand.Split(' ')[0], closestCommand)) + { + closestCommand = closestCommand.Split("---")[0]; + CommandInfo = CommandInfo.Split("---")[1]; + + // Handle arguments correctly + string[] typedParts = typedCommand.Split(' '); + string[] commandParts = closestCommand.Split(' '); + + // If there are arguments, ensure to display the appropriate part + string suggestion = commandParts[0].Substring(typedParts[0].Length); // Initial suggestion + + suggestion = typedParts[0].Substring(0, typedParts[0].Length) + suggestion; + + // Handle additional arguments + for (int i = 1; i < typedParts.Length; i++) + { + if (i < commandParts.Length && typedParts[i] != "") + { + // Add spaces to match the already typed part of the current argument + suggestion = typedCommand + " " + new string(' ', typedParts[i].Length); + } + else if (i < commandParts.Length) + { + // Add the next argument if the current one is empty + suggestion = typedCommand + " " + commandParts[i]; + } + } + + if (Input.GetKeyDown(KeyCode.Tab) && typedParts.Length < 2) + { + __instance.freeChatField.textArea.SetText(Main.CommandPrefix.Value + commandParts[0]); + } + + // Display the command suggestion + commandText.GetComponent().text = Main.CommandPrefix.Value + suggestion; + commandInfo.GetComponent().text = CommandInfo; + } + else + { + // Clear the suggestion if there is a mismatch + commandText.GetComponent().text = string.Empty; + commandInfo.GetComponent().text = string.Empty; + } + } + else + { + commandText.GetComponent().text = string.Empty; + commandInfo.GetComponent().text = string.Empty; + } + } + } + + public static string GetClosestCommand(string typedCommand) + { + var closestCommand = CommandListHelper.FirstOrDefault(c => c.StartsWith(typedCommand, StringComparison.OrdinalIgnoreCase)); + + if (closestCommand == null && PlayerControl.LocalPlayer.IsDev()) + closestCommand = DebugCommandListHelper.FirstOrDefault(c => c.StartsWith(typedCommand, StringComparison.OrdinalIgnoreCase)); + + return closestCommand ?? string.Empty; + } + + public static bool IsMatch(string typedCommand, string closestCommand) => closestCommand.Length >= typedCommand.Length && + closestCommand.Substring(0, typedCommand.Length).Equals(typedCommand, System.StringComparison.OrdinalIgnoreCase); +} diff --git a/Patches/DisconnectPenaltyPatch.cs b/Patches/DisconnectPenaltyPatch.cs new file mode 100644 index 0000000..7148df3 --- /dev/null +++ b/Patches/DisconnectPenaltyPatch.cs @@ -0,0 +1,17 @@ +using HarmonyLib; + +namespace BetterAmongUs.Patches; + +[HarmonyPatch(typeof(StatsManager), nameof(StatsManager.BanMinutesLeft), MethodType.Getter)] +public static class DisconnectPenaltyPatch { + public static bool Prefix(StatsManager __instance, ref int __result) + { + if (__instance.BanPoints != 0f) + { + __instance.BanPoints = 0f; + __result = 0; + return false; + } + return true; + } +} diff --git a/Patches/DiscordPatch.cs b/Patches/DiscordPatch.cs new file mode 100644 index 0000000..b05251b --- /dev/null +++ b/Patches/DiscordPatch.cs @@ -0,0 +1,67 @@ +using AmongUs.Data; +using Discord; +using HarmonyLib; + +namespace BetterAmongUs.Patches; + + +[HarmonyPatch(typeof(ActivityManager), nameof(ActivityManager.UpdateActivity))] +public class DiscordRPC +{ + private static string lobbycode = ""; + private static string region = ""; + + public static void Prefix([HarmonyArgument(0)] Activity activity) + { + if (activity == null) return; + + string details = $"BAU v{Main.PluginVersion}"; + activity.Details = details; + + if (activity.State == "In Menus") return; + + try + { + if (!DataManager.Settings.Gameplay.StreamerMode) + { + UpdateRegionAndLobbyCode(); + if (!string.IsNullOrEmpty(lobbycode) && !string.IsNullOrEmpty(region)) + { + if (GameStates.IsNormalGame) + details = $"BAU - {lobbycode} ({region})"; + else if (GameStates.IsHideNSeek) + details = $"BAU Hide & Seek - {lobbycode} ({region})"; + } + } + else + { + if (GameStates.IsHideNSeek) + details = $"BAU v{Main.PluginVersion} - Hide & Seek"; + } + } + catch + { + } + + activity.Details = details; + } + + private static void UpdateRegionAndLobbyCode() + { + if (GameStates.IsLobby) + { + lobbycode = GameStartManager.Instance.GameRoomNameCode.text; + region = ServerManager.Instance.CurrentRegion.Name; + region = region switch + { + "North America" => "NA", + "Europe" => "EU", + "Asia" => "AS", + _ when region.Contains("MNA") => "MNA", + _ when region.Contains("MEU") => "MEU", + _ when region.Contains("MAS") => "MAS", + _ => region + }; + } + } +} diff --git a/Patches/EOSManagerPatch.cs b/Patches/EOSManagerPatch.cs new file mode 100644 index 0000000..c2a58a6 --- /dev/null +++ b/Patches/EOSManagerPatch.cs @@ -0,0 +1,15 @@ +using HarmonyLib; + +namespace BetterAmongUs.Patches; + +[HarmonyPatch(typeof(EOSManager))] +public class EOSManagerPatch +{ + [HarmonyPatch(nameof(EOSManager.Update))] + [HarmonyPostfix] + public static void Postfix(EOSManager __instance) + { + __instance.ageOfConsent = 21; + __instance.isKWSMinor = false; + } +} diff --git a/Patches/GamePlayManagerPatch.cs b/Patches/GamePlayManagerPatch.cs new file mode 100644 index 0000000..9c0e924 --- /dev/null +++ b/Patches/GamePlayManagerPatch.cs @@ -0,0 +1,34 @@ +using HarmonyLib; + +namespace BetterAmongUs.Patches; + +class GamePlayManager +{ + [HarmonyPatch(typeof(GameStartManager))] + public class GameStartManagerPatch + { + [HarmonyPatch(nameof(GameStartManager.BeginGame))] + [HarmonyPostfix] + public static void Postfix(/*GameStartManager __instance*/) + { + AntiCheat.PauseAntiCheat(); + } + } + + [HarmonyPatch(typeof(GameManager))] + public class GameManagerPatch + { + [HarmonyPatch(nameof(GameManager.EndGame))] + [HarmonyPostfix] + public static void Postfix(/*GameManager __instance*/) + { + if (GameStates.IsHost) + { + foreach (PlayerControl player in Main.AllPlayerControls) + { + player.RpcSetName(player.Data.PlayerName); + } + } + } + } +} diff --git a/Patches/HudManagerPatch.cs b/Patches/HudManagerPatch.cs new file mode 100644 index 0000000..0997beb --- /dev/null +++ b/Patches/HudManagerPatch.cs @@ -0,0 +1,80 @@ +using HarmonyLib; +using UnityEngine; + + +namespace BetterAmongUs.Patches; + +[HarmonyPatch(typeof(HudManager))] +public class HudManagerPatch +{ + public static string WelcomeMessage = $"Welcome To Better Among Us\n{Main.GetVersionText()}\n" + + "Thanks for downloading!\n \n" + + "BAU Is a mod for improving the vanilla Among Us experience with a built-in Anti-Cheat and other futures, BAU is a client-sided mod so it can be used with other vanilla Among Us players.\n\n" + + "BAU Also has a option in Better Options on the pause menu called Better Host, Better Host is designed to enhance the vanilla experience for other players without having BAU installed!"; + + private static bool HasBeenWelcomed = false; + [HarmonyPatch(nameof(HudManager.Start))] + [HarmonyPostfix] + public static void Start_Postfix(HudManager __instance) + { + _ = new LateTask(() => + { + try + { + if (BAUNotificationManager.BAUNotificationManagerObj == null) + { + bool ChatState = GameObject.Find("ChatUi"); + __instance.Chat.gameObject.SetActive(true); + __instance.Chat.chatNotification.gameObject.SetActive(true); + __instance.Chat.chatNotification.SetUp(PlayerControl.LocalPlayer, ""); + __instance.Chat.chatNotification.timeOnScreen = 0f; + __instance.Chat.chatNotification.gameObject.SetActive(false); + __instance.Chat.gameObject.SetActive(ChatState); + } + + if (!HasBeenWelcomed && GameStates.IsInGame && GameStates.IsLobby && !GameStates.IsFreePlay) + { + BAUNotificationManager.Notify("Welcome To Better Among Us!", 8f); + + Utils.AddChatPrivate(WelcomeMessage, overrideName: " "); + HasBeenWelcomed = true; + } + } + catch { } + }, 1f, "HudManagerPatch Start"); + } + [HarmonyPatch(nameof(HudManager.Update))] + [HarmonyPostfix] + public static void Update_Postfix(HudManager __instance) + { + try + { + GameObject gameStart = GameObject.Find("GameStartManager"); + if (gameStart != null) + gameStart.transform.SetLocalY(-2.8f); + + if (GameStates.InGame && __instance?.Chat?.gameObject.active != true) + __instance.Chat.gameObject.SetActive(true); + + GameObject TaskDisplay = GameObject.Find("TaskDisplay"); + GameObject ProgressTracker = GameObject.Find("ProgressTracker"); + if (TaskDisplay != null && ProgressTracker != null) + { + if (PlayerControl.LocalPlayer.IsImpostorTeam()) + { + __instance.CrewmatesKilled.gameObject.SetActive(true); + __instance.CrewmatesKilled.gameObject.transform.SetLocalZ(-25); + TaskDisplay.transform.localPosition = new Vector3(0f, -0.4f, -5f); + ProgressTracker.transform.localPosition = new Vector3(-2.8733f, 3.1f); + } + else if (__instance.CrewmatesKilled.isActiveAndEnabled) + { + __instance.CrewmatesKilled.gameObject.SetActive(GameManager.Instance.ShowCrewmatesKilled()); + TaskDisplay.transform.localPosition = new Vector3(0f, 0f); + ProgressTracker.transform.localPosition = new Vector3(-2.8733f, 2.7f, 0); + } + } + } + catch { } + } +} diff --git a/Patches/LobbyPatch.cs b/Patches/LobbyPatch.cs new file mode 100644 index 0000000..129ceb3 --- /dev/null +++ b/Patches/LobbyPatch.cs @@ -0,0 +1,19 @@ +using HarmonyLib; + +namespace BetterAmongUs.Patches; + +public class LobbyPatch +{ + [HarmonyPatch(typeof(LobbyBehaviour))] + internal class LobbyBehaviourPatch + { + // Disabled annoying music + [HarmonyPatch(nameof(LobbyBehaviour.Update))] + [HarmonyPostfix] + public static void Postfix(PingTracker __instance) + { + if (Main.DisableLobbyTheme.Value) + SoundManager.instance.StopSound(LobbyBehaviour.Instance.MapTheme); + } + } +} diff --git a/Patches/MainMenuPatch.cs b/Patches/MainMenuPatch.cs new file mode 100644 index 0000000..90545e8 --- /dev/null +++ b/Patches/MainMenuPatch.cs @@ -0,0 +1,46 @@ +using HarmonyLib; +using UnityEngine; + +namespace BetterAmongUs.Patches; + +internal class MainMenuPatch +{ + private static readonly string OnlineMsg = "Online access has been temporarily disabled!"; + + // Handle FileChecker + [HarmonyPatch(typeof(AccountManager))] + internal class AccountManagerPatch + { + [HarmonyPatch(nameof(AccountManager.CanPlayOnline))] + [HarmonyPostfix] + public static void Postfix() + { + if (!FileChecker.HasShownPopUp && FileChecker.CheckIfUnauthorizedFiles()) + { + var lines = "----------------------------------------------------------------------------------------------"; + var icon = "♻"; + var warning = "⚠"; + FileChecker.HasShownPopUp = true; + DisconnectPopup.Instance._textArea.enableWordWrapping = false; + DisconnectPopup.Instance.ShowCustom($"{lines}\n{icon}Better Among Us{icon}\n{warning}{FileChecker.UnauthorizedReason}{warning}\n\n\n{OnlineMsg}\n{lines}"); + } + } + } + + // Replace AU logo with BAU logo + [HarmonyPatch(typeof(MainMenuManager))] + internal class MainMenuManagerPatch + { + [HarmonyPatch(nameof(MainMenuManager.Start))] + [HarmonyPostfix] + public static void Postfix(/*MainMenuManager __instance*/) + { + GameObject logo = GameObject.Find("LeftPanel/Sizer/LOGO-AU"); + GameObject sizer = logo.transform.parent.gameObject; + sizer.transform.localPosition = new Vector3(sizer.transform.localPosition.x, sizer.transform.localPosition.y - 0.035f, sizer.transform.localPosition.z); + sizer.transform.position = new Vector3(sizer.transform.position.x, sizer.transform.position.y, -0.5f); + logo.transform.localScale = new Vector3(0.003f, 0.0025f, 0f); + logo.GetComponent().sprite = Utils.LoadSprite("BetterAmongUs.Resources.Images.BetterAmongUs-Logo.png", 1f); + } + } +} diff --git a/Patches/MeetingHudPatch.cs b/Patches/MeetingHudPatch.cs new file mode 100644 index 0000000..5ecfb2e --- /dev/null +++ b/Patches/MeetingHudPatch.cs @@ -0,0 +1,166 @@ +using HarmonyLib; +using System.Text; +using TMPro; +using UnityEngine; + +namespace BetterAmongUs; +[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))] +class MeetingHudStartPatch +{ + // Set up meeting player info text + public static void Postfix(MeetingHud __instance) + { + foreach (var pva in __instance.playerStates) + { + var TextTopMeeting = UnityEngine.Object.Instantiate(pva.NameText, pva.NameText.transform); + TextTopMeeting.gameObject.name = "TextTop"; + TextTopMeeting.DestroyChildren(); + TextTopMeeting.transform.position = pva.NameText.transform.position; + TextTopMeeting.transform.position += new Vector3(0f, 0.15f); + TextTopMeeting.GetComponent().text = ""; + + var TextInfoMeeting = UnityEngine.Object.Instantiate(pva.NameText, pva.NameText.transform); + TextInfoMeeting.gameObject.name = "TextInfo"; + TextInfoMeeting.DestroyChildren(); + TextInfoMeeting.transform.position = pva.NameText.transform.position; + TextInfoMeeting.transform.position += new Vector3(0f, 0.28f); + TextInfoMeeting.GetComponent().text = ""; + + var PlayerLevel = pva.transform.Find("PlayerLevel"); + PlayerLevel.localPosition = new Vector3(PlayerLevel.localPosition.x, PlayerLevel.localPosition.y, -2f); + var LevelDisplay = UnityEngine.Object.Instantiate(PlayerLevel, pva.transform); + LevelDisplay.transform.SetSiblingIndex(pva.transform.Find("PlayerLevel").GetSiblingIndex() + 1); + LevelDisplay.gameObject.name = "PlayerId"; + LevelDisplay.GetComponent().color = new Color(1f, 0f, 1f, 1f); + var IdLabel = LevelDisplay.transform.Find("LevelLabel"); + var IdNumber = LevelDisplay.transform.Find("LevelNumber"); + UnityEngine.Object.Destroy(IdLabel.GetComponent()); + IdLabel.GetComponent().text = "ID"; + IdNumber.GetComponent().text = pva.TargetPlayerId.ToString(); + IdLabel.name = "IdLabel"; + IdNumber.name = "IdNumber"; + PlayerLevel.transform.position += new Vector3(0.23f, 0f); + } + + RPC.SyncAllNames(isForMeeting: true); + } +} +[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Update))] +class MeetingHudUpdatePatch +{ + // Set player meeting info + public static void Postfix(MeetingHud __instance) + { + foreach (var pva in __instance.playerStates) + { + if (pva == null) return; + + if (pva.ColorBlindName.isActiveAndEnabled) + { + pva.ColorBlindName.transform.localPosition = new Vector3(-0.91f, -0.19f, -0.05f); + pva.ColorBlindName.color = Palette.PlayerColors[pva.PlayerIcon.ColorId]; + pva.ColorBlindName.outlineWidth = 0.2745f; + } + + TextMeshPro TopText = pva.NameText.transform.Find("TextTop").gameObject.GetComponent(); + TextMeshPro InfoText = pva.NameText.transform.Find("TextInfo").gameObject.GetComponent(); + + bool flag = Main.AllPlayerControls.Any(player => player.PlayerId == pva.TargetPlayerId); + + if (!flag) + { + SetPlayerTextInfoMeeting(pva, "Disconnected", isInfo: true); + SetPlayerTextInfoMeeting(pva, ""); + pva.transform.Find("votePlayerBase").gameObject.SetActive(false); + pva.transform.Find("deadX_border").gameObject.SetActive(false); + pva.ClearForResults(); + pva.SetDisabled(); + } + else if (TopText != null && InfoText != null) + { + var target = Utils.PlayerFromId(pva.TargetPlayerId); + if (target == null) return; + string hashPuid = Utils.GetHashPuid(target); + string friendCode = target.Data.FriendCode; + + StringBuilder sbTag = new StringBuilder(); + StringBuilder sbInfo = new StringBuilder(); + + // Put +++ at the end of each tag + + if (!string.IsNullOrEmpty(hashPuid) && AntiCheat.SickoData.ContainsKey(hashPuid) || !string.IsNullOrEmpty(friendCode) && AntiCheat.SickoData.ContainsValue(friendCode)) + sbTag.Append("Sicko User+++"); + else if (!string.IsNullOrEmpty(hashPuid) && AntiCheat.AUMData.ContainsKey(hashPuid) || !string.IsNullOrEmpty(friendCode) && AntiCheat.AUMData.ContainsValue(friendCode)) + sbTag.Append("AUM User+++"); + else if (!string.IsNullOrEmpty(hashPuid) && AntiCheat.PlayerData.ContainsKey(hashPuid) || !string.IsNullOrEmpty(friendCode) && AntiCheat.PlayerData.ContainsValue(friendCode)) + sbTag.Append("Known Cheater+++"); + + for (int i = 0; i < sbTag.ToString().Split("+++").Length; i++) + { + if (!string.IsNullOrEmpty(sbTag.ToString().Split("+++")[i])) + { + if (i < sbTag.ToString().Split("+++").Length) + { + sbInfo.Append(sbTag.ToString().Split("+++")[i]); + } + if (i != sbTag.ToString().Split("+++").Length - 2) + { + sbInfo.Append(" - "); + } + } + } + + string RoleHexColor = target.IsImpostorTeam() ? "#ff1919" : "#8cffff"; + string Role = $"{target.GetRoleName()}"; + if (!target.IsImpostorTeammate()) + { + if (PlayerControl.LocalPlayer.IsAlive() && target != PlayerControl.LocalPlayer) + { + if (!DebugMenu.RevealRoles) + { + Role = ""; + } + } + } + + Vector3 textPos; + + if (Role != "" && !string.IsNullOrEmpty(sbInfo.ToString())) + textPos = new Vector3(pva.NameText.transform.localPosition.x, -0.045f); + else if (Role != "" || !string.IsNullOrEmpty(sbInfo.ToString())) + textPos = new Vector3(pva.NameText.transform.localPosition.x, -0.03f); + else + textPos = new Vector3(pva.NameText.transform.localPosition.x, 0.015f); + + pva.NameText.transform.localPosition = textPos; + + SetPlayerTextInfoMeeting(pva, $"{sbInfo}", isInfo: true); + SetPlayerTextInfoMeeting(pva, $"{Role}"); + } + } + } + + private static void SetPlayerTextInfoMeeting(PlayerVoteArea pva, string text, bool isInfo = false) + { + string InfoType = "TextTop"; + if (isInfo) + { + InfoType = "TextInfo"; + GameObject TopText = pva.NameText.transform.Find("TextTop").gameObject; + if (TopText != null) + { + if (TopText.GetComponent()?.text.Replace("", string.Empty).Replace("", string.Empty).Length < 1) + { + text = "" + text + ""; + } + } + } + + text = "" + text + ""; + GameObject TextObj = pva.NameText.transform.Find(InfoType).gameObject; ; + if (TextObj != null) + { + TextObj.GetComponent().text = text; + } + } +} \ No newline at end of file diff --git a/Patches/MiniMapBehaviourPatch.cs b/Patches/MiniMapBehaviourPatch.cs new file mode 100644 index 0000000..55c32ed --- /dev/null +++ b/Patches/MiniMapBehaviourPatch.cs @@ -0,0 +1,107 @@ +using HarmonyLib; +using UnityEngine; + + +namespace BetterAmongUs.Patches; + +public class MiniMapBehaviourPatch +{ + [HarmonyPatch(typeof(MapBehaviour))] + class MapBehaviourPatch + { + [HarmonyPatch(nameof(MapBehaviour.Awake))] + [HarmonyPostfix] + public static void Awake_Postfix(MapBehaviour __instance) + { + PoolablePlayer NewDisplay = __instance.HerePoint.gameObject.AddComponent(); + NewDisplay.SetBodyCosmeticsVisible(true); + } + [HarmonyPatch(nameof(MapBehaviour.Show))] + [HarmonyPrefix] + public static bool Show_Prefix(MapBehaviour __instance, ref MapOptions opts) + { + if (__instance.IsOpen) + { + __instance.Close(); + return false; + } + if (!PlayerControl.LocalPlayer.CanMove && !PlayerControl.LocalPlayer.IsInVent() && !PlayerControl.LocalPlayer.onLadder && !PlayerControl.LocalPlayer.inMovingPlat && !GameStates.IsMeeting) + { + return false; + } + switch (opts.Mode) + { + case MapOptions.Modes.Normal: + __instance.ShowNormalMap(); + break; + case MapOptions.Modes.CountOverlay: + __instance.ShowCountOverlay(opts.AllowMovementWhileMapOpen, opts.ShowLivePlayerPosition, opts.IncludeDeadBodies); + break; + case MapOptions.Modes.Sabotage: + __instance.ShowSabotageMap(); + break; + } + + return false; + } + [HarmonyPatch(nameof(MapBehaviour.ShowNormalMap))] + [HarmonyPrefix] + public static bool ShowNormalMap_Prefix(MapBehaviour __instance) + { + if (__instance.IsOpen) + { + __instance.Close(); + return false; + } + if (!PlayerControl.LocalPlayer.CanMove && !PlayerControl.LocalPlayer.IsInVent() && !PlayerControl.LocalPlayer.onLadder && !PlayerControl.LocalPlayer.inMovingPlat && !GameStates.IsMeeting) + { + return false; + } + PlayerControl.LocalPlayer.SetPlayerMaterialColors(__instance.HerePoint); + if (PlayerControl.LocalPlayer.isTrackingPlayer) + { + if (PlayerControl.LocalPlayer.trackedPlayer.Data.Disconnected) + { + __instance.TrackedHerePoint.gameObject.SetActive(false); + } + else + { + __instance.SetTrackedHerePointColor(PlayerControl.LocalPlayer.trackedPlayerColorID); + __instance.TrackedHerePoint.gameObject.SetActive(true); + __instance.UpdateTrackedPosition(); + } + } + __instance.GenericShow(); + __instance.taskOverlay.Show(); + __instance.ColorControl.SetColor(new Color(0.05f, 0.6f, 1f, 1f)); + DestroyableSingleton.Instance.SetHudActive(false); + return false; + } + [HarmonyPatch(nameof(MapBehaviour.ShowSabotageMap))] + [HarmonyPrefix] + public static bool ShowSabotageMap_Prefix(MapBehaviour __instance) + { + if (!PlayerControl.LocalPlayer.CanMove && !PlayerControl.LocalPlayer.IsInVent() && !PlayerControl.LocalPlayer.onLadder && !PlayerControl.LocalPlayer.inMovingPlat) + { + return false; + } + PlayerControl.LocalPlayer.SetPlayerMaterialColors(__instance.HerePoint); + __instance.GenericShow(); + __instance.infectedOverlay.gameObject.SetActive(true); + __instance.taskOverlay.Show(); + __instance.ColorControl.SetColor(new Color(1f, 0.3f, 0f, 1f)); + return false; + } + [HarmonyPatch(nameof(MapBehaviour.ShowCountOverlay))] + [HarmonyPostfix] + public static void ShowCountOverlay_Postfix(MapBehaviour __instance) => __instance.ColorControl.SetColor(new Color(0.2f, 0.5f, 0f, 1f)); + } + + [HarmonyPatch(typeof(MapConsole))] + class MapConsolePatch + { + [HarmonyPatch(nameof(MapConsole.Use))] + [HarmonyPostfix] + public static void ShowCountOverlay_Postfix() => MapBehaviour.Instance.ColorControl.SetColor(new Color(0.2f, 0.5f, 0f, 1f)); + } +} diff --git a/Patches/ModManagerPatch.cs b/Patches/ModManagerPatch.cs new file mode 100644 index 0000000..bf9c476 --- /dev/null +++ b/Patches/ModManagerPatch.cs @@ -0,0 +1,22 @@ +using HarmonyLib; +using UnityEngine; + +namespace BetterAmongUs.Patches; + +[HarmonyPatch(typeof(ModManager))] +public class ModManagerPatch +{ + [HarmonyPatch(nameof(ModManager.LateUpdate))] + [HarmonyPostfix] + public static void LateUpdate_Postfix(ModManager __instance) + { + // __instance.ShowModStamp(); + + if (__instance.ModStamp.gameObject.active == true) + __instance.ModStamp.GetComponent().sprite = Utils.LoadSprite("BetterAmongUs.Resources.Images.BetterAmongUs-Mod.png", 250f); + + FileChecker.UpdateUnauthorizedFiles(); + LateTask.Update(Time.deltaTime); + BAUNotificationManager.Update(); + } +} diff --git a/Patches/PingTrackerPatch.cs b/Patches/PingTrackerPatch.cs new file mode 100644 index 0000000..af80add --- /dev/null +++ b/Patches/PingTrackerPatch.cs @@ -0,0 +1,80 @@ +using HarmonyLib; +using System.Text; +using UnityEngine; + +namespace BetterAmongUs.Patches; + +[HarmonyPatch(typeof(PingTracker))] +public class PingTrackerPatch +{ + [HarmonyPatch(nameof(PingTracker.Update))] + [HarmonyPrefix] + public static bool Prefix(PingTracker __instance) + { + try + { + PlayerControl Host = AmongUsClient.Instance.GetHost().Character; + + if (GameStates.IsFreePlay) + { + __instance.gameObject.SetActive(false); + } + + StringBuilder sb = new StringBuilder(); + + sb.AppendFormat("PING: {0}\n", GetPingColor(AmongUsClient.Instance.Ping)); + sb.Append($"BetterAmongUs {Main.GetVersionText()}\n"); + sb.AppendFormat("{0}\n", Main.Github); + + if (Main.BetterHost.Value && GameStates.IsHost) + { + sb.Append("Better Host: Enabled\n"); + } + + if (Main.ShowFPS.Value) + { + float FPSNum = 1.0f / Time.deltaTime; + sb.AppendFormat("FPS: {0}\n", (int)FPSNum); + } + + // Add Host Info if not in lobby + if (GameStates.IsInGamePlay && Host != null) + { + string hostColor = Utils.Color32ToHex(Palette.PlayerColors[Host.CurrentOutfit.ColorId]); + sb.AppendFormat("Host: {1}\n", hostColor, Host.CurrentOutfit.PlayerName); + } + + __instance.aspectPosition.DistanceFromEdge = new Vector3(4f, 0.1f, -5); + __instance.aspectPosition.Alignment = AspectPosition.EdgeAlignments.RightTop; + __instance.text.outlineWidth = 0.3f; + __instance.text.text = sb.ToString(); + } + catch { } + + return false; + } + + private static string GetPingColor(int ping) + { + string color; + switch (ping) + { + case int n when n > 250: + color = "#ff0000"; + break; + case int n when n > 100: + color = "#ffa200"; + break; + case int n when n > 50: + color = "#ffff00"; + break; + default: + color = "#00f04c"; + break; + } + + string newPing = $"{ping} ms"; + + return newPing; + } +} diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs new file mode 100644 index 0000000..9a46efc --- /dev/null +++ b/Patches/PlayerControlPatch.cs @@ -0,0 +1,227 @@ +using AmongUs.Data; +using HarmonyLib; +using Hazel; +using Il2CppSystem.Linq; +using LibCpp2IL; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; + +namespace BetterAmongUs.Patches; + +[HarmonyPatch(typeof(PlayerControl))] +class PlayerControlPatch +{ + public static float infotime = 0f; + [HarmonyPatch(nameof(PlayerControl.FixedUpdate))] + [HarmonyPostfix] + public static void FixedUpdate_Postfix(PlayerControl __instance) + { + // Set up player text info + var NameText = GameObject.Find($"{__instance.gameObject.transform.name}/Names/NameText_TMP"); + var InfoText = GameObject.Find($"{__instance.gameObject.transform.name}/Names/NameText_TMP/InfoText_T_TMP"); + if (NameText != null && InfoText == null) + { + GameObject playerInfoDisplayInfo = UnityEngine.Object.Instantiate(NameText, NameText.transform); + playerInfoDisplayInfo.name = "InfoText_Info_TMP"; + playerInfoDisplayInfo.transform.DestroyChildren(); + playerInfoDisplayInfo.transform.position += new Vector3(0f, 0.25f); + playerInfoDisplayInfo.GetComponent().text = string.Empty; + playerInfoDisplayInfo.SetActive(true); + + GameObject playerInfoDisplayTop = UnityEngine.Object.Instantiate(NameText, NameText.transform); + playerInfoDisplayTop.name = "InfoText_T_TMP"; + playerInfoDisplayTop.transform.DestroyChildren(); + playerInfoDisplayTop.transform.position += new Vector3(0f, 0.15f); + playerInfoDisplayTop.GetComponent().text = string.Empty; + playerInfoDisplayTop.SetActive(true); + + GameObject playerInfoDisplayBottom = UnityEngine.Object.Instantiate(NameText, NameText.transform); + playerInfoDisplayBottom.name = "InfoText_B_TMP"; + playerInfoDisplayBottom.transform.DestroyChildren(); + playerInfoDisplayBottom.transform.position += new Vector3(0f, -0.15f); + playerInfoDisplayBottom.GetComponent().text = string.Empty; + playerInfoDisplayBottom.SetActive(true); + } + + // Set color blind text on player + if (__instance.DataIsCollected() && !__instance.shapeshifting) + { + __instance.cosmetics.SetColorBlindColor(__instance.CurrentOutfit.ColorId); + } + else + { + __instance.cosmetics.colorBlindText.text = string.Empty; + } + + infotime -= Time.deltaTime; + + if (infotime <= 0) + { + SetPlayerInfo(__instance); + infotime = 0.4f; + } + + if (GameStates.IsInGame && GameStates.IsHost) + RPC.SyncAllNames(isBetterHost: false); + } + + public static void SetPlayerInfo(PlayerControl player) + { + if (player == null) return; + + // Set player text info + if (player.DataIsCollected() != true) + { + if (player.gameObject.transform.Find("Names/NameText_TMP").GetComponent().text is "???" or "Player") + { + player.gameObject.transform.Find("Names/NameText_TMP").GetComponent().text = "Loading"; + } + + return; + } + + if (Main.AllPlayerControls.Any(pc => player.Data.PlayerId == pc.shapeshiftTargetPlayerId)) + return; + + string NewName = player.Data.PlayerName; + string hashPuid = Utils.GetHashPuid(player); + string friendCode = player.Data.FriendCode; + int playerId = player.PlayerId; + string Lv = " - Lv: " + player.Data.PlayerLevel.ToString() + ""; + + string pattern = @"^[a-zA-Z0-9#]+$"; + string hashtagPattern = @"^#[0-9]{4}$"; + + string friendCodeColor = (Regex.Replace(friendCode, hashtagPattern, string.Empty).Length is > 10 or < 5 || !Regex.IsMatch(friendCode, pattern) || !Regex.IsMatch(friendCode, hashtagPattern)) ? "#00f7ff" : "#ff0000"; + + if (string.IsNullOrEmpty(friendCode) || friendCode == "") + friendCode = "???"; + + if (DataManager.Settings.Gameplay.StreamerMode == true) + friendCode = string.Concat('*').Repeat(friendCode.Length); + + StringBuilder sbTag = new StringBuilder(); + StringBuilder sbInfo = new StringBuilder(); + + // Put +++ at the end of each tag + + if (player.IsDev() && !GameStates.IsInGamePlay) + sbTag.Append("(Dev)+++"); + + if ((player == PlayerControl.LocalPlayer || player.GetIsBetterUser() == true) && GameStates.IsLobby && !GameStates.IsFreePlay) + sbTag.Append("Better User+++"); + + if (!string.IsNullOrEmpty(hashPuid) && AntiCheat.SickoData.ContainsKey(hashPuid) || !string.IsNullOrEmpty(friendCode) && AntiCheat.SickoData.ContainsValue(friendCode)) + sbTag.Append("Sicko User+++"); + else if (!string.IsNullOrEmpty(hashPuid) && AntiCheat.AUMData.ContainsKey(hashPuid) || !string.IsNullOrEmpty(friendCode) && AntiCheat.AUMData.ContainsValue(friendCode)) + sbTag.Append("AUM User+++"); + else if (!string.IsNullOrEmpty(hashPuid) && AntiCheat.PlayerData.ContainsKey(hashPuid) || !string.IsNullOrEmpty(friendCode) && AntiCheat.PlayerData.ContainsValue(friendCode)) + sbTag.Append("Known Cheater+++"); + + if (GameStates.IsLobby && !GameStates.IsFreePlay) + sbTag.Append($"ID: {playerId}+++"); + + for (int i = 0; i < sbTag.ToString().Split("+++").Length; i++) + { + if (!string.IsNullOrEmpty(sbTag.ToString().Split("+++")[i])) + { + if (i < sbTag.ToString().Split("+++").Length) + { + sbInfo.Append(sbTag.ToString().Split("+++")[i]); + } + if (i != sbTag.ToString().Split("+++").Length - 2) + { + sbInfo.Append(" - "); + } + } + } + + PlayerControl Host = AmongUsClient.Instance.GetHost().Character; + if (player.IsHost() && GameStates.InGame && GameStates.IsLobby && !GameStates.IsFreePlay) + { + if (Main.LobbyPlayerInfo.Value == true) + { + string hexColor = Utils.Color32ToHex(Palette.PlayerColors[Host.CurrentOutfit.ColorId]); + NewName = $"{player.Data.PlayerName}"; + } + } + if (player.IsImpostorTeammate() && player != PlayerControl.LocalPlayer) + { + NewName = $"{player.Data.PlayerName}"; + } + + if (!player.IsInShapeshift()) + player.RawSetName(NewName); + + if (!GameStates.IsVanillaServer) + { + friendCode = string.Empty; + Lv = string.Empty; + } + + StringBuilder sbTag2 = new StringBuilder(); + StringBuilder sbInfo2 = new StringBuilder(); + + // Put +++ at the end of each tag + + sbTag2.Append($"{Utils.GetPlatformName(player, useTag: true)}"); // Don't put +++ here + sbTag2.Append($"{Lv}+++"); + + for (int i = 0; i < sbTag2.ToString().Split("+++").Length; i++) + { + if (!string.IsNullOrEmpty(sbTag2.ToString().Split("+++")[i])) + { + if (i < sbTag2.ToString().Split("+++").Length) + { + sbInfo2.Append(sbTag2.ToString().Split("+++")[i]); + } + if (i != sbTag2.ToString().Split("+++").Length - 2) + { + sbInfo2.Append(" - "); + } + } + } + + if (!GameStates.IsInGamePlay) + { + if (Main.LobbyPlayerInfo.Value == true) + { + player.SetPlayerTextInfo($"{sbInfo}", isInfo: true); + player.SetPlayerTextInfo($"{sbInfo2}"); + player.SetPlayerTextInfo($"{friendCode}", isBottom: true); + } + else + { + player.ResetAllPlayerTextInfo(); + } + } + else + { + string RoleHexColor = player.IsImpostorTeam() ? "#ff1919" : "#8cffff"; + string Role = $"{player.GetRoleName()}"; + if (!player.IsImpostorTeammate()) + { + if (PlayerControl.LocalPlayer.IsAlive() && player != PlayerControl.LocalPlayer) + { + if (!DebugMenu.RevealRoles) + { + Role = ""; + } + } + } + player.SetPlayerTextInfo($"{sbInfo}", isInfo: true); + player.SetPlayerTextInfo($"{Role}"); + player.SetPlayerTextInfo("", isBottom: true); + } + } + + [HarmonyPatch(nameof(PlayerControl.MurderPlayer))] + [HarmonyPostfix] + public static void MurderPlayer_Postfix(/*PlayerControl __instance, [HarmonyArgument(0)] PlayerControl target*/) + { + if (GameStates.IsInGame || GameStates.IsFreePlay && !GameStates.IsLobby && !GameStates.IsHideNSeek && HudManager.Instance.CrewmatesKilled.isActiveAndEnabled) + HudManager.Instance?.NotifyOfDeath(); + } +} diff --git a/Patches/PlayerJoinAndLeftPatch.cs b/Patches/PlayerJoinAndLeftPatch.cs new file mode 100644 index 0000000..82d86d9 --- /dev/null +++ b/Patches/PlayerJoinAndLeftPatch.cs @@ -0,0 +1,110 @@ +using InnerNet; +using HarmonyLib; +using Hazel; +using BetterAmongUs.Patches; + +namespace BetterAmongUs; + +[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnGameJoined))] +class OnGameJoinedPatch +{ + public static void Postfix(/*AmongUsClient __instance*/) + { + PlayerControlPatch.infotime = 0f; + + AntiCheat.PauseAntiCheat(); + + // Fix host icon in lobby on modded servers + if (!GameStates.IsVanillaServer) + { + var host = AmongUsClient.Instance.GetHost().Character; + host.SetColor(-2); + host.SetColor(host.CurrentOutfit.ColorId); + } + + _ = new LateTask(() => + { + // Send Better Among Us Check RPC + if (GameStates.IsInGame) + { + MessageWriter messageWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.BetterCheck, SendOption.None, -1); + messageWriter.Write((byte)PlayerControl.LocalPlayer.NetId); + AmongUsClient.Instance.FinishRpcImmediately(messageWriter); + } + }, 1.5f, "OnGameJoinedPatch"); + + Logger.Log($"Successfully joined {GameCode.IntToGameName(AmongUsClient.Instance.GameId)}", "OnGameJoinedPatch"); + } +} +[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnPlayerJoined))] +public static class OnPlayerJoinedPatch +{ + public static void Postfix(/*AmongUsClient __instance,*/ [HarmonyArgument(0)] ClientData client) + { + PlayerControlPatch.infotime = 0f; + + _ = new LateTask(() => + { + if (GameStates.IsInGame) + { + // Send Better Among Us Check RPC + MessageWriter messageWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, unchecked((byte)CustomRPC.BetterCheck), SendOption.None, -1); + messageWriter.Write((byte)PlayerControl.LocalPlayer.NetId); + AmongUsClient.Instance.FinishRpcImmediately(messageWriter); + + RPC.SyncAllNames(force: true); + + if (Main.BetterHost.Value) + client.Character.RpcSendHostChat(HudManagerPatch.WelcomeMessage, sendToBetterUser: false); + } + }, 1.5f, "OnPlayerJoinedPatch", false); + } +} +[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnPlayerLeft))] +class OnPlayerLeftPatch +{ + public static void Postfix(/*AmongUsClient __instance,*/ [HarmonyArgument(0)] ClientData data, [HarmonyArgument(1)] DisconnectReasons reason) + { + if ((GameStates.IsInGame || GameStates.IsFreePlay) && !GameStates.IsLobby && !GameStates.IsHideNSeek && HudManager.Instance.CrewmatesKilled.isActiveAndEnabled) + HudManager.Instance?.NotifyOfDisconnect(data.Character); + } +} + +[HarmonyPatch(typeof(GameData), nameof(GameData.ShowNotification))] +class GameDataPatch +{ + public static bool Prefix(AmongUsClient __instance, ref string playerName, ref DisconnectReasons reason) + { + string ReasonText; + switch (reason) + { + case DisconnectReasons.ExitGame: + ReasonText = $"{playerName} Left the game!"; + break; + case DisconnectReasons.ClientTimeout: + ReasonText = $"{playerName} Disconnected!"; + break; + case DisconnectReasons.Kicked: + ReasonText = $"{playerName} Was kicked by {AmongUsClient.Instance.GetHost().Character.Data.PlayerName}!"; + break; + case DisconnectReasons.Banned: + ReasonText = $"{playerName} Was banned by {AmongUsClient.Instance.GetHost().Character.Data.PlayerName}!"; + break; + case DisconnectReasons.Hacking: + ReasonText = $"{playerName} Was banned by Innersloth Anti-Cheat!"; + break; + case DisconnectReasons.Error: + ReasonText = $"{playerName} Was kicked due to an error!"; + break; + case DisconnectReasons.Unknown: + ReasonText = $"{playerName} Left the game due to unknown reason?"; + break; + default: + ReasonText = $"{playerName} Left the game!"; + break; + } + + DestroyableSingleton.Instance.Notifier.AddDisconnectMessage(ReasonText); + return false; + } +} diff --git a/Patches/RoleManagerPatch.cs b/Patches/RoleManagerPatch.cs new file mode 100644 index 0000000..ec1b58b --- /dev/null +++ b/Patches/RoleManagerPatch.cs @@ -0,0 +1,219 @@ +using AmongUs.GameOptions; +using HarmonyLib; + +namespace BetterAmongUs.Role; + +[HarmonyPatch(typeof(RoleManager))] +public class RoleManagerPatch +{ + public static Dictionary SetPlayerRole = []; // Player, Role + public static Dictionary ImpostorMultiplier = []; // HashPuid, Multiplier + private static Random random = new Random(); + + // Better role algorithm + [HarmonyPatch(nameof(RoleManager.SelectRoles))] + [HarmonyPrefix] + public static bool RoleManager_Prefix(/*RoleManager __instance*/) + { + if (!Main.BetterRoleAlgorithma.Value) return true; + + Logger.Log($" Better Role Assignment Has Started!", "RoleManager"); + Logger.Log($"--------------------------------------------", "RoleManager"); + + // Set roles up + foreach (var addplayer in Main.AllPlayerControls.Where(pc => !ImpostorMultiplier.ContainsKey(Utils.GetHashPuid(pc)))) + ImpostorMultiplier[Utils.GetHashPuid(addplayer)] = 0; + + int NumImpostors = GameOptionsManager.Instance.CurrentGameOptions.NumImpostors; + + List Impostors = []; + List Crewmates = []; + + Dictionary ImpostorRoles = new() // Role, Amount + { + { RoleTypes.Shapeshifter, 0 }, + { RoleTypes.Phantom, 0 } + }; + + Dictionary CrewmateRoles = new() // Role, Amount + { + { RoleTypes.Engineer, 0 }, + { RoleTypes.Scientist, 0 }, + { RoleTypes.Tracker, 0 }, + { RoleTypes.Noisemaker, 0 } + }; + + List Roles = [.. ImpostorRoles.Keys, .. CrewmateRoles.Keys]; + + foreach (RoleTypes role in Roles) + { + if (IsImpostorRole(role)) + ImpostorRoles[role] = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(role); + else + CrewmateRoles[role] = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(role); + } + + // Override player role assignment + if (SetPlayerRole.Keys.Any()) + { + foreach (var kvp in SetPlayerRole.Where(kvp => kvp.Key != null).OrderBy(kvp => kvp.Key == PlayerControl.LocalPlayer ? 0 : 1)) + { + var player = kvp.Key; + var role = kvp.Value; + + if (role is RoleTypes.Impostor) + { + if (Impostors.Count < NumImpostors) + { + Impostors.Add(player); + player.RpcSetRole(RoleTypes.Impostor); + player.roleAssigned = true; + Logger.Log($"Override Assigned {Utils.GetRoleName(RoleTypes.Impostor)} role to {player.Data.PlayerName}", "RoleManager"); + } + else continue; + } + else if (role is RoleTypes.Crewmate) + { + Crewmates.Add(player); + player.RpcSetRole(RoleTypes.Crewmate); + player.roleAssigned = true; + Logger.Log($"Override Assigned {Utils.GetRoleName(RoleTypes.Crewmate)} role to {player.Data.PlayerName}", "RoleManager"); + } + else + { + if (IsImpostorRole(role)) + { + if (Impostors.Count < NumImpostors && ImpostorRoles[role] > 0) + { + ImpostorRoles[role]--; + Impostors.Add(player); + } + else continue; + } + else + { + if (CrewmateRoles[role] > 0) + { + CrewmateRoles[role]--; + Crewmates.Add(player); + } + else continue; + } + + player.RpcSetRole(role); + player.roleAssigned = true; + Logger.Log($"Override Assigned {Utils.GetRoleName(role)} role to {player.Data.PlayerName}", "RoleManager"); + } + } + } + + // Get players in random order + List players = Main.AllPlayerControls + .Where(player => !Impostors.Contains(player) && !Crewmates.Contains(player) && player.roleAssigned == false) + .ToList(); + + int n = players.Count; + while (n > 1) + { + n--; + int k = random.Next(n + 1); + PlayerControl value = players[k]; + players[k] = players[n]; + players[n] = value; + } + + // Assign roles + foreach (PlayerControl pc in players) + { + if (pc == null || pc.roleAssigned == true) continue; + + if (Impostors.Count < NumImpostors && RNG() > ImpostorMultiplier[Utils.GetHashPuid(pc)]) + { + var impRoles = Shuffle(ImpostorRoles); + foreach (var kvp in impRoles) + { + if (RNG() < GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(kvp.Key) && kvp.Value > 0) + { + ImpostorMultiplier[Utils.GetHashPuid(pc)] += 15; + ImpostorRoles[kvp.Key]--; + Impostors.Add(pc); + pc.RpcSetRole(kvp.Key); + pc.roleAssigned = true; + Logger.Log($"Assigned {Utils.GetRoleName(kvp.Key)} role to {pc.Data.PlayerName}", "RoleManager"); + break; + } + } + + if (!Impostors.Contains(pc)) + { + ImpostorMultiplier[Utils.GetHashPuid(pc)] += 15; + Impostors.Add(pc); + pc.RpcSetRole(RoleTypes.Impostor); + pc.roleAssigned = true; + Logger.Log($"Assigned {Utils.GetRoleName(RoleTypes.Impostor)} role to {pc.Data.PlayerName}", "RoleManager"); + } + } + else + { + var crewRoles = Shuffle(CrewmateRoles); + foreach (var kvp in crewRoles) + { + if (RNG() < GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(kvp.Key) && kvp.Value > 0) + { + ImpostorMultiplier[Utils.GetHashPuid(pc)] = 0; + CrewmateRoles[kvp.Key]--; + Crewmates.Add(pc); + pc.RpcSetRole(kvp.Key); + pc.roleAssigned = true; + Logger.Log($"Assigned {Utils.GetRoleName(kvp.Key)} role to {pc.Data.PlayerName}", "RoleManager"); + break; + } + } + + if (!Crewmates.Contains(pc)) + { + ImpostorMultiplier[Utils.GetHashPuid(pc)] = 0; + Crewmates.Add(pc); + pc.RpcSetRole(RoleTypes.Crewmate); + pc.roleAssigned = true; + Logger.Log($"Assigned {Utils.GetRoleName(RoleTypes.Crewmate)} role to {pc.Data.PlayerName}", "RoleManager"); + } + } + } + + SetPlayerRole.Clear(); + RPC.SyncAllNames(); + + Logger.Log($"--------------------------------------------", "RoleManager"); + Logger.Log($" Better Role Assignment Has Finished!", "RoleManager"); + + return false; + } + + private static bool IsImpostorRole(RoleTypes role) => role is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.Phantom; + + private static Dictionary Shuffle(Dictionary dictionary) + { + List keys = dictionary.Keys.ToList(); + + // Fisher-Yates shuffle algorithm + for (int i = keys.Count - 1; i > 0; i--) + { + int j = random.Next(0, i + 1); + TKey temp = keys[i]; + keys[i] = keys[j]; + keys[j] = temp; + } + + // Rebuild dictionary with shuffled keys + Dictionary shuffledDictionary = new Dictionary(); + foreach (var key in keys) + { + shuffledDictionary[key] = dictionary[key]; + } + + return shuffledDictionary; + } + + private static int RNG() => random.Next(0, 100); +} \ No newline at end of file diff --git a/Patches/VersionShowerPatch.cs b/Patches/VersionShowerPatch.cs new file mode 100644 index 0000000..f952dfb --- /dev/null +++ b/Patches/VersionShowerPatch.cs @@ -0,0 +1,14 @@ +using HarmonyLib; + +namespace BetterAmongUs.Patches; + +[HarmonyPatch(typeof(VersionShower))] +public class VersionShowerPatch +{ + [HarmonyPatch(nameof(VersionShower.Start))] + [HarmonyPostfix] + public static void Postfix(VersionShower __instance) + { + __instance.text.text = $"♻BetterAmongUs♻ {Main.GetVersionText()} - " + __instance.text.text; + } +} diff --git a/Resources/DebugWindowRes/ReactorCoroutines/Coroutines.cs b/Resources/DebugWindowRes/ReactorCoroutines/Coroutines.cs new file mode 100644 index 0000000..51fde9c --- /dev/null +++ b/Resources/DebugWindowRes/ReactorCoroutines/Coroutines.cs @@ -0,0 +1,56 @@ +using BepInEx.Unity.IL2CPP.Utils; +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace BetterAmongUs.Resources; + +public static class Coroutines +{ + internal sealed class Component : MonoBehaviour + { + internal static Component? Instance { get; set; } + + public Component(IntPtr ptr) : base(ptr) + { + } + + private void Awake() + { + Instance = this; + } + + private void OnDestroy() + { + Instance = null; + } + } + + internal static readonly ConditionalWeakTable _ourCoroutineStore = new(); + + internal static IEnumerator GenericRoutine(IEnumerator coroutine) + { + yield return coroutine; + _ourCoroutineStore.Remove(coroutine); + } + + [return: NotNullIfNotNull("coroutine")] + public static IEnumerator? Start(IEnumerator? coroutine) + { + if (coroutine != null) + { + _ourCoroutineStore.AddOrUpdate(coroutine, Component.Instance!.StartCoroutine(GenericRoutine(coroutine))); + } + + return coroutine; + } + + public static void Stop(IEnumerator? coroutine) + { + if (coroutine != null && _ourCoroutineStore.TryGetValue(coroutine, out var routine)) + { + Component.Instance!.StopCoroutine(routine); + } + } +} \ No newline at end of file diff --git a/Resources/DebugWindowRes/ReactorImGui/DragWindow.cs b/Resources/DebugWindowRes/ReactorImGui/DragWindow.cs new file mode 100644 index 0000000..9c80203 --- /dev/null +++ b/Resources/DebugWindowRes/ReactorImGui/DragWindow.cs @@ -0,0 +1,20 @@ +using UnityEngine; + +namespace BetterAmongUs.Resources; + +public class DragWindow : Window +{ + public DragWindow(Rect rect, string title, Action func) : base(rect, title, func) + { + Func = id => + { + func(id); + + GUI.DragWindow(new Rect(0, 0, 10000, 20)); + Rect = Rect.ClampScreen(); + }; + } + public DragWindow(Rect rect, string title, Action func) : this(rect, title, _ => func()) + { + } +} diff --git a/Resources/DebugWindowRes/ReactorImGui/GUIExtensions.cs b/Resources/DebugWindowRes/ReactorImGui/GUIExtensions.cs new file mode 100644 index 0000000..4eefc1a --- /dev/null +++ b/Resources/DebugWindowRes/ReactorImGui/GUIExtensions.cs @@ -0,0 +1,29 @@ +using UnityEngine; + +namespace BetterAmongUs.Resources; + +public static class GUIExtensions +{ + public static Rect ClampScreen(this Rect rect) + { + rect.x = Mathf.Clamp(rect.x, 0, Screen.width - rect.width); + rect.y = Mathf.Clamp(rect.y, 0, Screen.height - rect.height); + + return rect; + } + public static Rect ResetSize(this Rect rect) + { + rect.width = rect.height = 0; + + return rect; + } + public static Sprite CreateSprite(this Texture2D texture, Vector2? pivot = null, float pixelsPerUnit = 100f) + { + return Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), pivot ?? new Vector2(0.5f, 0.5f), pixelsPerUnit); + } + public static void SetSize(this RectTransform rectTransform, float width, float height) + { + rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, width); + rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height); + } +} diff --git a/Resources/DebugWindowRes/ReactorImGui/Window.cs b/Resources/DebugWindowRes/ReactorImGui/Window.cs new file mode 100644 index 0000000..62f0e1e --- /dev/null +++ b/Resources/DebugWindowRes/ReactorImGui/Window.cs @@ -0,0 +1,44 @@ +using UnityEngine; + +namespace BetterAmongUs.Resources; + +public class Window +{ + private static int _lastWindowId; + public static int NextWindowId() + { + return _lastWindowId++; + } + public int Id { get; set; } = NextWindowId(); + public bool Enabled { get; set; } = true; + public Rect Rect { get; set; } + public Action Func { get; set; } + public string Title { get; set; } + public Window(Rect rect, string title, Action func) + { + Rect = rect; + Title = title; + Func = func; + } + public Window(Rect rect, string title, Action func) : this(rect, title, _ => func()) + { + } + public virtual void OnGUI() + { + if (Enabled) + { + if (Event.current.type == EventType.Layout) + { + Rect = Rect.ResetSize(); + } + + GUI.skin.label.wordWrap = false; + Rect = GUILayout.Window(Id, Rect, Func, Title, GUILayout.MinWidth(GUI.skin.label.CalcSize(new GUIContent(Title)).x * 2)); + + if ((Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1)) && Rect.Contains(new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y))) + { + Input.ResetInputAxes(); + } + } + } +} \ No newline at end of file diff --git a/Resources/Images/BetterAmongUs-Icon.png b/Resources/Images/BetterAmongUs-Icon.png new file mode 100644 index 0000000..23b3ce8 Binary files /dev/null and b/Resources/Images/BetterAmongUs-Icon.png differ diff --git a/Resources/Images/BetterAmongUs-Logo.png b/Resources/Images/BetterAmongUs-Logo.png new file mode 100644 index 0000000..878c67b Binary files /dev/null and b/Resources/Images/BetterAmongUs-Logo.png differ diff --git a/Resources/Images/BetterAmongUs-Mod.png b/Resources/Images/BetterAmongUs-Mod.png new file mode 100644 index 0000000..761fccc Binary files /dev/null and b/Resources/Images/BetterAmongUs-Mod.png differ diff --git a/main.cs b/main.cs new file mode 100644 index 0000000..abf5e9c --- /dev/null +++ b/main.cs @@ -0,0 +1,134 @@ +using UnityEngine; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Logging; +using HarmonyLib; +using Innersloth.IO; +using Il2CppInterop.Runtime.Injection; + +namespace BetterAmongUs; + +public enum ReleaseTypes : int +{ + Release, + Canary, + Dev, +} + +[BepInPlugin(PluginGuid, "BetterAmongUs", PluginVersion)] +[BepInProcess("Among Us.exe")] +public class Main : BasePlugin +{ + public const ReleaseTypes ReleaseBuildType = ReleaseTypes.Canary; + public const string CanaryNum = "1"; + public const string PluginGuid = "com.d1gq.betteramongus"; + public const string PluginVersion = "1.0.0"; + public const string ReleaseDate = "07.27.2024"; // dd/mm/yyyy + public const string Github = "https://github.com/D1GQ/BetterAmongUs"; + public static string GetVersionText() + { + if (ReleaseBuildType == ReleaseTypes.Release) + return $"v{BetterAmongUsVersion}"; + else if (ReleaseBuildType == ReleaseTypes.Canary) + return $"v{BetterAmongUsVersion} Canary {Main.CanaryNum}"; + else if (Main.ReleaseBuildType == ReleaseTypes.Dev) + return $"v{BetterAmongUsVersion} Dev {Main.ReleaseDate}"; + } + public Harmony Harmony { get; } = new Harmony(PluginGuid); + + public static string BetterAmongUsVersion => PluginVersion; + public static string AmongUsVersion => Application.version; + + public static List SupportedAmongUsVersions = + [ + "2024.6.18", + ]; + + public static string[] DevUser = + [ + "8f23c48e2", + ]; + + public static PlayerControl[] AllPlayerControls => PlayerControl.AllPlayerControls.ToArray().Where(pc => pc != null).ToArray(); + + public static PlayerControl[] AllAlivePlayerControls => AllPlayerControls.ToArray().Where(pc => pc.IsAlive()).ToArray(); + + public static Dictionary GetRoleName = new Dictionary + { + { 0, "Crewmate" }, + { 1, "Impostor" }, + { 2, "Scientist" }, + { 3, "Engineer" }, + { 4, "Guardian Angel" }, + { 5, "Shapeshifter" }, + { 6, "Crewmate" }, + { 7, "Impostor" }, + { 8, "Noisemaker" }, + { 9, "Phantom" }, + { 10, "Tracker" } + }; + + public static ManualLogSource Logger; + public static DebugMenu debugmenu { get; set; } = null; + + public override void Load() + { + try + { + Logger = BepInEx.Logging.Logger.CreateLogSource(PluginGuid); + + Harmony.PatchAll(); + BAUDataManager.SetUp(); + BAUDataManager.LoadCheatData(); + LoadOptions(); + + if (File.Exists(Path.Combine($"{Environment.CurrentDirectory}/Among Us_Data/Plugins/x86", "steam_api.dll"))) + File.WriteAllText(Path.Combine(Environment.CurrentDirectory, "steam_appid.txt"), "945360"); + + if (File.Exists(Path.Combine(Environment.CurrentDirectory, "better-log.txt"))) + File.WriteAllText(Path.Combine(Environment.CurrentDirectory, "better-previous-log.txt"), File.ReadAllText(Path.Combine(Environment.CurrentDirectory, "better-log.txt"))); + + File.WriteAllText(Path.Combine(Environment.CurrentDirectory, "better-log.txt"), ""); + BetterAmongUs.Logger.Log("Better Among Us successfully loaded!"); + + // Set up debug menu + ClassInjector.RegisterTypeInIl2Cpp(); + ClassInjector.RegisterTypeInIl2Cpp(); + debugmenu = AddComponent(); + AddComponent(); + + string SupportedVersions = string.Empty; + foreach (string text in SupportedAmongUsVersions.ToArray()) + SupportedVersions += $"{text} "; + BetterAmongUs.Logger.Log($"BetterAmongUs {BetterAmongUsVersion}-{ReleaseDate} - [{AmongUsVersion} --> {SupportedVersions.Substring(0, SupportedVersions.Length - 1)}]"); + } + catch (Exception ex) + { + BetterAmongUs.Logger.Error(ex.ToString()); + } + } + + public static ConfigEntry AntiCheat { get; private set; } + public static ConfigEntry BetterHost { get; private set; } + public static ConfigEntry BetterRoleAlgorithma { get; private set; } + public static ConfigEntry LobbyPlayerInfo { get; private set; } + public static ConfigEntry DisableLobbyTheme { get; private set; } + public static ConfigEntry UnlockFPS { get; private set; } + public static ConfigEntry ShowFPS { get; private set; } + public static ConfigEntry CommandPrefix { get; set; } + private void LoadOptions() + { + AntiCheat = Config.Bind("Better Options", "AntiCheat", true); + BetterHost = Config.Bind("Better Options", "BetterHost", false); + BetterRoleAlgorithma = Config.Bind("Better Options", "BetterRoleAlgorithma", true); + LobbyPlayerInfo = Config.Bind("Better Options", "LobbyPlayerInfo", true); + DisableLobbyTheme = Config.Bind("Better Options", "DisableLobbyTheme", true); + UnlockFPS = Config.Bind("Better Options", "UnlockFPS", false); + ShowFPS = Config.Bind("Better Options", "ShowFPS", false); + CommandPrefix = Config.Bind("Client Options", "CommandPrefix", "/"); + } + + public static string GetDataPathToAmongUs() => FileIO.GetRootDataPath(); + public static string GetGamePathToAmongUs() => Environment.CurrentDirectory; +}