commit 834cc6463ba29654ae5c29983f3b66fdc3be83ab Author: Anton Tarasenko Date: Sat Jul 18 19:26:04 2020 +0700 Initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -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/sources/FixAmmoSelling/AmmoPickupStalker.uc b/sources/FixAmmoSelling/AmmoPickupStalker.uc new file mode 100644 index 0000000..603f2f9 --- /dev/null +++ b/sources/FixAmmoSelling/AmmoPickupStalker.uc @@ -0,0 +1,85 @@ +/** + * This actor attaches itself to the ammo boxes + * and imitates their collision to let us detect when they're picked up. + * Copyright 2020 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class AmmoPickupStalker extends Actor; + +// Ammo box this stalker is attached to. +// If it is destroyed (not just picked up) - stalker must die too. +var private KFAmmoPickup target; + +// This variable is used to record if our 'target' ammo box was in +// active state ('Pickup') last time we've checked. +// We need this because ammo box's 'Touch' event can fire off first and +// force the box to sleep before stalker could catch same event. +// Without this variable we would have no way to know if player +// simply walked near the place of a sleeping box or actually grabbed it. +var private bool wasActive; + +// Static function that spawns a new stalker for the given box. +// Careful, as there's no checks for whether a stalker is +// already attached to it. +// Ensuring that is on the user of the function. +public final static function StalkAmmoPickup(KFAmmoPickup newTarget) +{ + local AmmoPickupStalker newStalker; + if (newTarget == none) return; + + newStalker = newTarget.Spawn(class'AmmoPickupStalker'); + newStalker.target = newTarget; + newStalker.SetBase(newTarget); + newStalker.SetCollision(true); + newStalker.SetCollisionSize(newTarget.collisionRadius, + newTarget.collisionHeight); +} + +event Touch(Actor other) +{ + local FixAmmoSelling ammoSellingFix; + if (target == none) return; + // If our box was sleeping for while (more than a tick), - + // player couldn't have gotten any ammo. + if (!wasActive && !target.IsInState('Pickup')) return; + + ammoSellingFix = FixAmmoSelling(class'FixAmmoSelling'.static.GetInstance()); + if (ammoSellingFix != none) + { + ammoSellingFix.RecordAmmoPickup(Pawn(other), target); + } +} + +event Tick(float delta) +{ + if (target != none) + { + wasActive = target.IsInState('Pickup'); + } + else + { + Destroy(); + } +} + +defaultproperties +{ + // Server-only, hidden + remoteRole = ROLE_None + bAlwaysRelevant = true + drawType = DT_None +} \ No newline at end of file diff --git a/sources/FixAmmoSelling/FixAmmoSelling.uc b/sources/FixAmmoSelling/FixAmmoSelling.uc new file mode 100644 index 0000000..5c0320e --- /dev/null +++ b/sources/FixAmmoSelling/FixAmmoSelling.uc @@ -0,0 +1,396 @@ +/** + * This feature addressed an oversight in vanilla code that + * allows clients to sell weapon's ammunition. + * Moreover, when being sold, ammunition cost is always multiplied by 0.75, + * without taking into an account possible discount a player might have. + * This allows cheaters to "print money" by buying and selling ammo over and + * over again ammunition for some weapons, + * notably pipe bombs (74% discount for lvl6 demolition) + * and crossbow (42% discount for lvl6 sharpshooter). + * + * This feature fixes this problem by setting 'pickupClass' variable in + * potentially abusable weapons to our own value that won't receive a discount. + * Luckily for us, it seems that pickup spawn and discount checks are the only + * two place where variable is directly checked in a vanilla game's code + * ('default.pickupClass' is used everywhere else), + * so we can easily deal with the side effects of such change. + * Copyright 2020 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixAmmoSelling extends Feature + config(AcediaFixes); + +/** + * We will replace 'pickupClass' variable for all instances of potentially + * abusable weapons. That is weapons, that have a discount for their ammunition + * (via 'GetAmmoCostScaling' function in a corresponding perk class). + * They are defined (along with our pickup replacements) in 'rules' array. + * That array isn't configurable, since the abusable status is hardcoded into + * perk classes and the main mod that allows to change those (ServerPerks), + * also solves ammo selling by a more direct method + * (only available for the mods that replace player pawn class). + * This change already completely fixes ammo printing. + * Possible concern with changing the value of 'pickupClass' is that + * it might affect gameplay in too many ways. + * But, luckily for us, that value is only used when spawning a new pickup and + * in 'ServerBuyAmmo' function of 'KFPawn' + * (all the other places use it's default value instead). + * This means that the only two side-effects of our change are: + * 1. That wrong pickup class will be spawned. This problem is easily + * solved by replacing spawned actor in 'CheckReplacement'. + * 2. That ammo will be sold at a different (lower for us) price, + * while trader would still display and require the original price. + * This problem is solved by manually taking from player the difference + * between what he should have had to pay and what he actually paid. + * This brings us to the second issue - + * detecting when player bought the ammo. + * Unfortunately, it doesn't seem possible to detect with 100% certainty + * without replacing pawn or shop classes, + * so we have to eliminate other possibilities. + * There are seem to be three ways for players to get more ammo: + * 1. For some mod to give it; + * 2. Found it an ammo box; + * 3. To buy ammo (can only happen in trader). + * We don't want to provide mods with low-level API for bug fixes, + * so to ensure the compatibility, mods that want to increase ammo values + * will have to solve compatibility issue by themselves: + * either by reimplementing this fix (possibly the best option) + * or by giving players appropriate money along with the ammo. + * The only other case we have to eliminate is ammo boxes. + * First, all cases of ammo boxes outside the trader are easy to detect, + * since in this case we can be sure that player didn't buy ammo + * (and mods that can allow it can just get rid of + * 'ServerSellAmmo' function directly, similarly to how ServerPerks does it). + * We'll detect all the other boxes by attaching an auxiliary actor + * ('AmmoPickupStalker') to them, that will fire off 'Touch' event + * at the same time as ammo boxes. + * The only possible problem is that part of the ammo cost is + * taken with a slight delay, which leaves cheaters a window of opportunity + * to buy more than they can afford. + * This issue is addressed by each ammo type costing as little as possible + * (its' cost for corresponding perk at lvl6) + * and a flag that does allow players to go into negative dosh values + * (the cost is potential bugs in this fix itself, that + * can somewhat affect regular players). + */ + +// Due to how this fix works, players with level below 6 get charged less +// than necessary by the shop and this fix must take the rest of +// the cost by itself. +// The problem is, due to how ammo purchase is coded, low-level (<6 lvl) +// players can actually buy more ammo for "fixed" weapons than they can afford +// by filling ammo for one or all weapons. +// Setting this flag to 'true' will allow us to still take full cost +// from them, putting them in "debt" (having negative dosh amount). +// If you don't want to have players with negative dosh values on your server +// as a side-effect of this fix, then leave this flag as 'false', +// letting low level players buy ammo cheaper +// (but not cheaper than lvl6 could). +// NOTE: this issue doesn't affect level 6 players. +// NOTE #2: this fix does give players below level 6 some +// technical advantage compared to vanilla game, but this advantage +// cannot exceed benefits of having level 6. +var private config const bool allowNegativeDosh; + +// This structure records what classes of weapons can be abused +// and what pickup class we should use to fix the exploit. +struct ReplacementRule +{ + var class abusableWeapon; + var class pickupReplacement; +}; + +// Actual list of abusable weapons. +var private const array rules; + +// We create one such record for any +// abusable weapon instance in the game to store: +struct WeaponRecord +{ + // The instance itself. + var KFWeapon weapon; + // Corresponding ammo instance + // (all abusable weapons only have one ammo type). + var KFAmmunition ammo; + // Last ammo amount we've seen, used to detect players gaining ammo + // (from either ammo boxes or buying it). + var int lastAmmoAmount; +}; + +// All weapons we've detected so far. +var private array registeredWeapons; + +protected function OnEnabled() +{ + local KFWeapon nextWeapon; + local KFAmmoPickup nextPickup; + // Find all abusable weapons + foreach level.DynamicActors(class'KFMod.KFWeapon', nextWeapon) + { + FixWeapon(nextWeapon); + } + // Start tracking all ammo boxes + foreach level.DynamicActors(class'KFMod.KFAmmoPickup', nextPickup) + { + class'AmmoPickupStalker'.static.StalkAmmoPickup(nextPickup); + } +} + +protected function OnDisabled() +{ + local int i; + local AmmoPickupStalker nextStalker; + local array stalkers; + // Restore all the 'pickupClass' variables we've changed. + for (i = 0; i < registeredWeapons.length; i += 1) + { + if (registeredWeapons[i].weapon != none) + { + registeredWeapons[i].weapon.pickupClass = + registeredWeapons[i].weapon.default.pickupClass; + } + } + registeredWeapons.length = 0; + // Kill all the stalkers; + // to be safe, avoid destroying them directly in the iterator. + foreach level.DynamicActors(class'AmmoPickupStalker', nextStalker) + { + stalkers[stalkers.length] = nextStalker; + } + for (i = 0; i < stalkers.length; i += 1) + { + if (stalkers[i] != none) + { + stalkers[i].Destroy(); + } + } +} + +// Checks if given class is a one of our pickup replacer classes. +public static final function bool IsReplacer(class pickupClass) +{ + local int i; + if (pickupClass == none) return false; + for (i = 0; i < default.rules.length; i += 1) + { + if (pickupClass == default.rules[i].pickupReplacement) + { + return true; + } + } + return false; +} + +// 1. Checks if weapon can be abused and if it can, - fixes the problem. +// 2. Starts tracking abusable weapon to detect when player buys ammo for it. +public final function FixWeapon(KFWeapon potentialAbuser) +{ + local int i; + local WeaponRecord newRecord; + if (potentialAbuser == none) return; + + for (i = 0; i < registeredWeapons.length; i += 1) + { + if (registeredWeapons[i].weapon == potentialAbuser) + { + return; + } + } + for (i = 0; i < rules.length; i += 1) + { + if (potentialAbuser.class == rules[i].abusableWeapon) + { + potentialAbuser.pickupClass = rules[i].pickupReplacement; + newRecord.weapon = potentialAbuser; + registeredWeapons[registeredWeapons.length] = newRecord; + return; + } + } +} + +// Finds ammo instance for recorded weapon in it's owner's inventory. +private final function WeaponRecord FindAmmoInstance(WeaponRecord record) +{ + local Inventory invIter; + local KFAmmunition ammo; + if (record.weapon == none) return record; + if (record.weapon.instigator == none) return record; + + // Find instances anew + invIter = record.weapon.instigator.inventory; + while (invIter != none) + { + if (record.weapon.ammoClass[0] == invIter.class) + { + ammo = KFAmmunition(invIter); + } + invIter = invIter.inventory; + } + // Add missing instances + if (ammo != none) + { + record.ammo = ammo; + record.lastAmmoAmount = ammo.ammoAmount; + } + return record; +} + +// Calculates how much more player should have paid for 'ammoAmount' +// amount of ammo, compared to how much trader took after our fix. +private final function float GetPriceCorrection +( + KFWeapon kfWeapon, + int ammoAmount +) +{ + local float boughtMagFraction; + // 'vanillaPrice' - price that would be calculated + // without our interference + // 'fixPrice' - price that will be calculated after + // we've replaced pickup class + local float vanillaPrice, fixPrice; + local KFPlayerReplicationInfo kfRI; + local class vanillaPickupClass, fixPickupClass; + if (kfWeapon == none || kfWeapon.instigator == none) return 0.0; + fixPickupClass = class(kfWeapon.pickupClass); + vanillaPickupClass = class(kfWeapon.default.pickupClass); + if (fixPickupClass == none || vanillaPickupClass == none) return 0.0; + + // Calculate base prices + boughtMagFraction = (float(ammoAmount) / kfWeapon.default.magCapacity); + fixPrice = boughtMagFraction * fixPickupClass.default.AmmoCost; + vanillaPrice = boughtMagFraction * vanillaPickupClass.default.AmmoCost; + // Apply perk discount for vanilla price + // (we don't need to consider secondary ammo or husk gun special cases, + // since such weapons can't be abused via ammo dosh-printing) + kfRI = KFPlayerReplicationInfo(kfWeapon.instigator.playerReplicationInfo); + if (kfRI != none && kfRI.clientVeteranSkill != none) + { + vanillaPrice *= kfRI.clientVeteranSkill.static. + GetAmmoCostScaling(kfRI, vanillaPickupClass); + } + // TWI's code rounds up ammo cost + // to the integer value whenever ammo is bought, + // so to calculate exactly how much we need to correct the cost, + // we must find difference between the final, rounded cost values. + return float(Max(0, int(vanillaPrice) - int(fixPrice))); +} + +// Takes current ammo and last recorded in 'record' value to calculate +// how much money to take from the player +// (calculations are done via 'GetPriceCorrection'). +private final function WeaponRecord TaxAmmoChange(WeaponRecord record) +{ + local int ammoDiff; + local KFPawn taxPayer; + local PlayerReplicationInfo replicationInfo; + taxPayer = KFPawn(record.weapon.instigator); + if (record.weapon == none || taxPayer == none) return record; + // No need to charge money if player couldn't have + // possibly bought the ammo. + if (!taxPayer.CanBuyNow()) return record; + // Find ammo difference with recorded value. + if (record.ammo != none) + { + ammoDiff = Max(0, record.ammo.ammoAmount - record.lastAmmoAmount); + record.lastAmmoAmount = record.ammo.ammoAmount; + } + // Make player pay dosh + replicationInfo = taxPayer.playerReplicationInfo; + if (replicationInfo != none) + { + replicationInfo.score -= GetPriceCorrection(record.weapon, ammoDiff); + // This shouldn't happen, since shop is supposed to make sure + // player has enough dosh to buy ammo at full price + // (actual price + our correction). + // But if user is extra concerned about it, - + // we can additionally for force the score above 0. + if (!allowNegativeDosh) + { + replicationInfo.score = FMax(0, replicationInfo.score); + } + } + return record; +} + +// Changes our records to account for player picking up the ammo box, +// to avoid charging his for it. +public final function RecordAmmoPickup(Pawn pawnWithAmmo, KFAmmoPickup pickup) +{ + local int i; + local int newAmount; + // Check conditions from 'KFAmmoPickup' code ('Touch' function) + if (pickup == none) return; + if (pawnWithAmmo == none) return; + if (pawnWithAmmo.controller == none) return; + if (!pawnWithAmmo.bCanPickupInventory) return; + if (!FastTrace(pawnWithAmmo.location, pickup.location)) return; + + // Add relevant amount of ammo to our records + for (i = 0; i < registeredWeapons.length; i += 1) + { + if (registeredWeapons[i].weapon == none) continue; + if (registeredWeapons[i].weapon.instigator == pawnWithAmmo) + { + newAmount = registeredWeapons[i].lastAmmoAmount + + registeredWeapons[i].ammo.ammoPickupAmount; + newAmount = Min(registeredWeapons[i].ammo.maxAmmo, newAmount); + registeredWeapons[i].lastAmmoAmount = newAmount; + } + } +} + +event Tick(float delta) +{ + local int i; + // For all the weapon records... + i = 0; + while (i < registeredWeapons.length) + { + // ...remove dead records + if (registeredWeapons[i].weapon == none) + { + registeredWeapons.Remove(i, 1); + continue; + } + // ...find ammo if it's missing + if (registeredWeapons[i].ammo == none) + { + registeredWeapons[i] = FindAmmoInstance(registeredWeapons[i]); + } + // ...tax for ammo, if we can + registeredWeapons[i] = TaxAmmoChange(registeredWeapons[i]); + i += 1; + } +} + +defaultproperties +{ + allowNegativeDosh = false + rules(0)=(abusableWeapon=class'KFMod.Crossbow',pickupReplacement=class'FixAmmoSellingClass_CrossbowPickup') + rules(1)=(abusableWeapon=class'KFMod.PipeBombExplosive',pickupReplacement=class'FixAmmoSellingClass_PipeBombPickup') + rules(2)=(abusableWeapon=class'KFMod.M79GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_M79Pickup') + rules(3)=(abusableWeapon=class'KFMod.GoldenM79GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_GoldenM79Pickup') + rules(4)=(abusableWeapon=class'KFMod.M32GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_M32Pickup') + rules(5)=(abusableWeapon=class'KFMod.CamoM32GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_CamoM32Pickup') + rules(6)=(abusableWeapon=class'KFMod.LAW',pickupReplacement=class'FixAmmoSellingClass_LAWPickup') + rules(7)=(abusableWeapon=class'KFMod.SPGrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_SPGrenadePickup') + rules(8)=(abusableWeapon=class'KFMod.SealSquealHarpoonBomber',pickupReplacement=class'FixAmmoSellingClass_SealSquealPickup') + rules(9)=(abusableWeapon=class'KFMod.SeekerSixRocketLauncher',pickupReplacement=class'FixAmmoSellingClass_SeekerSixPickup') + // Listeners + requiredListeners(0) = class'MutatorListener_FixAmmoSelling' +} \ No newline at end of file diff --git a/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CamoM32Pickup.uc b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CamoM32Pickup.uc new file mode 100644 index 0000000..9c55c9e --- /dev/null +++ b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CamoM32Pickup.uc @@ -0,0 +1,26 @@ +/** + * A helper class for 'FixAmmoSelling' that sets ammo cost for M32 to that + * of a level 6 player and doesn't allow for a perk discount. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixAmmoSellingClass_CamoM32Pickup extends CamoM32Pickup; + +defaultproperties +{ + AmmoCost = 42 +} \ No newline at end of file diff --git a/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CrossbowPickup.uc b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CrossbowPickup.uc new file mode 100644 index 0000000..6fd07c1 --- /dev/null +++ b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CrossbowPickup.uc @@ -0,0 +1,26 @@ +/** + * A helper class for 'FixAmmoSelling' that sets ammo cost for xbow to that + * of a level 6 player and doesn't allow for a perk discount. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixAmmoSellingClass_CrossbowPickup extends CrossbowPickup; + +defaultproperties +{ + AmmoCost = 11.6 +} \ No newline at end of file diff --git a/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_GoldenM79Pickup.uc b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_GoldenM79Pickup.uc new file mode 100644 index 0000000..ae1e1a3 --- /dev/null +++ b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_GoldenM79Pickup.uc @@ -0,0 +1,26 @@ +/** + * A helper class for 'FixAmmoSelling' that sets ammo cost for m79 to that + * of a level 6 player and doesn't allow for a perk discount. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixAmmoSellingClass_GoldenM79Pickup extends GoldenM79Pickup; + +defaultproperties +{ + AmmoCost = 7 +} \ No newline at end of file diff --git a/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_LAWPickup.uc b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_LAWPickup.uc new file mode 100644 index 0000000..d1cc802 --- /dev/null +++ b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_LAWPickup.uc @@ -0,0 +1,26 @@ +/** + * A helper class for 'FixAmmoSelling' that sets ammo cost for LAW to that + * of a level 6 player and doesn't allow for a perk discount. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixAmmoSellingClass_LAWPickup extends LAWPickup; + +defaultproperties +{ + AmmoCost = 21 +} \ No newline at end of file diff --git a/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M32Pickup.uc b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M32Pickup.uc new file mode 100644 index 0000000..04f0321 --- /dev/null +++ b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M32Pickup.uc @@ -0,0 +1,26 @@ +/** + * A helper class for 'FixAmmoSelling' that sets ammo cost for M32 to that + * of a level 6 player and doesn't allow for a perk discount. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixAmmoSellingClass_M32Pickup extends M32Pickup; + +defaultproperties +{ + AmmoCost = 42 +} \ No newline at end of file diff --git a/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M79Pickup.uc b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M79Pickup.uc new file mode 100644 index 0000000..21a93f4 --- /dev/null +++ b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M79Pickup.uc @@ -0,0 +1,26 @@ +/** + * A helper class for 'FixAmmoSelling' that sets ammo cost for M79 to that + * of a level 6 player and doesn't allow for a perk discount. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixAmmoSellingClass_M79Pickup extends M79Pickup; + +defaultproperties +{ + AmmoCost = 7 +} \ No newline at end of file diff --git a/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_PipeBombPickup.uc b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_PipeBombPickup.uc new file mode 100644 index 0000000..ff2041c --- /dev/null +++ b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_PipeBombPickup.uc @@ -0,0 +1,26 @@ +/** + * A helper class for 'FixAmmoSelling' that sets ammo cost for pipes + * to that of a level 6 player and doesn't allow for a perk discount. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixAmmoSellingClass_PipeBombPickup extends PipeBombPickup; + +defaultproperties +{ + AmmoCost = 195 +} \ No newline at end of file diff --git a/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SPGrenadePickup.uc b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SPGrenadePickup.uc new file mode 100644 index 0000000..5a1a29c --- /dev/null +++ b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SPGrenadePickup.uc @@ -0,0 +1,27 @@ +/** + * A helper class for 'FixAmmoSelling' that sets ammo cost for + * orca grnade launcher to that of a level 6 player + * and doesn't allow for a perk discount. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixAmmoSellingClass_SPGrenadePickup extends SPGrenadePickup; + +defaultproperties +{ + AmmoCost = 7 +} \ No newline at end of file diff --git a/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SealSquealPickup.uc b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SealSquealPickup.uc new file mode 100644 index 0000000..c80c56f --- /dev/null +++ b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SealSquealPickup.uc @@ -0,0 +1,26 @@ +/** + * A helper class for 'FixAmmoSelling' that sets ammo cost for harpoon + * to that of a level 6 player and doesn't allow for a perk discount. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixAmmoSellingClass_SealSquealPickup extends SealSquealPickup; + +defaultproperties +{ + AmmoCost = 21 +} \ No newline at end of file diff --git a/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SeekerSixPickup.uc b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SeekerSixPickup.uc new file mode 100644 index 0000000..31d3c26 --- /dev/null +++ b/sources/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SeekerSixPickup.uc @@ -0,0 +1,26 @@ +/** + * A helper class for 'FixAmmoSelling' that sets ammo cost for seeker + * to that of a level 6 player and doesn't allow for a perk discount. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixAmmoSellingClass_SeekerSixPickup extends SeekerSixPickup; + +defaultproperties +{ + AmmoCost = 10.5 +} \ No newline at end of file diff --git a/sources/FixAmmoSelling/MutatorListener_FixAmmoSelling.uc b/sources/FixAmmoSelling/MutatorListener_FixAmmoSelling.uc new file mode 100644 index 0000000..78178de --- /dev/null +++ b/sources/FixAmmoSelling/MutatorListener_FixAmmoSelling.uc @@ -0,0 +1,97 @@ +/** + * Overloaded mutator events listener to register every new + * spawned weapon and ammo pickup. + * Copyright 2020 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class MutatorListener_FixAmmoSelling extends MutatorListenerBase + abstract; + +static function bool CheckReplacement(Actor other, out byte isSuperRelevant) +{ + if (other == none) return true; + + // We need to replace pickup classes back, + // as they might not even exist on clients. + if (class'FixAmmoSelling'.static.IsReplacer(other.class)) + { + ReplacePickupWith(Pickup(other)); + return false; + } + CheckAbusableWeapon(KFWeapon(other)); + // If it's ammo pickup - we need to stalk it + class'AmmoPickupStalker'.static.StalkAmmoPickup(KFAmmoPickup(other)); + return true; +} + +private static function CheckAbusableWeapon(KFWeapon newWeapon) +{ + local FixAmmoSelling ammoSellingFix; + if (newWeapon == none) return; + ammoSellingFix = FixAmmoSelling(class'FixAmmoSelling'.static.GetInstance()); + if (ammoSellingFix == none) return; + ammoSellingFix.FixWeapon(newWeapon); +} + +// This function recreates the logic of 'KFWeapon.DropFrom()', +// since standard 'ReplaceWith' function produces bad results. +private static function ReplacePickupWith(Pickup oldPickup) +{ + local Pawn instigator; + local Pickup newPickup; + local KFWeapon relevantWeapon; + if (oldPickup == none) return; + instigator = oldPickup.instigator; + if (instigator == none) return; + relevantWeapon = GetWeaponOfClass(instigator, oldPickup.inventoryType); + if (relevantWeapon == none) return; + + newPickup = relevantWeapon.Spawn( relevantWeapon.default.pickupClass,,, + relevantWeapon.location); + newPickup.InitDroppedPickupFor(relevantWeapon); + newPickup.velocity = relevantWeapon.velocity + + Vector(instigator.rotation) * 100; + if (instigator.health > 0) + KFWeaponPickup(newPickup).bThrown = true; +} + +// TODO: this is code duplication, some sort of solution is needed +static final function KFWeapon GetWeaponOfClass +( + Pawn playerPawn, + class weaponClass +) +{ + local Inventory invIter; + if (playerPawn == none) return none; + + invIter = playerPawn.inventory; + while (invIter != none) + { + if (invIter.class == weaponClass) + { + return KFWeapon(invIter); + } + invIter = invIter.inventory; + } + return none; +} + +defaultproperties +{ + relatedEvents = class'MutatorEvents' +} \ No newline at end of file diff --git a/sources/FixDoshSpam/FixDoshSpam.uc b/sources/FixDoshSpam/FixDoshSpam.uc new file mode 100644 index 0000000..349883f --- /dev/null +++ b/sources/FixDoshSpam/FixDoshSpam.uc @@ -0,0 +1,253 @@ +/** + * This feature addressed two dosh-related issues: + * 1. Crashing servers by spamming 'CashPickup' actors with 'TossCash'; + * 2. Breaking collision detection logic by stacking large amount of + * 'CashPickup' actors in one place, which allows one to either + * reach unintended locations or even instantly kill zeds. + * + * It fixes them by limiting speed, with which dosh can spawn, and + * allowing this limit to decrease when there's already too much dosh + * present on the map. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixDoshSpam extends Feature + config(AcediaFixes); + +/** + * First, we limit amount of dosh that can be spawned simultaneously. + * The simplest method is to place a cooldown on spawning 'CashPickup' actors, + * i.e. after spawning one 'CashPickup' we'd completely prevent spawning + * any other instances of it for a fixed amount of time. + * However, that might allow a malicious spammer to block others from + * throwing dosh, - all he needs to do is to spam dosh at right time intervals. + * We'll resolve this issue by recording how many 'CashPickup' actors + * each player has spawned as their "contribution" and decay + * that value with time, only allowing to spawn new dosh after + * contribution decayed to zero. Speed of decay is derived from current dosh + * spawning speed limit and decreases with amount of players + * with non-zero contributions (since it means that they're throwing dosh). + * Second issue is player amassing a large amount of dosh in one point + * that leads to skipping collision checks, which then allows players to pass + * through level geometry or enter zeds' collisions, instantly killing them. + * Since dosh disappears on it's own, the easiest method to prevent that is to + * severely limit how much dosh players can throw per second, + * so that there's never enough dosh laying around to affect collision logic. + * The downside to such severe limitations is that game behaves less + * vanilla-like, where you could throw away streams of dosh. + * To solve that we'll first use a more generous limit on dosh players can + * throw per second, but will track how much dosh is currently present + * in a level and linearly decelerate speed, according to that amount. + */ + +// Highest and lowest speed with which players can throw dosh wads. +// It'll be evenly spread between all players. +// For example, if speed is set to 6 and only one player will be spamming dosh, +// - he'll be able to throw 6 wads of dosh per second; +// but if all 6 players are spamming it, - each will throw only 1 per second. +// NOTE: these speed values can be exceeded, since a player is guaranteed +// to be able to throw at least one wad of dosh, if he didn't do so in awhile. +// NOTE #2: if maximum value is less than minimum one, +// the lowest (maximum one) will be used. +var private config const float doshPerSecondLimitMax; +var private config const float doshPerSecondLimitMin; +// Amount of dosh pickups on the map at which we must set dosh per second +// to 'doshPerSecondLimitMin'. +// We use 'doshPerSecondLimitMax' when there's no dosh on the map and +// scale linearly between them as it's amount grows. +var private config const int criticalDoshAmount; + +// To limit dosh spawning speed we need some measure of +// time passage between ticks. +// This variable stores last value seen by us as a good approximation. +// It's a real (not in-game) time. +var private float lastTickDuration; + +// This structure records how much a certain player has +// contributed to an overall dosh creation. +struct DoshStreamPerPlayer +{ + var PlayerController player; + // Amount of dosh we remember this player creating, decays with time. + var float contribution; +}; +var private array currentContributors; + +// Wads of cash that are lying around on the map. +var private array wads; + +protected function OnEnabled() +{ + local CashPickup nextCash; + // Find all wads of cash laying around on the map, + // so that we could accordingly limit the cash spam. + foreach level.DynamicActors(class'KFMod.CashPickup', nextCash) + { + wads[wads.length] = nextCash; + } +} + +protected function OnDisabled() +{ + wads.length = 0; + currentContributors.length = 0; +} + +// Did player with this controller contribute to the latest dosh generation? +public final function bool IsContributor(PlayerController player) +{ + return (GetContributorIndex(player) >= 0); +} + +// Did we already reach allowed limit of dosh per second? +public final function bool IsDoshStreamOverLimit() +{ + local int i; + local float overallContribution; + overallContribution = 0.0; + for (i = 0; i < currentContributors.length; i += 1) + { + overallContribution += currentContributors[i].contribution; + } + return (overallContribution > lastTickDuration * GetCurrentDPSLimit()); +} + +// What is our current dosh per second limit? +private final function float GetCurrentDPSLimit() +{ + local float speedScale; + if (doshPerSecondLimitMax < doshPerSecondLimitMin) + { + return doshPerSecondLimitMax; + } + speedScale = Float(wads.length) / Float(criticalDoshAmount); + speedScale = FClamp(speedScale, 0.0, 1.0); + // At 0.0 scale (no dosh on the map) - use max speed + // At 1.0 scale (critical dosh on the map) - use min speed + return Lerp(speedScale, doshPerSecondLimitMax, doshPerSecondLimitMin); +} + +// Returns index of the contributor corresponding to the given controller. +// Returns '-1' if no connection correspond to the given controller. +// Returns '-1' if given controller is equal to 'none'. +private final function int GetContributorIndex(PlayerController player) +{ + local int i; + if (player == none) return -1; + + for (i = 0; i < currentContributors.length; i += 1) + { + if (currentContributors[i].player == player) + { + return i; + } + } + return -1; +} + +// Adds given cash to given player contribution record and +// registers that cash in our wads array. +// Does nothing if given cash was already registered. +public final function AddContribution(PlayerController player, CashPickup cash) +{ + local int i; + local int playerIndex; + local DoshStreamPerPlayer newStreamRecord; + // Check if given dosh was already accounted for. + for (i = 0; i < wads.length; i += 1) + { + if (cash == wads[i]) + { + return; + } + } + wads[wads.length] = cash; + // Add contribution to player + playerIndex = GetContributorIndex(player); + if (playerIndex >= 0) + { + currentContributors[playerIndex].contribution += 1.0; + return; + } + newStreamRecord.player = player; + newStreamRecord.contribution = 1.0; + currentContributors[currentContributors.length] = newStreamRecord; +} + +private final function ReducePlayerContributions(float trueTimePassed) +{ + local int i; + local float streamReduction; + streamReduction = trueTimePassed * + (GetCurrentDPSLimit() / currentContributors.length); + for (i = 0; i < currentContributors.length; i += 1) + { + currentContributors[i].contribution -= streamReduction; + } +} + +// Clean out wads that disappeared or were picked up by players. +private final function CleanWadsArray() +{ + local int i; + i = 0; + while (i < wads.length) + { + if (wads[i] == none) + { + wads.Remove(i, 1); + } + else + { + i += 1; + } + } +} + +// Don't track players that no longer contribute to dosh generation. +private final function RemoveNonContributors() +{ + local int i; + local array updContributors; + for (i = 0; i < currentContributors.length; i += 1) + { + // We want to keep on record even players that quit, + // since their contribution still must be accounted for. + if (currentContributors[i].contribution <= 0.0) continue; + updContributors[updContributors.length] = currentContributors[i]; + } + currentContributors = updContributors; +} + +event Tick(float delta) +{ + local float trueTimePassed; + trueTimePassed = delta * (1.1 / level.timeDilation); + CleanWadsArray(); + ReducePlayerContributions(trueTimePassed); + RemoveNonContributors(); + lastTickDuration = trueTimePassed; +} + +defaultproperties +{ + doshPerSecondLimitMax = 50 + doshPerSecondLimitMin = 5 + criticalDoshAmount = 25 + // Listeners + requiredListeners(0) = class'MutatorListener_FixDoshSpam' +} \ No newline at end of file diff --git a/sources/FixDoshSpam/MutatorListener_FixDoshSpam.uc b/sources/FixDoshSpam/MutatorListener_FixDoshSpam.uc new file mode 100644 index 0000000..5b85353 --- /dev/null +++ b/sources/FixDoshSpam/MutatorListener_FixDoshSpam.uc @@ -0,0 +1,51 @@ +/** + * Overloaded mutator events listener to catch and, possibly, + * prevent spawning dosh actors. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class MutatorListener_FixDoshSpam extends MutatorListenerBase + abstract; + +static function bool CheckReplacement(Actor other, out byte isSuperRelevant) +{ + local FixDoshSpam doshFix; + local PlayerController player; + if (other.class != class'CashPickup') return true; + // This means this dosh wasn't spawned in 'TossCash' of 'KFPawn', + // so it isn't related to the exploit we're trying to fix. + if (other.instigator == none) return true; + doshFix = FixDoshSpam(class'FixDoshSpam'.static.GetInstance()); + if (doshFix == none) return true; + + // We only want to prevent spawning cash if we're already over + // the limit and the one trying to throw this cash contributed to it. + // We allow other players to throw at least one wad of cash. + player = PlayerController(other.instigator.controller); + if (doshFix.IsDoshStreamOverLimit() && doshFix.IsContributor(player)) + { + return false; + } + // If we do spawn cash - record this contribution. + doshFix.AddContribution(player, CashPickup(other)); + return true; +} + +defaultproperties +{ + relatedEvents = class'MutatorEvents' +} \ No newline at end of file diff --git a/sources/FixDualiesCost/DualiesCostRule.uc b/sources/FixDualiesCost/DualiesCostRule.uc new file mode 100644 index 0000000..aa03156 --- /dev/null +++ b/sources/FixDualiesCost/DualiesCostRule.uc @@ -0,0 +1,45 @@ +/** + * This rule detects any pickup events to allow us to + * properly record and/or fix pistols' prices. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class DualiesCostRule extends GameRules; + +function bool OverridePickupQuery +( + Pawn other, + Pickup item, + out byte allowPickup +) +{ + local KFWeaponPickup weaponPickup; + local FixDualiesCost dualiesCostFix; + weaponPickup = KFWeaponPickup(item); + dualiesCostFix = FixDualiesCost(class'FixDualiesCost'.static.GetInstance()); + if (weaponPickup != none && dualiesCostFix != none) + { + dualiesCostFix.ApplyPendingValues(); + dualiesCostFix.StoreSinglePistolValues(); + dualiesCostFix.SetNextSellValue(weaponPickup.sellValue); + } + return super.OverridePickupQuery(other, item, allowPickup); +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/FixDualiesCost/FixDualiesCost.uc b/sources/FixDualiesCost/FixDualiesCost.uc new file mode 100644 index 0000000..5422ac0 --- /dev/null +++ b/sources/FixDualiesCost/FixDualiesCost.uc @@ -0,0 +1,455 @@ +/** + * This feature fixes several issues related to the selling price of both + * single and dual pistols, all originating from the existence of dual weapons. + * Most notable issue is the ability to "print" money by buying and + * selling pistols in a certain way. + * + * It fixes all of the issues by manually setting pistols' + * 'SellValue' variables to proper values. + * Fix only works with vanilla pistols, as it's unpredictable what + * custom ones can do and they can handle these issues on their own + * in a better way. + * Copyright 2020 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixDualiesCost extends Feature + config(AcediaFixes); + +/** + * Issues with pistols' cost may look varied and surface in + * a plethora of ways, but all of them originate from the two main errors + * in vanilla's code: + * 1. If you have a pistol in your inventory at the time when you + * buy/pickup another one - the sell value of resulting dualies is + * incorrectly set to the sell value of the second pistol; + * 2. When player has dual pistols and drops one on the floor, - + * the sell value for the one left with the player isn't set. + * All weapons in Killing Floor get sell value assigned to them + * (appropriately, in a 'SellValue' variable). This is to ensure that the sell + * price is set the moment players buys the gun. Otherwise, due to ridiculous + * perked discounts, you'd be able to buy a pistol at 30% price + * as sharpshooter, but sell at 75% of a price as any other perk, + * resulting in 45% of pure profit. + * Unfortunately, that's exactly what happens when 'SellValue' isn't set + * (left as it's default value of '-1'): sell value of such weapons is + * determined only at the moment of sale and depends on the perk of the seller, + * allowing for possible exploits. + * + * These issues are fixed by directly assigning + * proper values to 'SellValue'. To do that we need to detect when player + * buys/sells/drops/picks up weapons, which we accomplish by catching + * 'CheckReplacement' event for weapon instances. This approach has two issues. + * One is that, if vanilla's code sets an incorrect sell value, - + * it's doing it after weapon is spawned and, therefore, + * after 'CheckReplacement' call, so we have, instead, to remember to do + * it later, as early as possible + * (either the next tick or before another operation with weapons). + * Another issue is that when you have a pistol and pick up a pistol of + * the same type, - at the moment dualies instance is spawned, + * the original pistol in player's inventory is gone and we can't use + * it's sell value to calculate new value of dual pistols. + * This problem is solved by separately recording the value for every + * single pistol every tick. + * However, if pistol pickups are placed close enough together on the map, + * player can start touching them (which triggers a pickup) at the same time, + * picking them both in a single tick. This leaves us no room to record + * the value of a single pistol players picks up first. + * To get it we use game rules to catch 'OverridePickupQuery' event that's + * called before the first one gets destroyed, + * but after it's sell value was already set. + * Last issue is that when player picks up a second pistol - we don't know + * it's sell value and, therefore, can't calculate value of dual pistols. + * This is resolved by recording that value directly from a pickup, + * in abovementioned function 'OverridePickupQuery'. + * NOTE: 9mm is an exception due to the fact that you always have at least + * one and the last one can't be sold. We'll deal with it by setting + * the following rule: sell value of the un-droppable pistol is always 0 + * and the value of a pair of 9mms is the value of the single droppable pistol. + */ + +// Some issues involve possible decrease in pistols' price and +// don't lead to exploit, but are still bugs and require fixing. +// If you have a Deagle in your inventory and then get another one +// (by either buying or picking it off the ground) - the price of resulting +// dual pistols will be set to the price of the last deagle, +// like the first one wasn't worth anything at all. +// In particular this means that (prices are off-perk for more clarity): +// 1. If you buy dual deagles (-1000 do$h) and then sell them at 75% of +// the cost (+750 do$h), you lose 250 do$h; +// 2. If you first buy a deagle (-500 do$h), then buy +// the second one (-500 do$h) and then sell them, you'll only get +// 75% of the cost of 1 deagle (+375 do$h), now losing 625 do$h; +// 3. So if you already have bought a deagle (-500 do$h), +// you can get a more expensive weapon by doing a stupid thing +// and first selling your Deagle (+375 do$h), +// then buying dual deagles (-1000 do$h). +// If you sell them after that, you'll gain 75% of the cost of +// dual deagles (+750 do$h), leaving you with losing only 375 do$h. +// Of course, situations described above are only relevant if you're planning +// to sell your weapons at some point and most people won't even notice it. +// But such an oversight still shouldn't exist in a game and we fix it by +// setting sell value of dualies as a sum of values of each pistol. +// Yet, fixing this issue leads to players having more expensive +// (while fairly priced) weapons than on vanilla, technically making +// the game easier. And some people might object to having that in +// a whitelisted bug-fixing feature. +// These people are, without a question, complete degenerates. +// But making mods for only non-mentally challenged isn't inclusive. +// So we add this option. +// Set it to 'false' if you only want to fix ammo printing +// and leave the rest of the bullshit as-is. +var private config const bool allowSellValueIncrease; + +// Describe all the possible pairs of dual pistols in a vanilla game. +struct DualiesPair +{ + var class single; + var class dual; +}; +var private const array dualiesClasses; + +// Describe sell values that need to be applied at earliest later point. +struct WeaponValuePair +{ + var KFWeapon weapon; + var float value; +}; +var private const array pendingValues; + +// Describe sell values of all currently existing single pistols. +struct WeaponDataRecord +{ + var KFWeapon reference; + var class class; + var float value; + // The whole point of this structure is to remember value of a weapon + // after it's destroyed. Since 'reference' will become 'none' by then, + // we will use the 'owner' reference to identify the weapon. + var Pawn owner; +}; +var private const array storedValues; + +// Sell value of the last seen pickup in 'OverridePickupQuery' +var private int nextSellValue; + +protected function OnEnabled() +{ + local KFWeapon nextWeapon; + // Find all frags, that spawned when this fix wasn't running. + foreach level.DynamicActors(class'KFMod.KFWeapon', nextWeapon) + { + RegisterSinglePistol(nextWeapon, false); + } + level.game.AddGameModifier(Spawn(class'DualiesCostRule')); +} + +protected function OnDisabled() +{ + local GameRules rulesIter; + local DualiesCostRule ruleToDestroy; + // Check first rule + if (level.game.gameRulesModifiers == none) return; + + ruleToDestroy = DualiesCostRule(level.game.gameRulesModifiers); + if (ruleToDestroy != none) + { + level.game.gameRulesModifiers = ruleToDestroy.nextGameRules; + ruleToDestroy.Destroy(); + return; + } + // Check rest of the rules + rulesIter = level.game.gameRulesModifiers; + while (rulesIter != none) + { + ruleToDestroy = DualiesCostRule(rulesIter.nextGameRules); + if (ruleToDestroy != none) + { + rulesIter.nextGameRules = ruleToDestroy.nextGameRules; + ruleToDestroy.Destroy(); + } + rulesIter = rulesIter.nextGameRules; + } +} + +public final function SetNextSellValue(int newValue) +{ + nextSellValue = newValue; +} + +// Finds a weapon of a given class in given 'Pawn' 's inventory. +// Returns 'none' if weapon isn't there. +private final function KFWeapon GetWeaponOfClass +( + Pawn playerPawn, + class weaponClass +) +{ + local Inventory invIter; + if (playerPawn == none) return none; + + invIter = playerPawn.inventory; + while (invIter != none) + { + if (invIter.class == weaponClass) + { + return KFWeapon(invIter); + } + invIter = invIter.inventory; + } + return none; +} + +// Gets weapon index in our record of dual pistol classes. +// Second variable determines whether we're searching for single +// or dual variant: +// ~ 'true' - searching for single +// ~ 'false' - for dual +// Returns '-1' if weapon isn't found +// (dual MK23 won't be found as a single weapon). +private final function int GetIndexAs(KFWeapon weapon, bool asSingle) +{ + local int i; + if (weapon == none) return -1; + + for (i = 0; i < dualiesClasses.length; i += 1) + { + if (asSingle && dualiesClasses[i].single == weapon.class) + { + return i; + } + if (!asSingle && dualiesClasses[i].dual == weapon.class) + { + return i; + } + } + return -1; +} + +// Calculates full cost of a weapon with a discount, +// dependent on it's instigator's perk. +private final function float GetFullCost(KFWeapon weapon) +{ + local float cost; + local class pickupClass; + local KFPlayerReplicationInfo instigatorRI; + if (weapon == none) return 0.0; + pickupClass = class(weapon.default.pickupClass); + if (pickupClass == none) return 0.0; + + cost = pickupClass.default.cost; + if (weapon.instigator != none) + { + instigatorRI = + KFPlayerReplicationInfo(weapon.instigator.playerReplicationInfo); + } + if (instigatorRI != none && instigatorRI.clientVeteranSkill != none) + { + cost *= instigatorRI.clientVeteranSkill.static + .GetCostScaling(instigatorRI, pickupClass); + } + return cost; +} + +// If passed weapon is a pistol - we start tracking it's value; +// Otherwise - do nothing. +public final function RegisterSinglePistol +( + KFWeapon singlePistol, + bool justSpawned +) +{ + local WeaponDataRecord newRecord; + if (singlePistol == none) return; + if (GetIndexAs(singlePistol, true) < 0) return; + + newRecord.reference = singlePistol; + newRecord.class = singlePistol.class; + newRecord.owner = singlePistol.instigator; + if (justSpawned) + { + newRecord.value = nextSellValue; + } + else + { + newRecord.value = singlePistol.sellValue; + } + storedValues[storedValues.length] = newRecord; +} + +// Fixes sell value after player throws one pistol out of a pair. +public final function FixCostAfterThrow(KFWeapon singlePistol) +{ + local int index; + local KFWeapon dualPistols; + if (singlePistol == none) return; + index = GetIndexAs(singlePistol, true); + if (index < 0) return; + dualPistols = GetWeaponOfClass( singlePistol.instigator, + dualiesClasses[index].dual); + if (dualPistols == none) return; + + // Sell value recorded into 'dualPistols' will end up as a value of + // a dropped pickup. + // Sell value of 'singlePistol' will be the value for the pistol, + // left in player's hands. + if (dualPistols.class == class'KFMod.Single') + { + // 9mm is an exception. + // Remaining weapon costs nothing. + singlePistol.sellValue = 0; + // We don't change the sell value of the dropped weapon, + // as it's default behavior to transfer full value of a pair to it. + return; + } + // For other pistols - divide the value. + singlePistol.sellValue = dualPistols.sellValue / 2; + dualPistols.sellValue = singlePistol.sellValue; +} + +// Fixes sell value after buying a pair of dual pistols, +// if player already had a single version. +public final function FixCostAfterBuying(KFWeapon dualPistols) +{ + local int index; + local KFWeapon singlePistol; + local WeaponValuePair newPendingValue; + if (dualPistols == none) return; + index = GetIndexAs(dualPistols, false); + if (index < 0) return; + singlePistol = GetWeaponOfClass(dualPistols.instigator, + dualiesClasses[index].single); + if (singlePistol == none) return; + + // 'singlePistol' will get destroyed, so it's sell value is irrelevant. + // 'dualPistols' will be the new pair of pistols, but it's value will + // get overwritten by vanilla's code after this function. + // So we must add it to pending values to be changed later. + newPendingValue.weapon = dualPistols; + if (dualPistols.class == class'KFMod.Dualies') + { + // 9mm is an exception. + // The value of pair of 9mms is the price of additional pistol, + // that defined as a price of a pair in game. + newPendingValue.value = GetFullCost(dualPistols) * 0.75; + } + else + { + // Otherwise price of a pair is the price of two pistols: + // 'singlePistol.sellValue' - the one we had + // '(FullCost / 2) * 0.75' - and the one we bought + newPendingValue.value = singlePistol.sellValue + + (GetFullCost(dualPistols) / 2) * 0.75; + } + pendingValues[pendingValues.length] = newPendingValue; +} + +// Fixes sell value after player picks up a single pistol, +// while already having one of the same time in his inventory. +public final function FixCostAfterPickUp(KFWeapon dualPistols) +{ + local int i; + local int index; + local KFWeapon singlePistol; + local WeaponValuePair newPendingValue; + if (dualPistols == none) return; + // In both cases of: + // 1. buying dualies, without having a single pistol of + // corresponding type; + // 2. picking up a second pistol, while having another one; + // by the time of 'CheckReplacement' (and, therefore, this function) + // is called, there's no longer any single pistol in player's inventory + // (in first case it never was there, in second - it got destroyed). + // To distinguish between those possibilities we can check the owner of + // the spawned weapon, since it's only set to instigator at the time of + // 'CheckReplacement' when player picks up a weapon. + // So we require that owner exists. + if (dualPistols.owner == none) return; + index = GetIndexAs(dualPistols, false); + if (index < 0) return; + singlePistol = GetWeaponOfClass(dualPistols.instigator, + dualiesClasses[index].single); + if (singlePistol != none) return; + + if (nextSellValue == -1) + { + nextSellValue = GetFullCost(dualPistols) * 0.75; + } + for (i = 0; i < storedValues.length; i += 1) + { + if (storedValues[i].reference != none) continue; + if (storedValues[i].class != dualiesClasses[index].single) continue; + if (storedValues[i].owner != dualPistols.instigator) continue; + newPendingValue.weapon = dualPistols; + newPendingValue.value = storedValues[i].value + nextSellValue; + pendingValues[pendingValues.length] = newPendingValue; + break; + } +} + +public final function ApplyPendingValues() +{ + local int i; + for (i = 0; i < pendingValues.length; i += 1) + { + if (pendingValues[i].weapon == none) continue; + // Our fixes can only increase the correct ('!= -1') + // sell value of weapons, so if we only need to change sell value + // if we're allowed to increase it or it's incorrect. + if (allowSellValueIncrease || pendingValues[i].weapon.sellValue == -1) + { + pendingValues[i].weapon.sellValue = pendingValues[i].value; + } + } + pendingValues.length = 0; +} + +public final function StoreSinglePistolValues() +{ + local int i; + i = 0; + while (i < storedValues.length) + { + if (storedValues[i].reference == none) + { + storedValues.Remove(i, 1); + continue; + } + storedValues[i].owner = storedValues[i].reference.instigator; + storedValues[i].value = storedValues[i].reference.sellValue; + i += 1; + } +} + +event Tick(float delta) +{ + ApplyPendingValues(); + StoreSinglePistolValues(); +} + +defaultproperties +{ + allowSellValueIncrease = true + // Inner variables + dualiesClasses(0)=(single=class'KFMod.Single',dual=class'KFMod.Dualies') + dualiesClasses(1)=(single=class'KFMod.Magnum44Pistol',dual=class'KFMod.Dual44Magnum') + dualiesClasses(2)=(single=class'KFMod.MK23Pistol',dual=class'KFMod.DualMK23Pistol') + dualiesClasses(3)=(single=class'KFMod.Deagle',dual=class'KFMod.DualDeagle') + dualiesClasses(4)=(single=class'KFMod.GoldenDeagle',dual=class'KFMod.GoldenDualDeagle') + dualiesClasses(5)=(single=class'KFMod.FlareRevolver',dual=class'KFMod.DualFlareRevolver') + // Listeners + requiredListeners(0) = class'MutatorListener_FixDualiesCost' +} \ No newline at end of file diff --git a/sources/FixDualiesCost/MutatorListener_FixDualiesCost.uc b/sources/FixDualiesCost/MutatorListener_FixDualiesCost.uc new file mode 100644 index 0000000..0f209ae --- /dev/null +++ b/sources/FixDualiesCost/MutatorListener_FixDualiesCost.uc @@ -0,0 +1,43 @@ +/** + * Overloaded mutator events listener to catch when pistol-type weapons + * (single or dual) are spawned and to correct their price. + * Copyright 2020 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class MutatorListener_FixDualiesCost extends MutatorListenerBase + abstract; + +static function bool CheckReplacement(Actor other, out byte isSuperRelevant) +{ + local KFWeapon weapon; + local FixDualiesCost dualiesCostFix; + weapon = KFWeapon(other); + if (weapon == none) return true; + dualiesCostFix = FixDualiesCost(class'FixDualiesCost'.static.GetInstance()); + if (dualiesCostFix == none) return true; + + dualiesCostFix.RegisterSinglePistol(weapon, true); + dualiesCostFix.FixCostAfterThrow(weapon); + dualiesCostFix.FixCostAfterBuying(weapon); + dualiesCostFix.FixCostAfterPickUp(weapon); + return true; +} + +defaultproperties +{ + relatedEvents = class'MutatorEvents' +} \ No newline at end of file diff --git a/sources/FixFFHack/FFHackRule.uc b/sources/FixFFHack/FFHackRule.uc new file mode 100644 index 0000000..efe0556 --- /dev/null +++ b/sources/FixFFHack/FFHackRule.uc @@ -0,0 +1,74 @@ +/** + * This rule detects suspicious attempts to deal damage and + * applies friendly fire scaling according to 'FixFFHack's rules. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FFHackRule extends GameRules; + +function int NetDamage +( + int originalDamage, + int damage, + Pawn injured, + Pawn instigator, + Vector hitLocation, + out Vector momentum, + class damageType +) +{ + local KFGameType gameType; + local FixFFHack ffHackFix; + gameType = KFGameType(level.game); + // Something is very wrong and we can just bail on this damage + if (damageType == none || gameType == none) return 0; + + // We only check when suspicious instigators that aren't a world + if (!damageType.default.bCausedByWorld && IsSuspicious(instigator)) + { + ffHackFix = FixFFHack(class'FixFFHack'.static.GetInstance()); + if (ffHackFix != none && ffHackFix.ShouldScaleDamage(damageType)) + { + // Remove pushback to avoid environmental kills + momentum = Vect(0.0, 0.0, 0.0); + damage *= gameType.friendlyFireScale; + } + } + return super.NetDamage( originalDamage, damage, injured, instigator, + hitLocation, momentum, damageType); +} + +private function bool IsSuspicious(Pawn instigator) +{ + // Instigator vanished + if (instigator == none) return true; + + // Instigator already became spectator + if (KFPawn(instigator) != none) + { + if (instigator.playerReplicationInfo != none) + { + return instigator.playerReplicationInfo.bOnlySpectator; + } + return true; // Replication info is gone => suspicious + } + return false; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/FixFFHack/FixFFHack.uc b/sources/FixFFHack/FixFFHack.uc new file mode 100644 index 0000000..831da8d --- /dev/null +++ b/sources/FixFFHack/FixFFHack.uc @@ -0,0 +1,153 @@ +/** + * This feature fixes a bug that can allow players to bypass server's + * friendly fire limitations and teamkill. + * Usual fixes apply friendly fire scale to suspicious damage themselves, which + * also disables some of the environmental damage. + * In order to avoid that, this fix allows server owner to define precisely + * to what damage types to apply the friendly fire scaling. + * It should be all damage types related to projectiles. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixFFHack extends Feature + config(AcediaFixes); + +/** + * It's possible to bypass friendly fire damage scaling and always deal + * full damage to other players, if one were to either leave the server or + * spectate right after shooting a projectile. We use game rules to catch + * such occurrences and apply friendly fire scaling to weapons, + * specified by server admins. + * To specify required subset of weapons, one must first + * chose a general rule (scale by default / don't scale by default) and then, + * optionally, add exceptions to it. + * Choosing 'scaleByDefault == true' as a general rule will make this fix + * behave in the similar way to 'KFExplosiveFix' by mutant and will disable + * some environmental sources of damage on some maps. One can then add relevant + * damage classes as exceptions to fix that downside, but making an extensive + * list of such sources might prove problematic. + * On the other hand, setting 'scaleByDefault == false' will allow to get + * rid of team-killing exploits by simply adding damage types of all + * projectile weapons, used on a server. This fix comes with such filled-in + * list of all vanilla projectile classes. + */ + +// Defines a general rule for choosing whether or not to apply +// friendly fire scaling. +// This can be overwritten by exceptions ('alwaysScale' or 'neverScale'). +// Enabling scaling by default without any exceptions in 'neverScale' will +// make this fix behave almost identically to Mutant's 'Explosives Fix Mutator'. +var private config const bool scaleByDefault; +// Damage types, for which we should always reapply friendly fire scaling. +var private config const array< class > alwaysScale; +// Damage types, for which we should never reapply friendly fire scaling. +var private config const array< class > neverScale; + +protected function OnEnabled() +{ + level.game.AddGameModifier(Spawn(class'FFHackRule')); +} + +protected function OnDisabled() +{ + local GameRules rulesIter; + local FFHackRule ruleToDestroy; + // Check first rule + if (level.game.gameRulesModifiers == none) return; + + ruleToDestroy = FFHackRule(level.game.gameRulesModifiers); + if (ruleToDestroy != none) + { + level.game.gameRulesModifiers = ruleToDestroy.nextGameRules; + ruleToDestroy.Destroy(); + return; + } + // Check rest of the rules + rulesIter = level.game.gameRulesModifiers; + while (rulesIter != none) + { + ruleToDestroy = FFHackRule(rulesIter.nextGameRules); + if (ruleToDestroy != none) + { + rulesIter.nextGameRules = ruleToDestroy.nextGameRules; + ruleToDestroy.Destroy(); + } + rulesIter = rulesIter.nextGameRules; + } +} + +// Checks general rule and exception list +public final function bool ShouldScaleDamage(class damageType) +{ + local int i; + local array< class > exceptions; + if (damageType == none) return false; + + if (scaleByDefault) + exceptions = neverScale; + else + exceptions = alwaysScale; + for (i = 0; i < exceptions.length; i += 1) + { + if (exceptions[i] == damageType) + { + return (!scaleByDefault); + } + } + return scaleByDefault; +} + +defaultproperties +{ + scaleByDefault = false + // Vanilla damage types for projectiles + alwaysScale(0) = class'KFMod.DamTypeCrossbuzzsawHeadShot' + alwaysScale(1) = class'KFMod.DamTypeCrossbuzzsaw' + alwaysScale(2) = class'KFMod.DamTypeFrag' + alwaysScale(3) = class'KFMod.DamTypePipeBomb' + alwaysScale(4) = class'KFMod.DamTypeM203Grenade' + alwaysScale(5) = class'KFMod.DamTypeM79Grenade' + alwaysScale(6) = class'KFMod.DamTypeM79GrenadeImpact' + alwaysScale(7) = class'KFMod.DamTypeM32Grenade' + alwaysScale(8) = class'KFMod.DamTypeLAW' + alwaysScale(9) = class'KFMod.DamTypeLawRocketImpact' + alwaysScale(10) = class'KFMod.DamTypeFlameNade' + alwaysScale(11) = class'KFMod.DamTypeFlareRevolver' + alwaysScale(12) = class'KFMod.DamTypeFlareProjectileImpact' + alwaysScale(13) = class'KFMod.DamTypeBurned' + alwaysScale(14) = class'KFMod.DamTypeTrenchgun' + alwaysScale(15) = class'KFMod.DamTypeHuskGun' + alwaysScale(16) = class'KFMod.DamTypeCrossbow' + alwaysScale(17) = class'KFMod.DamTypeCrossbowHeadShot' + alwaysScale(18) = class'KFMod.DamTypeM99SniperRifle' + alwaysScale(19) = class'KFMod.DamTypeM99HeadShot' + alwaysScale(20) = class'KFMod.DamTypeShotgun' + alwaysScale(21) = class'KFMod.DamTypeNailGun' + alwaysScale(22) = class'KFMod.DamTypeDBShotgun' + alwaysScale(23) = class'KFMod.DamTypeKSGShotgun' + alwaysScale(24) = class'KFMod.DamTypeBenelli' + alwaysScale(25) = class'KFMod.DamTypeSPGrenade' + alwaysScale(26) = class'KFMod.DamTypeSPGrenadeImpact' + alwaysScale(27) = class'KFMod.DamTypeSeekerSixRocket' + alwaysScale(28) = class'KFMod.DamTypeSeekerRocketImpact' + alwaysScale(29) = class'KFMod.DamTypeSealSquealExplosion' + alwaysScale(30) = class'KFMod.DamTypeRocketImpact' + alwaysScale(31) = class'KFMod.DamTypeBlowerThrower' + alwaysScale(32) = class'KFMod.DamTypeSPShotgun' + alwaysScale(33) = class'KFMod.DamTypeZEDGun' + alwaysScale(34) = class'KFMod.DamTypeZEDGunMKII' +} \ No newline at end of file diff --git a/sources/FixInfiniteNades/FixInfiniteNades.uc b/sources/FixInfiniteNades/FixInfiniteNades.uc new file mode 100644 index 0000000..5dec069 --- /dev/null +++ b/sources/FixInfiniteNades/FixInfiniteNades.uc @@ -0,0 +1,234 @@ + /** + * This feature fixes a vulnerability in a code of 'Frag' that can allow + * player to throw grenades even when he no longer has any. + * There's also no cooldowns on the throw, which can lead to a server crash. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixInfiniteNades extends Feature + config(AcediaFixes); + +/** + * It is possible to call 'ServerThrow' function from client, + * forcing it to get executed on a server. This function consumes the grenade + * ammo and spawns a nade, but it doesn't check if player had any grenade ammo + * in the first place, allowing you him to throw however many grenades + * he wants. Moreover, unlike a regular throwing method, calling this function + * allows to spawn many grenades without any delay, + * which can lead to a server crash. + * + * This fix tracks every instance of 'Frag' weapon that's responsible for + * throwing grenades and records how much ammo they have have. + * This is necessary, because whatever means we use, when we get a say in + * preventing grenade from spawning the ammo was already reduced. + * This means that we can't distinguished between a player abusing a bug by + * throwing grenade when he doesn't have necessary ammo and player throwing + * his last nade, as in both cases current ammo visible to us will be 0. + * Then, before every nade throw, it checks if player has enough ammo and + * blocks grenade from spawning if he doesn't. + * We change a 'FireModeClass[0]' from 'FragFire' to 'FixedFragFire' and + * only call 'super.DoFireEffect()' if we decide spawning grenade + * should be allowed. The side effect is a change in server's 'FireModeClass'. + */ + +// Setting this flag to 'true' will allow to throw grenades by calling +// 'ServerThrow' directly, as long as player has necessary ammo. +// This can allow some players to throw grenades much quicker than intended, +// therefore it's suggested to keep this flag set to 'false'. +var private config const bool ignoreTossFlags; + +// Records how much ammo given frag grenade ('Frag') has. +struct FragAmmoRecord +{ + var public Frag fragReference; + var public int amount; +}; +var private array ammoRecords; + +protected function OnEnabled() +{ + local Frag nextFrag; + // Find all frags, that spawned when this fix wasn't running. + foreach level.DynamicActors(class'KFMod.Frag', nextFrag) + { + RegisterFrag(nextFrag); + } + RecreateFrags(); +} + +protected function OnDisabled() +{ + RecreateFrags(); + ammoRecords.length = 0; +} + +// Returns index of the connection corresponding to the given controller. +// Returns '-1' if no connection correspond to the given controller. +// Returns '-1' if given controller is equal to 'none'. +private final function int GetAmmoIndex(Frag fragToCheck) +{ + local int i; + if (fragToCheck == none) return -1; + + for (i = 0; i < ammoRecords.length; i += 1) + { + if (ammoRecords[i].fragReference == fragToCheck) + { + return i; + } + } + return -1; +} + +// Recreates all the 'Frag' actors, to change their fire mode mid-game. +private final function RecreateFrags() +{ + local int i; + local float maxAmmo, currentAmmo; + local Frag newFrag; + local Pawn fragOwner; + local array oldRecords; + oldRecords = ammoRecords; + for (i = 0; i < oldRecords.length; i += 1) + { + // Check if we even need to recreate that instance of 'Frag' + if (oldRecords[i].fragReference == none) continue; + fragOwner = oldRecords[i].fragReference.instigator; + if (fragOwner == none) continue; + // Recreate + oldRecords[i].fragReference.Destroy(); + fragOwner.CreateInventory("KFMod.Frag"); + newFrag = GetPawnFrag(fragOwner); + // Restore ammo amount + if (newFrag != none) + { + newFrag.GetAmmoCount(maxAmmo, currentAmmo); + newFrag.AddAmmo(oldRecords[i].amount - Int(currentAmmo), 0); + } + } +} + +// Utility function to help find a 'Frag' instance in a given pawn's inventory. +static private final function Frag GetPawnFrag(Pawn pawnWithFrag) +{ + local Frag foundFrag; + local Inventory invIter; + if (pawnWithFrag == none) return none; + invIter = pawnWithFrag.inventory; + while (invIter != none) + { + foundFrag = Frag(invIter); + if (foundFrag != none) + { + return foundFrag; + } + invIter = invIter.inventory; + } + return none; +} + +// Utility function for extracting current ammo amount from a frag class. +private final function int GetFragAmmo(Frag fragReference) +{ + local float maxAmmo; + local float currentAmmo; + if (fragReference == none) return 0; + + fragReference.GetAmmoCount(maxAmmo, currentAmmo); + return Int(currentAmmo); +} + +// Attempts to add new 'Frag' instance to our records. +public final function RegisterFrag(Frag newFrag) +{ + local int index; + local FragAmmoRecord newRecord; + index = GetAmmoIndex(newFrag); + if (index >= 0) return; + + newRecord.fragReference = newFrag; + newRecord.amount = GetFragAmmo(newFrag); + ammoRecords[ammoRecords.length] = newRecord; +} + +// This function tells our fix that there was a nade throw and we should +// reduce current 'Frag' ammo in our records. +// Returns 'true' if we had ammo for that, and 'false' if we didn't. +public final function bool RegisterNadeThrow(Frag relevantFrag) +{ + if (CanThrowGrenade(relevantFrag)) + { + ReduceGrenades(relevantFrag); + return true; + } + return false; +} + +// Can we throw grenade according to our rules? +// A throw can be prevented if: +// - we think that player doesn't have necessary ammo; +// - Player isn't currently 'tossing' a nade, +// meaning it was a direct call of 'ServerThrow'. +private final function bool CanThrowGrenade(Frag fragToCheck) +{ + local int index; + // Nothing to check + if (fragToCheck == none) return false; + // No ammo + index = GetAmmoIndex(fragToCheck); + if (index < 0) return false; + if (ammoRecords[index].amount <= 0) return false; + // Not tossing + if (ignoreTossFlags) return true; + if (!fragToCheck.bTossActive || fragToCheck.bTossSpawned) return false; + return true; +} + +// Reduces recorded amount of ammo in our records for the given nade. +private final function ReduceGrenades(Frag relevantFrag) +{ + local int index; + index = GetAmmoIndex(relevantFrag); + if (index < 0) return; + ammoRecords[index].amount -= 1; +} + +event Tick(float delta) +{ + local int i; + // Update our ammo records with current, correct data. + i = 0; + while (i < ammoRecords.length) + { + if (ammoRecords[i].fragReference != none) + { + ammoRecords[i].amount = GetFragAmmo(ammoRecords[i].fragReference); + i += 1; + } + else + { + ammoRecords.Remove(i, 1); + } + } +} + +defaultproperties +{ + ignoreTossFlags = false + // Listeners + requiredListeners(0) = class'MutatorListener_FixInfiniteNades' +} \ No newline at end of file diff --git a/sources/FixInfiniteNades/FixedFragFire.uc b/sources/FixInfiniteNades/FixedFragFire.uc new file mode 100644 index 0000000..e0e356c --- /dev/null +++ b/sources/FixInfiniteNades/FixedFragFire.uc @@ -0,0 +1,36 @@ + /** + * A replacement for vanilla 'FragFire' fire class for 'Frag' weapon that + * adds additional ammo check in accordance to ammo records + * of 'FixInfiniteNades'. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixedFragFire extends KFMod.FragFire; + +function DoFireEffect() +{ + local FixInfiniteNades nadeFix; + nadeFix = FixInfiniteNades(class'FixInfiniteNades'.static.GetInstance()); + if (nadeFix == none || nadeFix.RegisterNadeThrow(Frag(weapon))) + { + super.DoFireEffect(); + } +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/FixInfiniteNades/MutatorListener_FixInfiniteNades.uc b/sources/FixInfiniteNades/MutatorListener_FixInfiniteNades.uc new file mode 100644 index 0000000..29e6e3e --- /dev/null +++ b/sources/FixInfiniteNades/MutatorListener_FixInfiniteNades.uc @@ -0,0 +1,44 @@ +/** + * Overloaded mutator events listener to catch + * new 'Frag' weapons and 'Nade' projectiles. + * Copyright 2019 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class MutatorListener_FixInfiniteNades extends MutatorListenerBase + abstract; + +static function bool CheckReplacement(Actor other, out byte isSuperRelevant) +{ + local Frag relevantFrag; + local FixInfiniteNades nadeFix; + nadeFix = FixInfiniteNades(class'FixInfiniteNades'.static.GetInstance()); + if (nadeFix == none) return true; + + // Handle detecting new frag (weapons that allows to throw nades) + relevantFrag = Frag(other); + if (relevantFrag != none) + { + nadeFix.RegisterFrag(relevantFrag); + relevantFrag.FireModeClass[0] = class'FixedFragFire'; + return true; + } + return true; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/FixInventoryAbuse/FixInventoryAbuse.uc b/sources/FixInventoryAbuse/FixInventoryAbuse.uc new file mode 100644 index 0000000..6c4f380 --- /dev/null +++ b/sources/FixInventoryAbuse/FixInventoryAbuse.uc @@ -0,0 +1,226 @@ +/** + * This feature addressed two inventory issues: + * 1. Players carrying amount of weapons that shouldn't be allowed by the + * weight limit. + * 2. Players carrying two variants of the same gun. + * For example carrying both M32 and camo M32. + * Single and dual version of the same weapon are also considered + * the same gun, so you can't carry both MK23 and dual MK23 or + * dual handcannons and golden handcannon. + * + * It fixes them by doing repeated checks to find violations of those rules + * and destroys all droppable weapons of people that use this exploit. + * Copyright 2020 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixInventoryAbuse extends Feature + config(AcediaFixes); + +// How often (in seconds) should we do our inventory validations? +// We shouldn't really worry about performance, but there's also no need to +// do this check too often. +var private config const float checkInterval; + +struct DualiesPair +{ + var class single; + var class dual; +}; +// For this fix to properly work, this array must contain an entry for +// every dual weapon in the game (like pistols, with single and dual versions). +// It's made configurable in case of custom dual weapons. +var private config const array dualiesClasses; + +protected function OnEnabled() +{ + local float actualInterval; + actualInterval = checkInterval; + if (actualInterval <= 0) + { + actualInterval = 0.25; + } + SetTimer(actualInterval, true); +} + +protected function OnDisabled() +{ + SetTimer(0.0f, false); +} + +// Did player with this controller contribute to the latest dosh generation? +private final function bool IsWeightLimitViolated(KFHumanPawn playerPawn) +{ + if (playerPawn == none) return false; + return (playerPawn.currentWeight > playerPawn.maxCarryWeight); +} + +// Returns a root pickup class. +// For non-dual weapons, root class is defined as either: +// 1. the first variant (reskin), if there are variants for that weapon; +// 2. and as the class itself, if there are no variants. +// For dual weapons (all dual pistols) root class is defined as +// a root of their single version. +// This definition is useful because: +// ~ Vanilla game rules are such that player can only have two weapons +// in the inventory if they have different roots; +// ~ Root is easy to find. +private final function class GetRootPickupClass(KFWeapon weapon) +{ + local int i; + local class root; + if (weapon == none) return none; + // Start with a pickup of the given weapons + root = class(weapon.default.pickupClass); + if (root == none) return none; + + // In case it's a dual version - find corresponding single pickup class + // (it's root would be the same). + for (i = 0; i < dualiesClasses.length; i += 1) + { + if (dualiesClasses[i].dual == root) + { + root = dualiesClasses[i].single; + break; + } + } + // Take either first variant class or the class itself, - + // it's going to be root by definition. + if (root.default.variantClasses.length > 0) + { + root = class(root.default.variantClasses[0]); + } + return root; +} + +// Returns 'true' if passed pawn has two weapons that are just variants of +// each other (they have the same root, see 'GetRootPickupClass'). +private final function bool HasDuplicateGuns(KFHumanPawn playerPawn) +{ + local int i, j; + local Inventory inv; + local KFWeapon nextWeapon; + local class rootClass; + local array< class > rootList; + if (playerPawn == none) return false; + + // First find a root for every weapon in the pawn's inventory. + for (inv = playerPawn.inventory; inv != none; inv = inv.inventory) + { + nextWeapon = KFWeapon(inv); + if (nextWeapon == none) continue; + if (nextWeapon.bKFNeverThrow) continue; + rootClass = GetRootPickupClass(nextWeapon); + if (rootClass != none) + { + rootList[rootList.length] = rootClass; + } + } + // Then just check obtained roots for duplicates. + for (i = 0; i < rootList.length; i += 1) + { + for (j = i + 1; j < rootList.length; j += 1) + { + if (rootList[i] == rootList[j]) + { + return true; + } + } + } + return false; +} + +private final function Vector DropWeapon(KFWeapon weaponToDrop) +{ + local Vector x, y, z; + local Vector weaponVelocity; + local Vector dropLocation; + local KFHumanPawn playerPawn; + if (weaponToDrop == none) return Vect(0, 0, 0); + playerPawn = KFHumanPawn(weaponToDrop.instigator); + if (playerPawn == none) return Vect(0, 0, 0); + + // Calculations from 'PlayerController.ServerThrowWeapon' + weaponVelocity = Vector(playerPawn.GetViewRotation()); + weaponVelocity *= (playerPawn.velocity dot weaponVelocity) + 150; + weaponVelocity += Vect(0, 0, 100); + // Calculations from 'Pawn.TossWeapon' + GetAxes(playerPawn.rotation, x, y, z); + dropLocation = playerPawn.location + 0.8 * playerPawn.collisionRadius * x - + 0.5 * playerPawn.collisionRadius * y; + // Do the drop + weaponToDrop.velocity = weaponVelocity; + weaponToDrop.DropFrom(dropLocation); +} + +// Kill the gun devil! +private final function DropEverything(KFHumanPawn playerPawn) +{ + local int i; + local Inventory inv; + local KFWeapon nextWeapon; + local array weaponList; + if (playerPawn == none) return; + // Going through the linked list while removing items can be tricky, + // so just find all weapons first. + for (inv = playerPawn.inventory; inv != none; inv = inv.inventory) + { + nextWeapon = KFWeapon(inv); + if (nextWeapon == none) continue; + if (nextWeapon.bKFNeverThrow) continue; + weaponList[weaponList.length] = nextWeapon; + } + // And destroy them later. + for(i = 0; i < weaponList.length; i += 1) + { + DropWeapon(weaponList[i]); + } +} + +event Timer() +{ + local int i; + local KFHumanPawn nextPawn; + local ConnectionService service; + local array connections; + service = ConnectionService(class'ConnectionService'.static.GetInstance()); + if (service == none) return; + + connections = service.GetActiveConnections(); + for (i = 0; i < connections.length; i += 1) + { + nextPawn = none; + if (connections[i].controllerReference != none) + { + nextPawn = KFHumanPawn(connections[i].controllerReference.pawn); + } + if (IsWeightLimitViolated(nextPawn) || HasDuplicateGuns(nextPawn)) + { + DropEverything(nextPawn); + } + } +} + +defaultproperties +{ + checkInterval = 0.25 + dualiesClasses(0)=(single=class'KFMod.SinglePickup',dual=class'KFMod.DualiesPickup') + dualiesClasses(1)=(single=class'KFMod.Magnum44Pickup',dual=class'KFMod.Dual44MagnumPickup') + dualiesClasses(2)=(single=class'KFMod.MK23Pickup',dual=class'KFMod.DualMK23Pickup') + dualiesClasses(3)=(single=class'KFMod.DeaglePickup',dual=class'KFMod.DualDeaglePickup') + dualiesClasses(4)=(single=class'KFMod.GoldenDeaglePickup',dual=class'KFMod.GoldenDualDeaglePickup') + dualiesClasses(5)=(single=class'KFMod.FlareRevolverPickup',dual=class'KFMod.DualFlareRevolverPickup') +} \ No newline at end of file diff --git a/sources/FixSpectatorCrash/BroadcastListener_FixSpectatorCrash.uc b/sources/FixSpectatorCrash/BroadcastListener_FixSpectatorCrash.uc new file mode 100644 index 0000000..297dc5b --- /dev/null +++ b/sources/FixSpectatorCrash/BroadcastListener_FixSpectatorCrash.uc @@ -0,0 +1,51 @@ +/** + * Overloaded broadcast events listener to catch the moment + * someone becomes alive player / spectator. + * Copyright 2020 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class BroadcastListener_FixSpectatorCrash extends BroadcastListenerBase + abstract; + +var private const int becomeAlivePlayerID; +var private const int becomeSpectatorID; + +static function bool HandleLocalized +( + Actor sender, + BroadcastEvents.LocalizedMessage message +) +{ + local FixSpectatorCrash specFix; + local PlayerController senderController; + if (sender == none) return true; + if (sender.level == none || sender.level.game == none) return true; + if (message.class != sender.level.game.gameMessageClass) return true; + if ( message.id != default.becomeAlivePlayerID + && message.id != default.becomeSpectatorID) return true; + + specFix = FixSpectatorCrash(class'FixSpectatorCrash'.static.GetInstance()); + senderController = GetController(sender); + specFix.NotifyStatusChange(senderController); + return (!specFix.IsViolator(senderController)); +} + +defaultproperties +{ + becomeAlivePlayerID = 1 + becomeSpectatorID = 14 +} \ No newline at end of file diff --git a/sources/FixSpectatorCrash/FixSpectatorCrash.uc b/sources/FixSpectatorCrash/FixSpectatorCrash.uc new file mode 100644 index 0000000..3d63ebd --- /dev/null +++ b/sources/FixSpectatorCrash/FixSpectatorCrash.uc @@ -0,0 +1,292 @@ +/** + * This feature attempts to prevent server crashes caused by someone + * quickly switching between being spectator and an active player. + * + * We do so by disconnecting players who start switching way too fast + * (more than twice in a short amount of time) and temporarily faking a large + * amount of players on the server, to prevent such spam from affecting the server. + * Copyright 2020 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixSpectatorCrash extends Feature + dependson(ConnectionService) + config(AcediaFixes); + +/** + * We use broadcast events to track when someone is switching + * to active player or spectator and remember such people + * for a short time (cooldown), defined by ('spectatorChangeTimeout'). + * If one of the player we've remembered tries to switch again, + * before the defined cooldown ran out, - we kick him + * by destroying his controller. + * One possible problem arises from the fact that controllers aren't + * immediately destroyed and instead initiate player disconnection, - + * exploiter might have enough time to cause a lag or even crash the server. + * We address this issue by temporarily blocking anyone from + * becoming active player (we do this by setting 'numPlayers' variable in + * killing floor's game info to a large value). + * After all malicious players have successfully disconnected, - + * we remove the block. + */ + +// This fix will try to kick any player that switches between active player +// and cooldown faster than time (in seconds) in this value. +// NOTE: raising this value past default value of '0.25' +// won't actually improve crash prevention. +var private config const float spectatorChangeTimeout; + +// [ADVANCED] Don't change this setting unless you know what you're doing. +// Allows you to turn off server blocking. +// Players that don't respect timeout will still be kicked. +// This might be needed if this fix conflicts with another mutator +// that also changes 'numPlayers'. +// However, it is necessary to block aggressive enough server crash attempts, +// but can cause compatibility issues with some mutators. +// It's highly preferred to rewrite such a mutator to be compatible. +// NOTE: it should be compatible with most faked players-type mutators, +// since this fix remembers the difference between amount of +// real players and 'numPlayers'. +// After unblocking, it sets 'numPlayers' to +// the current amount of real players + that difference. +// So 4 players + 3 (=7 numPlayers) after kicking 1 player becomes +// 3 players + 3 (=6 numPlayers). +var private config const bool allowServerBlock; + +// Stores remaining cooldown value before the next allowed +// spectator change per player. +struct CooldownRecord +{ + var PlayerController player; + var float cooldown; +}; + +// Currently active cooldowns +var private array currentCooldowns; + +// Players who were decided to be violators and +// were marked for disconnecting. +// We'll be maintaining server block as long as even one +// of them hasn't yet disconnected. +var private array violators; + +// Is server currently blocked? +var private bool becomingActiveBlocked; +// This value introduced to accommodate mods such as faked player that can +// change 'numPlayers' to a value that isn't directly tied to the +// current number of active players. +// We remember the difference between active players and 'numPlayers' +/// variable in game type before server block and add it after block is over. +// If some mod introduces a more complicated relation between amount of +// active players and 'numPlayers', then it must take care of +// compatibility on it's own. +var private int recordedNumPlayersMod; + +// If given 'PlayerController' is registered in our cooldown records, - +// returns it's index. +// If it doesn't exists (or 'none' value was passes), - returns '-1'. +private final function int GetCooldownIndex(PlayerController player) +{ + local int i; + if (player == none) return -1; + + for (i = 0; i < currentCooldowns.length; i += 1) + { + if (currentCooldowns[i].player == player) + { + return i; + } + } + return -1; +} + +// Checks if given 'PlayerController' is registered as a violator. +// 'none' value isn't a violator. +public final function bool IsViolator(PlayerController player) +{ + local int i; + if (player == none) return false; + + for (i = 0; i < violators.length; i += 1) + { + if (violators[i] == player) + { + return true; + } + } + return false; +} + +// This function is to notify our fix that some player just changed status +// of active player / spectator. +// If passes value isn't 'none', it puts given player on cooldown or kicks him. +public final function NotifyStatusChange(PlayerController player) +{ + local int index; + local CooldownRecord newRecord; + if (player == none) return; + + index = GetCooldownIndex(player); + // Players already on cool down must be kicked and marked as violators + if (index >= 0) + { + player.Destroy(); + currentCooldowns.Remove(index, 1); + violators[violators.length] = player; + if (allowServerBlock) + { + SetBlock(true); + } + } + // Players that aren't on cooldown are + // either violators (do nothing, just wait for their disconnect) + // or didn't recently change their status (put them on cooldown). + else if (!IsViolator(player)) + { + newRecord.player = player; + newRecord.cooldown = spectatorChangeTimeout; + currentCooldowns[currentCooldowns.length] = newRecord; + } +} + +// Pass 'true' to block server, 'false' to unblock. +// Only works if 'allowServerBlock' is set to 'true'. +private final function SetBlock(bool activateBlock) +{ + local KFGameType kfGameType; + // Do we even need to do anything? + if (!allowServerBlock) return; + if (activateBlock == becomingActiveBlocked) return; + // Only works with 'KFGameType' and it's children. + if (level != none) kfGameType = KFGameType(level.game); + if (kfGameType == none) return; + + // Actually block/unblock + becomingActiveBlocked = activateBlock; + if (activateBlock) + { + recordedNumPlayersMod = GetNumPlayersMod(); + // This value both can't realistically fall below + // 'kfGameType.maxPlayer' and won't overflow from random increase + // in vanilla code. + kfGameType.numPlayers = maxInt / 2; + } + else + { + // Adding 'recordedNumPlayersMod' should prevent + // faked players from breaking. + kfGameType.numPlayers = GetRealPlayers() + recordedNumPlayersMod; + } +} + +// Performs server blocking if violators have disconnected. +private final function TryUnblocking() +{ + local int i; + if (!allowServerBlock) return; + if (!becomingActiveBlocked) return; + + for (i = 0; i < violators.length; i += 1) + { + if (violators[i] != none) + { + return; + } + } + SetBlock(false); +} + +// Counts current amount of "real" active players +// (connected to the server and not spectators). +// Need 'ConnectionService' to be running, otherwise return '-1'. +private final function int GetRealPlayers() +{ + // Auxiliary variables + local int i; + local int realPlayersAmount; + local PlayerController player; + // Information extraction + local ConnectionService service; + local array connections; + service = ConnectionService(class'ConnectionService'.static.GetInstance()); + if (service == none) return -1; + + // Count non-spectators + connections = service.GetActiveConnections(); + realPlayersAmount = 0; + for (i = 0; i < connections.length; i += 1) + { + player = connections[i].controllerReference; + if (player == none) continue; + if (player.playerReplicationInfo == none) continue; + if (!player.playerReplicationInfo.bOnlySpectator) + { + realPlayersAmount += 1; + } + } + return realPlayersAmount; +} + +// Calculates difference between current amount of "real" active players +// and 'numPlayers' from 'KFGameType'. +// Most typically this difference will be non-zero when using +// faked players-type mutators +// (difference will be equal to the amount of faked players). +private final function int GetNumPlayersMod() +{ + local KFGameType kfGameType; + if (level != none) kfGameType = KFGameType(level.game); + if (kfGameType == none) return 0; + return kfGameType.numPlayers - GetRealPlayers(); +} + +private final function ReduceCooldowns(float timePassed) +{ + local int i; + i = 0; + while (i < currentCooldowns.length) + { + currentCooldowns[i].cooldown -= timePassed; + if ( currentCooldowns[i].player != none + && currentCooldowns[i].cooldown > 0.0) + { + i += 1; + } + else + { + currentCooldowns.Remove(i, 1); + } + } +} + +event Tick(float delta) +{ + local float trueTimePassed; + trueTimePassed = delta * (1.1 / level.timeDilation); + TryUnblocking(); + ReduceCooldowns(trueTimePassed); +} + +defaultproperties +{ + // Configurable variables + spectatorChangeTimeout = 0.25 + allowServerBlock = true + // Inner variables + becomingActiveBlocked = false + // Listeners + requiredListeners(0) = class'BroadcastListener_FixSpectatorCrash' +} \ No newline at end of file diff --git a/sources/FixZedTimeLags/FixZedTimeLags.uc b/sources/FixZedTimeLags/FixZedTimeLags.uc new file mode 100644 index 0000000..c43ab2f --- /dev/null +++ b/sources/FixZedTimeLags/FixZedTimeLags.uc @@ -0,0 +1,189 @@ +/** + * This feature fixes lags caused by a zed time that can occur + * on some maps when a lot of zeds are present at once. + * As a side effect it also fixes an issue where during zed time speed up + * 'zedTimeSlomoScale' was assumed to be default value of '0.2'. + * Now zed time will behave correctly with mods that + * change 'zedTimeSlomoScale'. + * Copyright 2020 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class FixZedTimeLags extends Feature + dependson(ConnectionService) + config(AcediaFixes); + +/** + * When zed time activates, game speed is immediately set to + * 'zedTimeSlomoScale' (0.2 by default), defined, like all other variables, + * in 'KFGameType'. Zed time lasts 'zedTimeDuration' seconds (3.0 by default), + * but during last 'zedTimeDuration * 0.166' seconds (by default 0.498) + * it starts to speed back up, causing game speed to update every tick. + * This makes animations look more smooth when exiting zed-time; + * however, updating speed every tick for that purpose seems like + * an overkill and, combined with things like + * increased tick rate, certain maps and raised zed limit, + * it can lead to noticeable lags at the end of zed time. + * To fix this issue we disable 'Tick' event in + * 'KFGameType' and then repeat that functionality in our own 'Tick' event, + * but only perform game speed updates occasionally, + * to make sure that overall amount of updates won't go over a limit, + * that can be configured via 'maxGameSpeedUpdatesAmount' + * Author's test (looking really hard on clots' animations) + * seem to suggest that there shouldn't be much visible difference if + * we limit game speed updates to about 2 or 3. + */ + +// Max amount of game speed updates during speed up phase +// (actual amount of updates can't be larger than amount of ticks). +// On servers with default 30 tick rate there's usually +// about 13 updates total on vanilla game. +// Values lower than 1 are treated like 1. +var private config const int maxGameSpeedUpdatesAmount; +// [ADVANCED] Don't change this setting unless you know what you're doing. +// Compatibility setting that allows to keep 'GameInfo' 's 'Tick' event +// from being disabled. +// Useful when running Acedia along with custom 'GameInfo' +// (that isn't 'KFGameType') that relies on 'Tick' event. +// Note, however, that in order to keep this fix working properly, +// it's on you to make sure 'KFGameType.Tick()' logic isn't executed. +var private config const bool disableTick; +// Counts how much time is left until next update +var private float updateCooldown; +// Recorded game type, to avoid constant conversions every tick +var private KFGameType gameType; + +protected function OnEnabled() +{ + gameType = KFGameType(level.game); + if (gameType == none) + { + Destroy(); + } + else if (disableTick) + { + gameType.Disable('Tick'); + } +} + +protected function OnDisabled() +{ + gameType = KFGameType(level.game); + if (gameType != none && disableTick) + { + gameType.Enable('Tick'); + } +} + +event Tick(float delta) +{ + local float trueTimePassed; + if (gameType == none) return; + if (!gameType.bZEDTimeActive) return; + // Unfortunately we need to keep disabling 'Tick' probe function, + // because it constantly gets enabled back and I don't know where + // (maybe native code?); only really matters during zed time. + if (disableTick) + { + gameType.Disable('Tick'); + } + // How much real (not in-game) time has passed + trueTimePassed = delta * (1.1 / level.timeDilation); + gameType.currentZEDTimeDuration -= trueTimePassed; + + // Handle speeding up phase + if (gameType.bSpeedingBackUp) + { + DoSpeedBackUp(trueTimePassed); + } + else if (gameType.currentZEDTimeDuration < GetSpeedupDuration()) + { + gameType.bSpeedingBackUp = true; + updateCooldown = GetFullUpdateCooldown(); + TellClientsZedTimeEnds(); + DoSpeedBackUp(trueTimePassed); + } + // End zed time once it's duration has passed + if (gameType.currentZEDTimeDuration <= 0) + { + gameType.bZEDTimeActive = false; + gameType.bSpeedingBackUp = false; + gameType.zedTimeExtensionsUsed = 0; + gameType.SetGameSpeed(1.0); + } +} + +private final function TellClientsZedTimeEnds() +{ + local int i; + local KFPlayerController player; + local ConnectionService service; + local array connections; + service = ConnectionService(class'ConnectionService'.static.GetInstance()); + if (service == none) return; + connections = service.GetActiveConnections(); + for (i = 0; i < connections.length; i += 1) + { + player = KFPlayerController(connections[i].controllerReference); + if (player != none) + { + // Play sound of leaving zed time + player.ClientExitZedTime(); + } + } +} + +// This function is called every tick during speed up phase and manages +// gradual game speed increase. +private final function DoSpeedBackUp(float trueTimePassed) +{ + // Game speed will always be updated in our 'Tick' event + // at the very end of the zed time. + // The rest of the updates will be uniformly distributed + // over the speed up duration. + + local float newGameSpeed; + local float slowdownScale; + if (maxGameSpeedUpdatesAmount <= 1) return; + if (updateCooldown > 0.0) + { + updateCooldown -= trueTimePassed; + return; + } + else + { + updateCooldown = GetFullUpdateCooldown(); + } + slowdownScale = gameType.currentZEDTimeDuration / GetSpeedupDuration(); + newGameSpeed = Lerp(slowdownScale, 1.0, gameType.zedTimeSlomoScale); + gameType.SetGameSpeed(newGameSpeed); +} + +private final function float GetSpeedupDuration() +{ + return gameType.zedTimeDuration * 0.166; +} + +private final function float GetFullUpdateCooldown() +{ + return GetSpeedupDuration() / maxGameSpeedUpdatesAmount; +} + +defaultproperties +{ + maxGameSpeedUpdatesAmount = 3 + disableTick = true +} \ No newline at end of file diff --git a/sources/Manifest.uc b/sources/Manifest.uc new file mode 100644 index 0000000..4276afb --- /dev/null +++ b/sources/Manifest.uc @@ -0,0 +1,33 @@ +/** + * Manifest for AcediaFixes package + * Copyright 2020 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ + class Manifest extends _manifest + abstract; + +defaultproperties +{ + features(0) = class'FixZedTimeLags' + features(1) = class'FixDoshSpam' + features(2) = class'FixFFHack' + features(3) = class'FixInfiniteNades' + features(4) = class'FixAmmoSelling' + features(5) = class'FixSpectatorCrash' + features(6) = class'FixDualiesCost' + features(7) = class'FixInventoryAbuse' +} \ No newline at end of file