diff --git a/.github/workflows/gcc-build.yml b/.github/workflows/gcc-build.yml index 1f6ade7000282..84ece5be934e1 100644 --- a/.github/workflows/gcc-build.yml +++ b/.github/workflows/gcc-build.yml @@ -8,7 +8,9 @@ jobs: build: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + with: + submodules: recursive - name: Dependencies run: | sudo apt-get update && sudo apt-get install -yq libboost-all-dev g++-11 @@ -17,7 +19,7 @@ jobs: run: | mkdir bin cd bin - cmake ../ -DWITH_WARNINGS=1 -DWITH_COREDEBUG=0 -DUSE_COREPCH=1 -DUSE_SCRIPTPCH=1 -DTOOLS=1 -DSCRIPTS=dynamic -DSERVERS=1 -DNOJEM=0 -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS="-Werror" -DCMAKE_CXX_FLAGS="-Werror" -DCMAKE_C_FLAGS_DEBUG="-DNDEBUG" -DCMAKE_CXX_FLAGS_DEBUG="-DNDEBUG" -DCMAKE_INSTALL_PREFIX=check_install -DBUILD_TESTING=1 + cmake ../ -DWITH_WARNINGS=1 -DWITH_COREDEBUG=0 -DUSE_COREPCH=1 -DUSE_SCRIPTPCH=1 -DTOOLS=1 -DSCRIPTS=dynamic -DSERVERS=1 -DNOJEM=0 -DWITH_CAIO_EXAMPLES=1 -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS="-Werror" -DCMAKE_CXX_FLAGS="-Werror" -DCMAKE_C_FLAGS_DEBUG="-DNDEBUG" -DCMAKE_CXX_FLAGS_DEBUG="-DNDEBUG" -DCMAKE_INSTALL_PREFIX=check_install -DBUILD_TESTING=1 cd .. - name: Build run: | diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000..b284b822b0405 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "dep/smallfolk_cpp/smallfolk_cpp"] + path = dep/smallfolk_cpp/smallfolk_cpp + url = https://github.com/Rochet2/smallfolk_cpp.git \ No newline at end of file diff --git a/CAIO LICENSE.txt b/CAIO LICENSE.txt new file mode 100644 index 0000000000000..94a9ed024d385 --- /dev/null +++ b/CAIO LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/CAIO_README.md b/CAIO_README.md new file mode 100644 index 0000000000000..6a45fed45c329 --- /dev/null +++ b/CAIO_README.md @@ -0,0 +1,185 @@ +## Introduction + +CAIO is a server-client communication system for WoW AddOns. It is an extension of [AIO](https://github.com/Rochet2/AIO) to support C++ server side handling. +AIO is designed for sending lua addons and data between players and server. + +Currently CAIO only supports TrinityCore 3.3.5 branch. [Compare and review](https://github.com/TrinityCore/TrinityCore/compare/3.3.5...SaiFi0102:CAIO-3.3.5). + +## Supported AIO version + +AIO version **1.75** — must match `AIO_VERSION` in your server and client `AIO.lua` files (for example `C:\Users\tqbat\Documents\Cores\stuff\AIO`). + +## Install + ++ Clone this repository/branch or merge with your own TrinityCore 3.3.5 branch ++ `git submodule update --init --recursive` (required for `dep/smallfolk_cpp/smallfolk_cpp` — [smallfolk_cpp](https://github.com/Rochet2/smallfolk_cpp); pin the submodule commit in production, e.g. `git -C dep/smallfolk_cpp/smallfolk_cpp checkout ` after init) ++ Build/Install TrinityCore ++ [Install(Add) (C)AIO scripts](#api-reference) — see also `doc/CAIO_SCRIPT_EXAMPLE.md` ++ Run SQL files from `TrinityCore_Installation_Dir/sql/CAIO` (`Auth.sql` on auth DB, `World.sql` on world DB) ++ Copy `AIO_Client` from your AIO tree to `WoW_Installation_Dir/Interface/AddOns/AIO_Client` (use the same AIO repo/commit as the server expects) ++ Copy server-side client addon sources into `TrinityCore_Installation_Dir/lua_client_scripts` (one folder per addon, e.g. `lua_client_scripts/ExampleWindow/ExampleWindow.lua`) ++ Set `AIO.MsgMaxLen` to **255** in `worldserver.conf` (WoW addon whisper limit; matches client `AIO.lua` when `AIO_SERVER` is false) ++ Optional: build with `-DWITH_CAIO_EXAMPLES=ON` to include the `ExampleWindow` test script (ships `worldserver/lua_client_scripts/ExampleWindow/ExampleWindow.lua` for install) + +## Build notes (TrinityCore 3.3.5 + CAIO) + ++ OpenSSL **3.x** is detected by upstream `cmake/macros/FindOpenSSL.cmake` after the 3.3.5 merge (no manual `-D_OPENSSL_VERSION` needed in most cases). ++ Boost **1.86** with MSVC may still fail on very old 3.3.5 code paths; **Boost 1.81** is a safe choice if you hit `std::_snprintf` errors in Boost headers. ++ Example CMake configure: + +```powershell +cmake -S . -B build -G "Visual Studio 17 2022" -A x64 -DSCRIPTS=static -DTOOLS=0 ` + -DOPENSSL_INCLUDE_DIR="C:/Program Files/OpenSSL-Win64/include" ` + -DSSL_EAY_RELEASE="C:/Program Files/OpenSSL-Win64/lib/VC/x64/MD/libssl.lib" ` + -DLIB_EAY_RELEASE="C:/Program Files/OpenSSL-Win64/lib/VC/x64/MD/libcrypto.lib" ` + -DMYSQL_INCLUDE_DIR="C:/Program Files/MySQL/MySQL Server 8.0/include" ` + -DMYSQL_LIBRARY="C:/Program Files/MySQL/MySQL Server 8.0/lib/libmysql.lib" ` + -DBoost_DIR="C:/local/boost_1_81_0/lib64-msvc-14.3/cmake/Boost-1.81.0" +``` + +## Stock AIO server parity (C++ vs `AIO.lua` with `AIO_SERVER = true`) + ++ **Transport:** `LANG_ADDON` whispers with `S`/`C` prefix are required on 3.3.5; the client receives them as `CHAT_MSG_ADDON` (same as stock Eluna server). Not a CAIO gap. ++ **Init hooks:** C++ `AddInitArgs` appends extra handler blocks to the init reply. `AddOnInit` on `AIOScript` mutates the full outgoing init table before send (stock `AIO.AddOnInit` parity). ++ **Pre-init gating:** Stock server does **not** queue pre-init blocks (`AIO_INITED` is client-only). CAIO matches that. ++ **Block arg limit:** Server rejects blocks with `n > 15` (same as stock server Lua). ++ **Message cache:** `AIO.MsgCacheTime` / `AIO.MsgCacheDelay` match `AIO_MSG_CACHE_TIME` / `AIO_MSG_CACHE_DELAY` in `AIO.lua`. + +## Todo + ++ Implement obfuscation (optional, deferred) ++ Implement compression (optional, deferred) ++ Add individual RBAC permissions per `.caio` subcommand (optional; all subcommands use `RBAC_PERM_COMMAND_CAIO` today) + +## API reference + +### Creating a CAIO script + +```cpp +class ExampleCAIOScript : public AIOScript +{ + public: + ExampleCAIOScript() + : AIOScript("ExampleScriptName") + { + using namespace std::placeholders; + + // Loads addon files; path from AIO.ClientScriptPath in worldserver.conf + AddAddon("ExampleAddon", "example_addon.lua"); + AddAddon("AnotherAddon", "example_addon.lua", 192); + + AddHandler("Print", std::bind(&ExampleCAIOScript::HandlePrint, this, _1, _2)); + AddInitArgs("ExampleScriptName", "Init", std::bind(&ExampleCAIOScript::InitArg, this, _1)); + } + + void HandlePrint(Player* sender, LuaVal const& args) + { + LuaVal inputVal = args.get(4); + if (!inputVal.isstring()) + return; + } + + private: + std::string storedString; +}; +``` + +### smallfolk_cpp LuaVal reference + +https://github.com/Rochet2/smallfolk_cpp (v2.x on `master` — linked as CMake target `smallfolk_cpp::smallfolk`, include `smallfolk.h`) + +Use `LuaVal::nil` (not `LuaVal::nil()`) for default optional arguments. Type tag accessor is `typetag()`. + +### CAIO reference and functions + +**AIOScript.h** (included from `ScriptMgr.h`) + +```cpp +class AIOScript : public ScriptObject +{ + protected: + AIOScript(LuaVal const& scriptKey); + void AddHandler(LuaVal const& handlerKey, HandlerFunc function); + void AddInitArgs(LuaVal const& scriptKey, LuaVal const& handlerKey, ...); + void AddOnInit(InitMessageFunc func); // mutates full init reply (AIO.AddOnInit parity) + bool AddAddon(std::string const& addonName, std::string const& addonFile, uint32 permission = AIO_DEFAULT_ADDON_PERMISSION); +}; +``` + +**AIOMsg.h** + +```cpp +class AIOMsg +{ + public: + AIOMsg(); + AIOMsg(LuaVal const& scriptKey, LuaVal const& handlerKey, + LuaVal const& a1 = LuaVal::nil, LuaVal const& a2 = LuaVal::nil, LuaVal const& a3 = LuaVal::nil, + LuaVal const& a4 = LuaVal::nil, LuaVal const& a5 = LuaVal::nil, LuaVal const& a6 = LuaVal::nil); + AIOMsg& Add(LuaVal const& scriptKey, LuaVal const& handlerKey, + LuaVal const& a1 = LuaVal::nil, LuaVal const& a2 = LuaVal::nil, LuaVal const& a3 = LuaVal::nil, + LuaVal const& a4 = LuaVal::nil, LuaVal const& a5 = LuaVal::nil, LuaVal const& a6 = LuaVal::nil); + AIOMsg& AppendLast(LuaVal const& a1 = LuaVal::nil, LuaVal const& a2 = LuaVal::nil, LuaVal const& a3 = LuaVal::nil, + LuaVal const& a4 = LuaVal::nil, LuaVal const& a5 = LuaVal::nil, LuaVal const& a6 = LuaVal::nil); + std::string dumps() const; +}; +``` + +**Player.h** / **PlayerAIO.h** + +```cpp +// Player.h — no LuaVal in the public player API +void Player::SendSimpleAIOMessage(std::string const& message); +void Player::ForceReloadAddons(); +void Player::ForceResetAddons(); + +// PlayerAIO.h — use from CAIO scripts +namespace Trinity::AIO { + void Message(Player* player, AIOMsg& msg); + void Handle(Player* player, LuaVal const& scriptKey, LuaVal const& handlerKey, ...); + void Handle(Player* player, char const* scriptKey, char const* handlerKey, ...); +} +``` + +**World.h** + +```cpp +struct AIOAddon { /* name, file, permission */ }; + +std::string GetAIOPrefix() const; +std::string GetAIOClientScriptPath() const; +void ForceReloadPlayerAddons(uint32 permission = AIO_DEFAULT_ADDON_PERMISSION); +void ForceResetPlayerAddons(uint32 permission = AIO_DEFAULT_ADDON_PERMISSION); +void AIOMessageAll(AIOMsg& msg, uint32 permission = AIO_DEFAULT_ADDON_PERMISSION); +void SendAllSimpleAIOMessage(std::string const& message, uint32 permission = AIO_DEFAULT_ADDON_PERMISSION); +bool ReloadAddons(); +bool AddAddon(AIOAddon const& addon); +bool RemoveAddon(std::string const& addonName, uint32* permission = nullptr); +``` + +## CAIO game commands + ++ .caio version ++ .caio addaddon $addonName [$permission] "$addonFile" ++ .caio removeaddon $addonName ++ .caio reloadaddons ++ .caio forcereload $playerName ++ .caio forcereset $playerName ++ .caio forcereloadall [$permission] ++ .caio forceresetall [$permission] ++ .caio send $playerName "Message" ++ .caio sendall [$permission] "Message" + +Note: By default every player has permission 195. Permission 195 will be used if not specified. + +## Reporting issues and submitting fixes + +Issues can be reported via the [Github issue tracker](https://github.com/SaiFi0102/TrinityCore/issues). Fixes can be submitted as pull requests on the Github repository. + +## Authors, Contributors & Thanks + ++ Saif + + CAIO ++ Rochet2 + + [AIO](https://github.com/Rochet2/AIO) + + [smallfolk_cpp](https://github.com/Rochet2/smallfolk_cpp) to handle and transmit Lua data in C++ diff --git a/cmake/macros/ConfigureScripts.cmake b/cmake/macros/ConfigureScripts.cmake index 7ad99f95ba72a..de6304b8d8ac4 100644 --- a/cmake/macros/ConfigureScripts.cmake +++ b/cmake/macros/ConfigureScripts.cmake @@ -36,7 +36,7 @@ endfunction() # Stores the project name of the given module in the variable function(GetProjectNameOfScriptModule module variable) - string(TOLOWER "scripts_${SCRIPT_MODULE}" GENERATED_NAME) + string(TOLOWER "scripts_${module}" GENERATED_NAME) set(${variable} "${GENERATED_NAME}" PARENT_SCOPE) endfunction() diff --git a/cmake/options.cmake b/cmake/options.cmake index e939528584f0a..005d74770001c 100644 --- a/cmake/options.cmake +++ b/cmake/options.cmake @@ -33,6 +33,7 @@ foreach(SCRIPT_MODULE ${SCRIPT_MODULE_LIST}) set_property(CACHE ${SCRIPT_MODULE_VARIABLE} PROPERTY STRINGS default disabled static dynamic) endforeach() +option(WITH_CAIO_EXAMPLES "Build optional CAIO ExampleWindow test script" 0) option(TOOLS "Build map/vmap/mmap extraction/assembler tools" 1) option(USE_SCRIPTPCH "Use precompiled headers when compiling scripts" 1) option(USE_COREPCH "Use precompiled headers when compiling servers" 1) diff --git a/cmake/showoptions.cmake b/cmake/showoptions.cmake index 242b2500cdefc..9206f803f0d79 100644 --- a/cmake/showoptions.cmake +++ b/cmake/showoptions.cmake @@ -34,6 +34,12 @@ else() message("* Build with scripts : No") endif() +if(WITH_CAIO_EXAMPLES) + message("* CAIO ExampleWindow : Yes") +else() + message("* CAIO ExampleWindow : No (default)") +endif() + if(TOOLS) message("* Build map/vmap tools : Yes (default)") else() diff --git a/dep/CMakeLists.txt b/dep/CMakeLists.txt index 869f4ef298165..4d0d859c7783e 100644 --- a/dep/CMakeLists.txt +++ b/dep/CMakeLists.txt @@ -31,6 +31,11 @@ if(SERVERS) add_subdirectory(mysql) add_subdirectory(readline) add_subdirectory(gsoap) + set(SMALLFOLK_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(SMALLFOLK_BUILD_BENCHMARK OFF CACHE BOOL "" FORCE) + set(SMALLFOLK_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(SMALLFOLK_ENABLE_CLANG_TIDY OFF CACHE BOOL "" FORCE) + add_subdirectory(smallfolk_cpp/smallfolk_cpp) endif() if(SERVERS AND BUILD_EFSW) diff --git a/dep/smallfolk_cpp/smallfolk_cpp b/dep/smallfolk_cpp/smallfolk_cpp new file mode 160000 index 0000000000000..750580db99847 --- /dev/null +++ b/dep/smallfolk_cpp/smallfolk_cpp @@ -0,0 +1 @@ +Subproject commit 750580db99847f6043fdf952d572f0c40acc3e55 diff --git a/doc/CAIO_MESSAGE_FORMAT.md b/doc/CAIO_MESSAGE_FORMAT.md new file mode 100644 index 0000000000000..31b647cccff4f --- /dev/null +++ b/doc/CAIO_MESSAGE_FORMAT.md @@ -0,0 +1,37 @@ +# CAIO / AIO message format + +This matches [Rochet2 AIO](https://github.com/Rochet2/AIO) block layout as used by CAIO and `smallfolk_cpp`. + +## Serialized message + +One whisper addon payload is a **smallfolk** dump of an array of blocks: + +```lua +{ + { n, scriptKey, handlerKey, arg1, arg2, ... }, + { n, scriptKey2, handlerKey2, ... }, +} +``` + +- `n` — number of arguments **including** `handlerKey` (CAIO sets `block[1]` accordingly). Server rejects `n > 15` (stock `AIO_SERVER` limit). +- `scriptKey` — block name (`AIO`, `AIOExample`, …); must be registered with `AIO.RegisterEvent` on the client or `AIOScript` on the server. +- Arguments from index 4 onward are handler parameters (`unpack(data, 3, n+2)` on the Lua side). + +Use `AIOMsg` / `AIO.Msg():Add(...)` rather than building tables manually. + +## Wire encoding (3.3.5) + +On TrinityCore, AIO uses **`CHAT_MSG_WHISPER` with `LANG_ADDON`** (not a separate addon channel packet). The client still sees **`CHAT_MSG_ADDON`**; this matches stock `AIO.lua` server behaviour. + +- Prefix: `S` + `AIO.Prefix` + `\t` (server→client) or `C` + prefix + `\t` (client→server). +- Short message: two bytes `\1\1` then the smallfolk string. +- Long message: 2-byte message id + 2-byte part count + 2-byte part id + chunk. +- Each whisper packet must fit in **255 bytes** total (WoW addon limit). Configure `AIO.MsgMaxLen` (default **255**). Long payloads split using `chunkLen = MsgMaxLen - headerBytes` where `headerBytes = 1 + len(prefix) + 1 + 6`. + +## Version handshake + +On init, client sends `AIO` / `Init` with protocol version **1.75**. Server defines `AIO_VERSION` / `AIO_VERSION_STRING` in `src/server/game/AIO/AIO.h`. + +## Compression / obfuscation + +Not implemented on this branch. Addon files are sent with an `U` (uncompressed) prefix only. diff --git a/doc/CAIO_SCRIPT_EXAMPLE.md b/doc/CAIO_SCRIPT_EXAMPLE.md new file mode 100644 index 0000000000000..fde77911f2686 --- /dev/null +++ b/doc/CAIO_SCRIPT_EXAMPLE.md @@ -0,0 +1,75 @@ +# CAIO script example + +Inherit `AIOScript` to register server-side AIO handlers. See [smallfolk_cpp](https://github.com/Rochet2/smallfolk_cpp) for `LuaVal` usage. + +```cpp +class ExampleAIOScript : public AIOScript +{ + public: + ExampleAIOScript() + : AIOScript("ExampleScriptName") + { + using namespace std::placeholders; + + // Loads addon files; path from AIO.ClientScriptPath in worldserver.conf + AddAddon("ExampleAddon", "example_addon.lua"); + + // Optional permission-gated addon (RBAC id) + AddAddon("AnotherAddon", "example_addon.lua", 192); + + AddHandler("Print", std::bind(&ExampleAIOScript::HandlePrint, this, _1, _2)); + AddHandler("Save", std::bind(&ExampleAIOScript::HandleSave, this, _1, _2)); + + AddInitArgs("ExampleScriptName", "Init", std::bind(&ExampleAIOScript::InitArg, this, _1), std::bind(&ExampleAIOScript::InitArg, this, _1)); + AddInitArgs("ExampleScriptName", "Init", std::bind(&ExampleAIOScript::InitArg2, this, _1)); + AddInitArgs("AnotherScript", "InitB"); + } + + void HandlePrint(Player* sender, LuaVal const& args) + { + LuaVal inputVal = args.get(4); + LuaVal sliderVal = args.get(5); + + if (!inputVal.isstring() || !sliderVal.isnumber()) + return; + + sender->GetSession()->SendNotification("HandlePrint -> Stored String: %s, Input: %s, Slider Value: %f", + storedString.c_str(), inputVal.str().c_str(), sliderVal.num()); + } + + void HandleSave(Player* sender, LuaVal const& args) + { + LuaVal saveVal = args.get(4); + + if (!saveVal.isstring()) + return; + + storedString = saveVal.str(); + sender->GetSession()->SendNotification("Saved"); + } + + LuaVal InitArg(Player* /*sender*/) + { + LuaVal arg(TTABLE); + arg.set("key", 12.3); + arg["key2"] = false; + return arg; + } + + LuaVal InitArg2(Player* /*sender*/) + { + return LuaVal("LuaVal will implicitly create a string LuaVal for this arg"); + } + + private: + std::string storedString; +}; +``` + +Register the script in a loader (see `src/server/scripts/AIO/aio_script_loader.cpp` and `WITH_CAIO_EXAMPLES`). + +To send messages to the client from C++, use `Trinity::AIO::Handle` / `Trinity::AIO::Message` from `PlayerAIO.h` (keeps `Player.h` free of `LuaVal`). + +For full init-message rewriting (stock `AIO.AddOnInit`), call `AddOnInit` with a callback that receives `(Player*, LuaVal& initMessage)` after the init blocks are assembled and before send. + +Handler `args` is always a table; handler parameters start at index **4**. Always check value types before use. diff --git a/sql/CAIO/Auth.sql b/sql/CAIO/Auth.sql new file mode 100644 index 0000000000000..8cf9785f911db --- /dev/null +++ b/sql/CAIO/Auth.sql @@ -0,0 +1,2 @@ +REPLACE INTO `rbac_permissions` (`id`, `name`) VALUES (5000, 'Command: caio'); +REPLACE INTO `rbac_linked_permissions` (`id`, `linkedId`) VALUES (192, 5000); \ No newline at end of file diff --git a/sql/CAIO/World.sql b/sql/CAIO/World.sql new file mode 100644 index 0000000000000..38e57572c297b --- /dev/null +++ b/sql/CAIO/World.sql @@ -0,0 +1,24 @@ +-- CAIO command result strings +REPLACE INTO `trinity_string` (`entry`, `content_default`, `content_loc1`, `content_loc2`, `content_loc3`, `content_loc4`, `content_loc5`, `content_loc6`, `content_loc7`, `content_loc8`) +VALUES (60002, 'Force reload message sent to %s', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); +REPLACE INTO `trinity_string` (`entry`, `content_default`, `content_loc1`, `content_loc2`, `content_loc3`, `content_loc4`, `content_loc5`, `content_loc6`, `content_loc7`, `content_loc8`) +VALUES (60003, 'Force reset message sent to %s', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); +REPLACE INTO `trinity_string` (`entry`, `content_default`, `content_loc1`, `content_loc2`, `content_loc3`, `content_loc4`, `content_loc5`, `content_loc6`, `content_loc7`, `content_loc8`) +VALUES (60004, 'There was a problem reloading client addons. Force reload was not sent.', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); +REPLACE INTO `trinity_string` (`entry`, `content_default`, `content_loc1`, `content_loc2`, `content_loc3`, `content_loc4`, `content_loc5`, `content_loc6`, `content_loc7`, `content_loc8`) +VALUES (60005, 'Addon with name \'%s\' already exists or file not found.', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); +REPLACE INTO `trinity_string` (`entry`, `content_default`, `content_loc1`, `content_loc2`, `content_loc3`, `content_loc4`, `content_loc5`, `content_loc6`, `content_loc7`, `content_loc8`) +VALUES (60006, 'Addon with name \'%s\' not found.', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +-- CAIO commands +REPLACE INTO `command` (`name`, `permission`, `help`) VALUES ('caio', 5000, 'Syntax: .caio $subcommand\nType .caio to see the list of possible subcommands or .help caio $subcommand to see info on subcommands'); +REPLACE INTO `command` (`name`, `permission`, `help`) VALUES ('caio version', 5000, 'Syntax: .caio version\nShows the AIO version'); +REPLACE INTO `command` (`name`, `permission`, `help`) VALUES ('caio addaddon', 5000, 'Syntax: .caio addaddon $addonName [$permission] "$addonFile"\nAdds an addon to addon list for players with $permission and force reloads all affected player addons'); +REPLACE INTO `command` (`name`, `permission`, `help`) VALUES ('caio removeaddon', 5000, 'Syntax: .caio removeaddon $addonName\nRemoves an addon from addon list and force reloads all affected player addons'); +REPLACE INTO `command` (`name`, `permission`, `help`) VALUES ('caio reloadaddons', 5000, 'Syntax: .caio reloadaddons\nReloads all client addons loaded on the server and forces reload on all players.'); +REPLACE INTO `command` (`name`, `permission`, `help`) VALUES ('caio forcereload', 5000, 'Syntax: .caio forcereload $playerName\nForce reloads player\'s addons. Player addons are synced with server.'); +REPLACE INTO `command` (`name`, `permission`, `help`) VALUES ('caio forcereset', 5000, 'Syntax: .caio forcereset $playerName\nForce resets player\'s addons. Player addons are deleted and downloaded again.'); +REPLACE INTO `command` (`name`, `permission`, `help`) VALUES ('caio forcereloadall', 5000, 'Syntax: .caio forcereloadall [$permission]\nForce reloads players of $permission and above. Affected players\' addons are synced with the server.'); +REPLACE INTO `command` (`name`, `permission`, `help`) VALUES ('caio forceresetall', 5000, 'Syntax: .caio forceresetall [$permission]\nForce resets players of $permission and above. Affected players\' addons are deleted and downloaded again.'); +REPLACE INTO `command` (`name`, `permission`, `help`) VALUES ('caio send', 5000, 'Syntax: .caio send $playerName "Message"\nSends an addon message to player'); +REPLACE INTO `command` (`name`, `permission`, `help`) VALUES ('caio sendall', 5000, 'Syntax: .caio sendall [$permission] "Message"\nSends an addon message to all players of $permission and above'); diff --git a/sql/updates/world/3.3.5/2026_05_31_00_world.sql b/sql/updates/world/3.3.5/2026_05_31_00_world.sql new file mode 100644 index 0000000000000..0398a8c84df7d --- /dev/null +++ b/sql/updates/world/3.3.5/2026_05_31_00_world.sql @@ -0,0 +1,3 @@ +UPDATE `creature_template` SET `gossip_menu_id`=7169 WHERE `entry`=16237; +UPDATE `gossip_menu` SET `MenuID`=7169 WHERE `MenuID`=57031; +UPDATE `conditions` SET `SourceGroup`=7169 WHERE `SourceTypeOrReferenceId`=14 AND `SourceGroup`=57031; diff --git a/src/common/Logging/Log.cpp b/src/common/Logging/Log.cpp index 304cee0fbfa74..1cd98cc057755 100644 --- a/src/common/Logging/Log.cpp +++ b/src/common/Logging/Log.cpp @@ -224,6 +224,11 @@ void Log::OutCommandImpl(uint32 account, Trinity::FormatStringView messageFormat write(std::make_unique(LOG_LEVEL_INFO, "commands.gm", Trinity::StringVFormat(messageFormat, messageFormatArgs), Trinity::ToString(account))); } +void Log::OutAIOMessageImpl(uint32 account, LogLevel level, Trinity::FormatStringView messageFormat, Trinity::FormatArgs messageFormatArgs) +{ + write(std::make_unique(level, "AIO", Trinity::StringVFormat(messageFormat, messageFormatArgs), account ? Trinity::ToString(account) : "World")); +} + void Log::write(std::unique_ptr msg) const { Logger const* logger = GetLoggerByType(msg->type); diff --git a/src/common/Logging/Log.h b/src/common/Logging/Log.h index 33a4d6c5731e9..d6ee52f671186 100644 --- a/src/common/Logging/Log.h +++ b/src/common/Logging/Log.h @@ -86,6 +86,15 @@ class TC_COMMON_API Log this->OutCommandImpl(account, fmt, Trinity::MakeFormatArgs(args...)); } + template + void outAIOMessage(uint32 account, LogLevel level, Trinity::FormatString fmt, Args&&... args) + { + if (!ShouldLog("AIO", level)) + return; + + OutAIOMessageImpl(account, level, fmt, Trinity::MakeFormatArgs(args...)); + } + void OutCharDump(char const* str, uint32 account_id, uint64 guid, char const* name); void SetRealmId(uint32 id); @@ -113,6 +122,7 @@ class TC_COMMON_API Log void RegisterAppender(uint8 index, AppenderCreatorFn appenderCreateFn); void OutMessageImpl(std::string_view filter, LogLevel level, Trinity::FormatStringView messageFormat, Trinity::FormatArgs messageFormatArgs); void OutCommandImpl(uint32 account, Trinity::FormatStringView messageFormat, Trinity::FormatArgs messageFormatArgs); + void OutAIOMessageImpl(uint32 account, LogLevel level, Trinity::FormatStringView messageFormat, Trinity::FormatArgs messageFormatArgs); std::unordered_map appenderFactory; std::unordered_map> appenders; diff --git a/src/server/game/AIO/AIO.h b/src/server/game/AIO/AIO.h new file mode 100644 index 0000000000000..96c67f3f9989f --- /dev/null +++ b/src/server/game/AIO/AIO.h @@ -0,0 +1,31 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#ifndef TRINITY_AIO_H +#define TRINITY_AIO_H + +#include "Define.h" + +#define AIO_VERSION 1.75 +#define AIO_VERSION_STRING "1.75" + +// RBAC permission id used when distributing addons/messages to players (see sql/CAIO/Auth.sql) +constexpr uint32 AIO_DEFAULT_ADDON_PERMISSION = 195; +// WoW addon whisper payload limit (matches client AIO.lua when AIO_SERVER is false) +constexpr uint32 AIO_MAX_WHISPER_LENGTH = 255; + +#endif diff --git a/src/server/game/AIO/AIOCodec.h b/src/server/game/AIO/AIOCodec.h new file mode 100644 index 0000000000000..06815448b9d9e --- /dev/null +++ b/src/server/game/AIO/AIOCodec.h @@ -0,0 +1,48 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#ifndef TRINITY_AIO_CODEC_H +#define TRINITY_AIO_CODEC_H + +#include "Define.h" +#include +#include +#include + +namespace Trinity::AIO::Codec +{ + inline uint32 DecodeByte(char value) + { + return uint32(static_cast(value) - 1u); + } + + inline uint32 DecodePair(char high, char low) + { + return DecodeByte(high) * 254u + DecodeByte(low); + } + + inline bool IsClientPrefix(std::string const& clientWirePrefix, std::string const& msg, size_t& delimPosOut) + { + delimPosOut = msg.find('\t'); + if (delimPosOut == std::string::npos) + return false; + + return delimPosOut == clientWirePrefix.size() && msg.compare(0, delimPosOut, clientWirePrefix) == 0; + } +} + +#endif diff --git a/src/server/game/AIO/AIOUtil.cpp b/src/server/game/AIO/AIOUtil.cpp new file mode 100644 index 0000000000000..c8b62f106b1c3 --- /dev/null +++ b/src/server/game/AIO/AIOUtil.cpp @@ -0,0 +1,81 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#include "AIOUtil.h" + +#include + +namespace Trinity::AIO +{ +bool IsSafeAddonRelativePath(std::string const& path) +{ + if (path.empty() || path.front() == '/' || path.front() == '\\') + return false; + + if (path.find("..") != std::string::npos) + return false; + + return path.find_first_of(":*?\"<>|") == std::string::npos; +} + +LoadMessageOutcome TryLoadIncomingMessage(std::string const& message, uint32 maxBytes, uint32 maxBlocks) +{ + LoadMessageOutcome outcome; + + if (message.size() > maxBytes) + { + outcome.result = LoadMessageResult::Oversize; + return outcome; + } + + try + { + outcome.table = LuaVal::loads(message); + } + catch (std::exception const&) + { + outcome.result = LoadMessageResult::ParseError; + return outcome; + } + catch (...) + { + outcome.result = LoadMessageResult::ParseError; + return outcome; + } + + if (outcome.table.isnil()) + { + outcome.result = LoadMessageResult::ParseError; + return outcome; + } + + if (!outcome.table.istable()) + { + outcome.result = LoadMessageResult::NotTable; + return outcome; + } + + if (outcome.table.len() > maxBlocks) + { + outcome.result = LoadMessageResult::TooManyBlocks; + return outcome; + } + + outcome.result = LoadMessageResult::Ok; + return outcome; +} +} diff --git a/src/server/game/AIO/AIOUtil.h b/src/server/game/AIO/AIOUtil.h new file mode 100644 index 0000000000000..d5fb87076748c --- /dev/null +++ b/src/server/game/AIO/AIOUtil.h @@ -0,0 +1,49 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#ifndef TRINITY_AIO_UTIL_H +#define TRINITY_AIO_UTIL_H + +#include "Define.h" +#include "smallfolk.h" +#include + +namespace Trinity::AIO +{ + constexpr uint32 MAX_BLOCK_ARGS = 15; + + TC_GAME_API bool IsSafeAddonRelativePath(std::string const& path); + + enum class LoadMessageResult : uint8 + { + Ok, + Oversize, + ParseError, + NotTable, + TooManyBlocks + }; + + struct LoadMessageOutcome + { + LoadMessageResult result = LoadMessageResult::ParseError; + LuaVal table; + }; + + TC_GAME_API LoadMessageOutcome TryLoadIncomingMessage(std::string const& message, uint32 maxBytes, uint32 maxBlocks); +} + +#endif diff --git a/src/server/game/AIO/PlayerAIO.cpp b/src/server/game/AIO/PlayerAIO.cpp new file mode 100644 index 0000000000000..2cf462a3ad736 --- /dev/null +++ b/src/server/game/AIO/PlayerAIO.cpp @@ -0,0 +1,46 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#include "PlayerAIO.h" +#include "Player.h" + +namespace Trinity::AIO +{ +void Message(Player* player, AIOMsg& msg) +{ + if (player) + player->SendSimpleAIOMessage(msg.dumps()); +} + +void Handle(Player* player, LuaVal const& scriptKey, LuaVal const& handlerKey, + LuaVal const& a1, LuaVal const& a2, LuaVal const& a3, + LuaVal const& a4, LuaVal const& a5, LuaVal const& a6) +{ + if (!player) + return; + + AIOMsg msg(scriptKey, handlerKey, a1, a2, a3, a4, a5, a6); + player->SendSimpleAIOMessage(msg.dumps()); +} + +void Handle(Player* player, char const* scriptKey, char const* handlerKey, + LuaVal const& a1, LuaVal const& a2, LuaVal const& a3, + LuaVal const& a4, LuaVal const& a5, LuaVal const& a6) +{ + Handle(player, LuaVal(scriptKey), LuaVal(handlerKey), a1, a2, a3, a4, a5, a6); +} +} diff --git a/src/server/game/AIO/PlayerAIO.h b/src/server/game/AIO/PlayerAIO.h new file mode 100644 index 0000000000000..620decce65806 --- /dev/null +++ b/src/server/game/AIO/PlayerAIO.h @@ -0,0 +1,38 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#ifndef TRINITY_PLAYER_AIO_H +#define TRINITY_PLAYER_AIO_H + +#include "AIOMsg.h" +#include "smallfolk.h" + +class Player; + +namespace Trinity::AIO +{ + TC_GAME_API void Message(Player* player, AIOMsg& msg); + TC_GAME_API void Handle(Player* player, LuaVal const& scriptKey, LuaVal const& handlerKey, + LuaVal const& a1 = LuaVal::nil, LuaVal const& a2 = LuaVal::nil, LuaVal const& a3 = LuaVal::nil, + LuaVal const& a4 = LuaVal::nil, LuaVal const& a5 = LuaVal::nil, LuaVal const& a6 = LuaVal::nil); + + TC_GAME_API void Handle(Player* player, char const* scriptKey, char const* handlerKey, + LuaVal const& a1 = LuaVal::nil, LuaVal const& a2 = LuaVal::nil, LuaVal const& a3 = LuaVal::nil, + LuaVal const& a4 = LuaVal::nil, LuaVal const& a5 = LuaVal::nil, LuaVal const& a6 = LuaVal::nil); +} + +#endif diff --git a/src/server/game/AIO/WorldAIO.cpp b/src/server/game/AIO/WorldAIO.cpp new file mode 100644 index 0000000000000..3e3be6acbb724 --- /dev/null +++ b/src/server/game/AIO/WorldAIO.cpp @@ -0,0 +1,50 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#include "WorldAIO.h" +#include "AIOMsg.h" +#include "Player.h" +#include "World.h" +#include "WorldSession.h" + +namespace Trinity::AIO +{ +void MessageAll(World* world, AIOMsg& msg, uint32 permission) +{ + if (!world) + return; + + std::string const messageStr = msg.dumps(); + for (SessionMap::const_iterator itr = world->GetAllSessions().begin(); itr != world->GetAllSessions().end(); ++itr) + { + if (itr->second->GetPlayer() && itr->second->HasPermission(permission)) + itr->second->GetPlayer()->SendSimpleAIOMessage(messageStr); + } +} + +void SendAllSimple(World* world, std::string const& message, uint32 permission) +{ + if (!world) + return; + + for (SessionMap::const_iterator itr = world->GetAllSessions().begin(); itr != world->GetAllSessions().end(); ++itr) + { + if (itr->second->GetPlayer() && itr->second->HasPermission(permission)) + itr->second->GetPlayer()->SendSimpleAIOMessage(message); + } +} +} diff --git a/src/server/game/AIO/WorldAIO.h b/src/server/game/AIO/WorldAIO.h new file mode 100644 index 0000000000000..8095c25b961a2 --- /dev/null +++ b/src/server/game/AIO/WorldAIO.h @@ -0,0 +1,33 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#ifndef TRINITY_WORLD_AIO_H +#define TRINITY_WORLD_AIO_H + +#include "AIO.h" +#include + +class AIOMsg; +class World; + +namespace Trinity::AIO +{ + TC_GAME_API void MessageAll(World* world, AIOMsg& msg, uint32 permission = AIO_DEFAULT_ADDON_PERMISSION); + TC_GAME_API void SendAllSimple(World* world, std::string const& message, uint32 permission = AIO_DEFAULT_ADDON_PERMISSION); +} + +#endif diff --git a/src/server/game/Accounts/RBAC.h b/src/server/game/Accounts/RBAC.h index e4ae4da4d92c2..d19f0f352a871 100644 --- a/src/server/game/Accounts/RBAC.h +++ b/src/server/game/Accounts/RBAC.h @@ -751,6 +751,7 @@ enum RBACPermissions // // IF YOU ADD NEW PERMISSIONS, ADD THEM IN MASTER BRANCH AS WELL! // + RBAC_PERM_COMMAND_CAIO = 5000, // custom permissions 1000+ RBAC_PERM_MAX }; diff --git a/src/server/game/Achievements/AchievementMgr.cpp b/src/server/game/Achievements/AchievementMgr.cpp index 109ee47a5957f..d71d6e0d1dc25 100644 --- a/src/server/game/Achievements/AchievementMgr.cpp +++ b/src/server/game/Achievements/AchievementMgr.cpp @@ -379,7 +379,7 @@ bool AchievementCriteriaData::Meets(uint32 criteria_id, Player const* source, Wo Unit const* unitTarget = target->ToUnit(); if (!unitTarget) return false; - return unitTarget->GetGender() == Gender(gender.gender); + return unitTarget->GetGender() == static_cast<::Gender>(gender.gender); } case ACHIEVEMENT_CRITERIA_DATA_TYPE_SCRIPT: { diff --git a/src/server/game/CMakeLists.txt b/src/server/game/CMakeLists.txt index 48731a4ec3061..1918cf9a17d73 100644 --- a/src/server/game/CMakeLists.txt +++ b/src/server/game/CMakeLists.txt @@ -37,7 +37,8 @@ target_include_directories(game-interface target_link_libraries(game-interface INTERFACE shared - Detour) + Detour + smallfolk_cpp::smallfolk) add_library(game ${PRIVATE_SOURCES}) diff --git a/src/server/game/Chat/AIOMsg.cpp b/src/server/game/Chat/AIOMsg.cpp new file mode 100644 index 0000000000000..5778e2a9500c1 --- /dev/null +++ b/src/server/game/Chat/AIOMsg.cpp @@ -0,0 +1,98 @@ +#include "AIOMsg.h" +#include "Player.h" + +AIOMsg::AIOMsg() + : _val(TTABLE) +{ } + +AIOMsg& AIOMsg::Add(LuaVal const& scriptKey, LuaVal const& handlerKey, LuaVal const& a1, LuaVal const& a2, LuaVal const& a3, LuaVal const& a4, LuaVal const& a5, LuaVal const& a6) +{ + LuaVal block(TTABLE); + uint32 nArgs = 1; + + block[1] = 0; + block[2] = scriptKey; + block[3] = handlerKey; + + if (!a1.isnil()) + { + block.insert(a1); + ++nArgs; + } + if (!a2.isnil()) + { + block.insert(a2); + ++nArgs; + } + if (!a3.isnil()) + { + block.insert(a3); + ++nArgs; + } + if (!a4.isnil()) + { + block.insert(a4); + ++nArgs; + } + if (!a5.isnil()) + { + block.insert(a5); + ++nArgs; + } + if (!a6.isnil()) + { + block.insert(a6); + ++nArgs; + } + + block[1] = static_cast(nArgs); + _val.insert(block); + return *this; +} + +AIOMsg& AIOMsg::AppendLast(LuaVal const& a1, LuaVal const& a2, LuaVal const& a3, LuaVal const& a4, LuaVal const& a5, LuaVal const& a6) +{ + unsigned int lastBlock = _val.len(); + if (!lastBlock) + return *this; + + LuaVal& block = _val.at(static_cast(lastBlock)); + LuaVal nArgsVal = block.get(1); + if (!nArgsVal.isnumber()) + return *this; + + unsigned int nArgs = static_cast(static_cast(nArgsVal.num())); + if (!a1.isnil()) + { + block.insert(a1); + ++nArgs; + } + if (!a2.isnil()) + { + block.insert(a2); + ++nArgs; + } + if (!a3.isnil()) + { + block.insert(a3); + ++nArgs; + } + if (!a4.isnil()) + { + block.insert(a4); + ++nArgs; + } + if (!a5.isnil()) + { + block.insert(a5); + ++nArgs; + } + if (!a6.isnil()) + { + block.insert(a6); + ++nArgs; + } + + block[1] = static_cast(nArgs); + return *this; +} diff --git a/src/server/game/Chat/AIOMsg.h b/src/server/game/Chat/AIOMsg.h new file mode 100644 index 0000000000000..582a910d13c47 --- /dev/null +++ b/src/server/game/Chat/AIOMsg.h @@ -0,0 +1,54 @@ +#ifndef AIO_MESSAGE_H +#define AIO_MESSAGE_H + +#include "smallfolk.h" + +class Player; + +class AIOMsg +{ + public: + // Creates an empty AIOMsg + AIOMsg(); + + // Creates an AIO message and adds one block + AIOMsg(LuaVal const& scriptKey, LuaVal const& handlerKey, + LuaVal const& a1 = LuaVal::nil, LuaVal const& a2 = LuaVal::nil, LuaVal const& a3 = LuaVal::nil, + LuaVal const& a4 = LuaVal::nil, LuaVal const& a5 = LuaVal::nil, LuaVal const& a6 = LuaVal::nil) + : _val(TTABLE) + { + Add(scriptKey, handlerKey, a1, a2, a3, a4, a5, a6); + } + + AIOMsg(char const* scriptKey, char const* handlerKey, + LuaVal const& a1 = LuaVal::nil, LuaVal const& a2 = LuaVal::nil, LuaVal const& a3 = LuaVal::nil, + LuaVal const& a4 = LuaVal::nil, LuaVal const& a5 = LuaVal::nil, LuaVal const& a6 = LuaVal::nil) + : _val(TTABLE) + { + Add(LuaVal(scriptKey), LuaVal(handlerKey), a1, a2, a3, a4, a5, a6); + } + + // Adds another block (calls another handler in one message) + AIOMsg& Add(LuaVal const& scriptKey, LuaVal const& handlerKey, + LuaVal const& a1 = LuaVal::nil, LuaVal const& a2 = LuaVal::nil, LuaVal const& a3 = LuaVal::nil, + LuaVal const& a4 = LuaVal::nil, LuaVal const& a5 = LuaVal::nil, LuaVal const& a6 = LuaVal::nil); + + AIOMsg& Add(char const* scriptKey, char const* handlerKey, + LuaVal const& a1 = LuaVal::nil, LuaVal const& a2 = LuaVal::nil, LuaVal const& a3 = LuaVal::nil, + LuaVal const& a4 = LuaVal::nil, LuaVal const& a5 = LuaVal::nil, LuaVal const& a6 = LuaVal::nil) + { + return Add(LuaVal(scriptKey), LuaVal(handlerKey), a1, a2, a3, a4, a5, a6); + } + + // Appends arguments to the last block + AIOMsg& AppendLast(LuaVal const& a1 = LuaVal::nil, LuaVal const& a2 = LuaVal::nil, LuaVal const& a3 = LuaVal::nil, + LuaVal const& a4 = LuaVal::nil, LuaVal const& a5 = LuaVal::nil, LuaVal const& a6 = LuaVal::nil); + + // Returns smallfolk dump of the AIO message + std::string dumps() const { return _val.dumps(); } + + private: + LuaVal _val; +}; + +#endif diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index e6d03d71ddb72..fa336e8416f4e 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -111,6 +111,8 @@ #include "World.h" #include "WorldPacket.h" #include "WorldSession.h" +#include "AIOMsg.h" +#include "PlayerAIO.h" #include "WorldStatePackets.h" #define ZONE_UPDATE_INTERVAL (1*IN_MILLISECONDS) @@ -337,6 +339,10 @@ Player::Player(WorldSession* session): Unit(true) m_achievementMgr = new AchievementMgr(this); m_reputationMgr = new ReputationMgr(this); + + m_aioInitCd = false; + m_aioInitTimer = 0; + m_messageIdIndex = 1; } Player::~Player() @@ -1240,6 +1246,17 @@ void Player::Update(uint32 p_time) if (IsHasDelayedTeleport()) TeleportTo(m_teleport_dest, m_teleport_options); + + // AIO init cooldown + if (m_aioInitCd) + { + m_aioInitTimer += p_time; + if (m_aioInitTimer >= sWorld->getIntConfig(CONFIG_AIO_INIT_COOLDOWN)) + { + m_aioInitCd = false; + m_aioInitTimer = 0; + } + } } void Player::Heartbeat() @@ -20556,6 +20573,70 @@ void Player::Whisper(uint32 textId, Player* target, bool /*isBossWhisper = false target->SendDirectMessage(packet.Write()); } +void Player::ForceReloadAddons() +{ + Trinity::AIO::Handle(this, "AIO", "ForceReload"); +} + +void Player::ForceResetAddons() +{ + Trinity::AIO::Handle(this, "AIO", "ForceReset"); +} + +void Player::SendSimpleAIOMessage(std::string const& message) +{ + if (message.empty()) + return; + + std::string const& aioPrefix = sWorld->GetAIOPrefix(); + uint32 const maxPacketLen = std::min(sWorld->getIntConfig(CONFIG_AIO_MSG_MAX_LEN), AIO_MAX_WHISPER_LENGTH); + uint32 const shortHeaderLen = 1 + uint32(aioPrefix.size()) + 1 + 2; // S + prefix + tab + short id + uint32 const longHeaderLen = 1 + uint32(aioPrefix.size()) + 1 + 6; // S + prefix + tab + long meta + + if (shortHeaderLen + message.size() <= size_t(maxPacketLen)) + { + std::string fullmsg = "S" + aioPrefix + "\t\x1\x1" + message; + WorldPackets::Chat::Chat packet; + packet.Initialize(CHAT_MSG_WHISPER, LANG_ADDON, this, this, fullmsg); + SendDirectMessage(packet.Write()); + return; + } + + uint32 const chunkLen = maxPacketLen > longHeaderLen ? maxPacketLen - longHeaderLen : 1; + float const messageLen = float(message.size()); + uint16 const parts = uint16(std::ceil(messageLen / float(chunkLen))); + + uint16 high = uint16(std::floor(float(parts) / 254.0f)); + std::string partsStr(1, char(high + 1)); + partsStr += char(parts - high * 254 + 1); + + high = uint16(std::floor(float(m_messageIdIndex) / 254.0f)); + std::string messageIdStr(1, char(high + 1)); + messageIdStr += char(m_messageIdIndex - high * 254 + 1); + + if (m_messageIdIndex >= 64769) // 2^16 - 767 + m_messageIdIndex = 1; + else + ++m_messageIdIndex; + + size_t cursor = 0; + for (uint16 partId = 1; partId <= parts; ++partId) + { + high = uint16(std::floor(float(partId) / 254.0f)); + std::string partIdStr(1, char(high + 1)); + partIdStr += char(partId - high * 254 + 1); + + std::string fullmsg = "S" + aioPrefix + "\t" + messageIdStr + partsStr + partIdStr; + fullmsg += message.substr(cursor, chunkLen); + + WorldPackets::Chat::Chat packet; + packet.Initialize(CHAT_MSG_WHISPER, LANG_ADDON, this, this, fullmsg); + SendDirectMessage(packet.Write()); + + cursor += chunkLen; + } +} + Item* Player::GetMItem(ObjectGuid::LowType id) { ItemMap::const_iterator itr = mMitems.find(id); diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index 03d5bc6863d82..bd32749972ed1 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -73,6 +73,7 @@ class PlayerMenu; class PlayerSocial; class ReputationMgr; class SpellCastTargets; +class AIOMsg; class TradeData; enum InventoryType : uint8; @@ -1063,6 +1064,24 @@ class TC_GAME_API Player : public Unit, public GridObject void Whisper(uint32 textId, Player* target, bool isBossWhisper = false) override; void WhisperAddon(std::string const& text, Player* receiver); + // AIO can only understand smallfolk LuaVal::dumps() format + // Handler functions are called by creating a table as below + // { + // {n, ScriptName, HandlerName(optional), Arg1..N(optional) }, + // {n, AnotherScriptName, AnotherHandlerName(optional), Arg1..N(optional) } + // } + // Where n is number of arguments including handler name as an argument + void SendSimpleAIOMessage(const std::string &message); + + // Forces reload on the player AIO addons (syncs with server) + void ForceReloadAddons(); + + // Force reset: client addon data is deleted and downloaded again + void ForceResetAddons(); + + bool isAIOInitOnCooldown() const { return m_aioInitCd; } + void setAIOIntOnCooldown(bool cd) { m_aioInitCd = cd; m_aioInitTimer = 0; } + /*********************************************************/ /*** STORAGE SYSTEM ***/ /*********************************************************/ @@ -2549,11 +2568,16 @@ class TC_GAME_API Player : public Unit, public GridObject uint32 _activeCheats; + bool m_aioInitCd; + uint32 m_aioInitTimer; + uint16 m_messageIdIndex; + // variables to save health and mana before duel and restore them after duel uint32 healthBeforeDuel; uint32 manaBeforeDuel; WorldLocation _corpseLocation; + }; TC_GAME_API void AddItemsSetItem(Player* player, Item* item); diff --git a/src/server/game/Groups/Group.cpp b/src/server/game/Groups/Group.cpp index dfbfb700fab5a..1011ccea29efa 100644 --- a/src/server/game/Groups/Group.cpp +++ b/src/server/game/Groups/Group.cpp @@ -63,7 +63,7 @@ Group::Group() : m_leaderGuid(), m_leaderName(""), m_groupType(GROUPTYPE_NORMAL) m_dungeonDifficulty(DUNGEON_DIFFICULTY_NORMAL), m_raidDifficulty(RAID_DIFFICULTY_10MAN_NORMAL), m_bgGroup(nullptr), m_bfGroup(nullptr), m_lootMethod(FREE_FOR_ALL), m_lootThreshold(ITEM_QUALITY_UNCOMMON), m_looterGuid(), m_masterLooterGuid(), m_subGroupsCounts(nullptr), m_guid(), m_counter(0), m_maxEnchantingLevel(0), m_dbStoreId(0), m_isLeaderOffline(false), - m_scriptRef(this, NoopGroupDeleter()) +m_scriptRef(this, NoopGroupDeleter()) { for (uint8 i = 0; i < TARGET_ICONS_COUNT; ++i) m_targetIcons[i].Clear(); diff --git a/src/server/game/Handlers/ChatHandler.cpp b/src/server/game/Handlers/ChatHandler.cpp index 5721df4b60d2b..37a061164ae97 100644 --- a/src/server/game/Handlers/ChatHandler.cpp +++ b/src/server/game/Handlers/ChatHandler.cpp @@ -179,7 +179,7 @@ void WorldSession::HandleChatMessage(ChatMsg type, Language lang, std::string ms return; } - if (msg.size() > 255) + if (msg.size() > 255u) return; // Our Warden module also uses SendAddonMessage as a way to communicate Lua check results to the server, see if this is that @@ -293,6 +293,15 @@ void WorldSession::HandleChatMessage(ChatMsg type, Language lang, std::string ms } Player* receiver = ObjectAccessor::FindConnectedPlayerByName(target); + + if (lang == LANG_ADDON && receiver) + { + IncomingAIOWhisperResult const aioResult = HandleIncomingAIOClientWhisper(sender, receiver, msg); + if (aioResult == IncomingAIOWhisperResult::DropPacket) + return; + if (aioResult == IncomingAIOWhisperResult::Consumed) + break; + } if (!receiver || (lang != LANG_ADDON && !receiver->isAcceptWhispers() && receiver->GetSession()->HasPermission(rbac::RBAC_PERM_CAN_FILTER_WHISPERS) && !receiver->IsInWhisperWhiteList(sender->GetGUID()))) { SendPlayerNotFoundNotice(target); diff --git a/src/server/game/Miscellaneous/Language.h b/src/server/game/Miscellaneous/Language.h index 7df9976243c81..6b4a64eb4caad 100644 --- a/src/server/game/Miscellaneous/Language.h +++ b/src/server/game/Miscellaneous/Language.h @@ -1243,6 +1243,12 @@ enum TrinityStrings LANG_SHUTDOWN_CANCELLED = 11018, LANG_YOU_CHANGE_POWER = 11019, // master branch ONLY LANG_YOUR_POWER_CHANGED = 11020, // master branch ONLY - LANG_INVALID_POWER_NAME = 11021 // master branch ONLY + LANG_INVALID_POWER_NAME = 11021, // master branch ONLY + + LANG_CAIO_FORCERELOAD_SENT = 60002, + LANG_CAIO_FORCERESET_SENT = 60003, + LANG_CAIO_RELOADADDONS_ERROR = 60004, + LANG_CAIO_ADDADDON_ERROR = 60005, + LANG_CAIO_REMOVEADDON_ERROR = 60006, }; #endif diff --git a/src/server/game/Scripting/AIOScript.cpp b/src/server/game/Scripting/AIOScript.cpp new file mode 100644 index 0000000000000..50ff0fa90a734 --- /dev/null +++ b/src/server/game/Scripting/AIOScript.cpp @@ -0,0 +1,283 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#include "ScriptMgr.h" +#include "AIOUtil.h" +#include "Log.h" +#include "Player.h" +#include "PlayerAIO.h" +#include "ScriptMgr.h" +#include "World.h" +#include +#include + +class AIOHandlers : public AIOScript +{ + public: + struct InitHookInfo + { + LuaVal scriptKey; + LuaVal handlerKey; + std::list argsList; + + InitHookInfo(LuaVal const& scriptKey, LuaVal const& handlerKey) + : scriptKey(scriptKey), handlerKey(handlerKey) + { } + }; + + using HookListType = std::list; + + void RegisterInitMessageHook(AIOScript::InitMessageFunc func) + { + _initMessageHooks.push_back(std::move(func)); + } + + void RegisterInitArgs(LuaVal const& scriptKey, LuaVal const& handlerKey, + AIOScript::ArgFunc a1, AIOScript::ArgFunc a2, AIOScript::ArgFunc a3, + AIOScript::ArgFunc a4, AIOScript::ArgFunc a5, AIOScript::ArgFunc a6) + { + std::list* list = nullptr; + for (HookListType::iterator itr = _initHookList.begin(); itr != _initHookList.end(); ++itr) + { + if (itr->scriptKey == scriptKey && itr->handlerKey == handlerKey) + { + list = &itr->argsList; + break; + } + } + + if (!list) + { + _initHookList.push_back(InitHookInfo(scriptKey, handlerKey)); + list = &_initHookList.back().argsList; + } + + if (a1) + list->push_back(a1); + if (a2) + list->push_back(a2); + if (a3) + list->push_back(a3); + if (a4) + list->push_back(a4); + if (a5) + list->push_back(a5); + if (a6) + list->push_back(a6); + } + + private: + friend AIOHandlers* CreateAIOHandlers(); + AIOHandlers(); + void HandleInit(Player* sender, LuaVal const& args); + void HandleError(Player* sender, LuaVal const& args); + + HookListType _initHookList; + std::list _initMessageHooks; +}; + +AIOScript::AIOScriptByKeyMap AIOScript::_scriptByKeyMap = AIOScript::AIOScriptByKeyMap(); + +AIOScript::~AIOScript() +{ + _scriptByKeyMap.erase(_key); +} + +void AIOScript::AddOnInit(InitMessageFunc func) +{ + sScriptMgr->RegisterAIOInitHook(std::move(func)); +} + +void AIOScript::AddInitArgs(LuaVal const& scriptKey, LuaVal const& handlerKey, ArgFunc a1, ArgFunc a2, ArgFunc a3, ArgFunc a4, ArgFunc a5, ArgFunc a6) +{ + sScriptMgr->RegisterAIOInitArgs(scriptKey, handlerKey, std::move(a1), std::move(a2), std::move(a3), std::move(a4), std::move(a5), std::move(a6)); +} + +AIOScript::AIOScript(LuaVal const& scriptKey) + : ScriptObject(scriptKey.tostring().c_str()), _key(scriptKey) +{ + if (AIOScript::_scriptByKeyMap.find(scriptKey) != AIOScript::_scriptByKeyMap.end()) + { + sLog->outAIOMessage(0, LOG_LEVEL_FATAL, "AIO scriptKey '{}' of type tag '{}' already exist. Use another key.", scriptKey.tostring(), static_cast(scriptKey.typetag())); + ASSERT(false); + } + sScriptMgr->RegisterAIOScript(this); + AIOScript::_scriptByKeyMap[scriptKey] = this; +} + +void AIOScript::ClearScriptByKeyMap() +{ + _scriptByKeyMap.clear(); +} + +AIOScript* AIOScript::FindByKey(LuaVal const& scriptKey) +{ + AIOScriptByKeyMap::const_iterator itr = _scriptByKeyMap.find(scriptKey); + if (itr == _scriptByKeyMap.end()) + return nullptr; + + return itr->second; +} + +void AIOScript::HandleAddonBlock(Player* sender, LuaVal const& handlerKey, LuaVal const& args) +{ + OnHandle(sender, handlerKey, args); +} + +void AIOScript::DispatchIncomingBlocks(Player* sender, LuaVal const& mainTable) +{ + for (unsigned int i = 1; i <= mainTable.len(); ++i) + { + LuaVal block = mainTable.get(static_cast(i)); + if (!block.istable()) + continue; + + LuaVal nArgsVal = block.get(1); + LuaVal scriptKeyVal = block.get(2); + LuaVal handlerKeyVal = block.get(3); + if (!nArgsVal.isnumber() || scriptKeyVal.isnil() || handlerKeyVal.isnil()) + continue; + + if (nArgsVal.num() > double(Trinity::AIO::MAX_BLOCK_ARGS)) + { + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_ERROR, + "AIO: Block from '{}' has over {} arguments (n={:.0f}). Sender: {}", + scriptKeyVal.tostring(), Trinity::AIO::MAX_BLOCK_ARGS, nArgsVal.num(), sender->GetName()); + continue; + } + + if (AIOScript* aioScript = AIOScript::FindByKey(scriptKeyVal)) + aioScript->HandleAddonBlock(sender, handlerKeyVal, block); + } +} + +bool AIOScript::AddAddon(std::string const& addonName, std::string const& addonFile, uint32 permission) +{ + return sWorld->AddAddon(World::AIOAddon(addonName, addonFile, permission)); +} + +void AIOScript::OnHandle(Player* sender, LuaVal const& handlerKey, LuaVal const& args) +{ + HandlerMapType::const_iterator itr = _handlerMap.find(handlerKey); + if (itr != _handlerMap.end()) + { + itr->second(sender, args); + return; + } + + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_ERROR, "AIO: No handler '{}' on script '{}'. Sender: {}", + handlerKey.tostring(), _key.tostring(), sender->GetName()); +} + +AIOHandlers::AIOHandlers() + : AIOScript("AIO") +{ + AddHandler("Init", std::bind(&AIOHandlers::HandleInit, this, std::placeholders::_1, std::placeholders::_2)); + AddHandler("Error", std::bind(&AIOHandlers::HandleError, this, std::placeholders::_1, std::placeholders::_2)); +} + +void AIOHandlers::HandleInit(Player* sender, LuaVal const& args) +{ + if (sender->isAIOInitOnCooldown()) + return; + + LuaVal versionVal = args.get(4); + LuaVal clientDataVal = args.get(5); + if (!versionVal.isnumber() || !clientDataVal.istable()) + { + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_ERROR, "AIOHandlers::HandleInit: Invalid version value or clientData value. Sender: {}, Args: {}", sender->GetName(), args.dumps()); + return; + } + + if (std::abs(versionVal.num() - AIO_VERSION) > 0.01) + { + Trinity::AIO::Handle(sender, "AIO", "Init", AIO_VERSION); + return; + } + + sender->setAIOIntOnCooldown(true); + + LuaVal addonTable(TTABLE); + LuaVal cacheTable(TTABLE); + uint32 const nAddons = sWorld->PrepareClientAddons(clientDataVal, addonTable, cacheTable, sender); + + LuaVal argsToSend(TTABLE); + + uint32 blockIndex = 1; + for (HookListType::const_iterator itr = _initHookList.begin(); itr != _initHookList.end(); ++itr) + { + uint32 index = 3; + LuaVal hookBlock(TTABLE); + + hookBlock[1] = static_cast(itr->argsList.size()) + 1u; + hookBlock[2] = itr->scriptKey; + hookBlock[3] = itr->handlerKey; + for (std::list::const_iterator it = itr->argsList.begin(); it != itr->argsList.end(); ++it) + hookBlock[static_cast(++index)] = (*it)(sender); + + argsToSend[static_cast(++blockIndex)] = hookBlock; + } + + LuaVal AIOInitBlock(TTABLE); + AIOInitBlock[1] = 5; + AIOInitBlock[2] = "AIO"; + AIOInitBlock[3] = "Init"; + AIOInitBlock[4] = AIO_VERSION; + AIOInitBlock[5] = static_cast(nAddons); + AIOInitBlock[6] = addonTable; + AIOInitBlock[7] = cacheTable; + + argsToSend[1] = AIOInitBlock; + + for (AIOScript::InitMessageFunc const& hook : _initMessageHooks) + hook(sender, argsToSend); + + sender->SendSimpleAIOMessage(argsToSend.dumps()); +} + +void AIOHandlers::HandleError(Player* sender, LuaVal const& args) +{ + LuaVal msgVal = args.get(4); + if (!msgVal.isstring()) + return; + + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_ERROR, "{} Received client addon error: {}", sender->GetSession()->GetPlayerInfo(), msgVal.str()); +} + +AIOHandlers* CreateAIOHandlers() +{ + return new AIOHandlers(); +} + +void DestroyAIOHandlers(AIOHandlers* handlers) +{ + delete handlers; +} + +void RegisterAIOInitHookOnHandlers(AIOHandlers* handlers, AIOScript::InitMessageFunc func) +{ + if (handlers) + handlers->RegisterInitMessageHook(std::move(func)); +} + +void RegisterAIOInitArgsOnHandlers(AIOHandlers* handlers, LuaVal const& scriptKey, LuaVal const& handlerKey, + AIOScript::ArgFunc a1, AIOScript::ArgFunc a2, AIOScript::ArgFunc a3, + AIOScript::ArgFunc a4, AIOScript::ArgFunc a5, AIOScript::ArgFunc a6) +{ + if (handlers) + handlers->RegisterInitArgs(scriptKey, handlerKey, std::move(a1), std::move(a2), std::move(a3), std::move(a4), std::move(a5), std::move(a6)); +} diff --git a/src/server/game/Scripting/AIOScript.h b/src/server/game/Scripting/AIOScript.h new file mode 100644 index 0000000000000..6b21c7e2433aa --- /dev/null +++ b/src/server/game/Scripting/AIOScript.h @@ -0,0 +1,93 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#ifndef TRINITY_AIOSCRIPT_H +#define TRINITY_AIOSCRIPT_H + +// Included from ScriptMgr.h after ScriptObject is defined. +#include "AIO.h" +#include "Define.h" +#include "smallfolk.h" +#include +#include + +class Player; + +// Inherit AIOScript for server-side AIO handlers (LuaVal / smallfolk_cpp). +// Full example: doc/CAIO_SCRIPT_EXAMPLE.md +class TC_GAME_API AIOScript : public ScriptObject +{ + public: + ~AIOScript() override; + + LuaVal GetKey() const { return _key; } + bool IsDatabaseBound() const { return false; } + + static AIOScript* FindByKey(LuaVal const& scriptKey); + static void ClearScriptByKeyMap(); + + void HandleAddonBlock(Player* sender, LuaVal const& handlerKey, LuaVal const& args); + static void DispatchIncomingBlocks(Player* sender, LuaVal const& mainTable); + + using HandlerFunc = std::function; + using ArgFunc = std::function; + using InitMessageFunc = std::function; + + protected: + AIOScript(LuaVal const& scriptKey); + + void AddHandler(LuaVal const& handlerKey, HandlerFunc function) { _handlerMap[handlerKey] = std::move(function); } + + void AddInitArgs(LuaVal const& scriptKey, LuaVal const& handlerKey, + ArgFunc a1 = ArgFunc(), ArgFunc a2 = ArgFunc(), ArgFunc a3 = ArgFunc(), + ArgFunc a4 = ArgFunc(), ArgFunc a5 = ArgFunc(), ArgFunc a6 = ArgFunc()); + + void AddOnInit(InitMessageFunc func); + + bool AddAddon(std::string const& addonName, std::string const& addonFile, uint32 permission = AIO_DEFAULT_ADDON_PERMISSION); + + template + ScriptClass* GetScript(LuaVal const& scriptKey); + + private: + void OnHandle(Player* sender, LuaVal const& handlerKey, LuaVal const& args); + + LuaVal _key; + + using HandlerMapType = std::unordered_map; + HandlerMapType _handlerMap; + + using AIOScriptByKeyMap = std::unordered_map; + static AIOScriptByKeyMap _scriptByKeyMap; +}; + +template +ScriptClass* AIOScript::GetScript(LuaVal const& scriptKey) +{ + return dynamic_cast(FindByKey(scriptKey)); +} + +class AIOHandlers; + +TC_GAME_API AIOHandlers* CreateAIOHandlers(); +TC_GAME_API void DestroyAIOHandlers(AIOHandlers* handlers); +TC_GAME_API void RegisterAIOInitHookOnHandlers(AIOHandlers* handlers, AIOScript::InitMessageFunc func); +TC_GAME_API void RegisterAIOInitArgsOnHandlers(AIOHandlers* handlers, LuaVal const& scriptKey, LuaVal const& handlerKey, + AIOScript::ArgFunc a1, AIOScript::ArgFunc a2, AIOScript::ArgFunc a3, + AIOScript::ArgFunc a4, AIOScript::ArgFunc a5, AIOScript::ArgFunc a6); + +#endif diff --git a/src/server/game/Scripting/ScriptMgr.cpp b/src/server/game/Scripting/ScriptMgr.cpp index 9e672f4ffe961..6a93f2f526a7d 100644 --- a/src/server/game/Scripting/ScriptMgr.cpp +++ b/src/server/game/Scripting/ScriptMgr.cpp @@ -30,7 +30,9 @@ #include "MapManager.h" #include "ObjectMgr.h" #include "OutdoorPvPMgr.h" +#include "AIOUtil.h" #include "Player.h" +#include "WorldSession.h" #include "ScriptReloadMgr.h" #include "ScriptSystem.h" #include "SmartAI.h" @@ -40,8 +42,12 @@ #include "Transport.h" #include "Vehicle.h" #include "Weather.h" +#include "World.h" + +#include #include "WorldPacket.h" #include "WorldSession.h" +#include "smallfolk.h" // Trait which indicates whether this script type // must be assigned in the database. @@ -1018,7 +1024,7 @@ std::string const& ScriptObject::GetName() const } ScriptMgr::ScriptMgr() - : _scriptCount(0), _script_loader_callback(nullptr) + : _scriptCount(0), _scheduledScripts(0), _aioHandlers(nullptr), _script_loader_callback(nullptr) { } @@ -1042,6 +1048,7 @@ void ScriptMgr::Initialize() TC_LOG_INFO("server.loading", "Loading C++ scripts"); FillSpellSummary(); + _aioHandlers = CreateAIOHandlers(); // Load core scripts SetScriptContext(GetNameOfStaticContext()); @@ -1127,6 +1134,8 @@ std::shared_ptr void ScriptMgr::Unload() { sScriptRegistryCompositum->Unload(); + AIOScript::ClearScriptByKeyMap(); + DestroyAIOHandlers(_aioHandlers); delete[] SpellSummary; delete[] UnitAI::AISpellInfo; @@ -2574,6 +2583,58 @@ PlayerScript::PlayerScript(char const* name) ScriptRegistry::Instance()->AddScript(this); } +void ScriptMgr::RegisterAIOScript(AIOScript* script) +{ + ScriptRegistry::Instance()->AddScript(script); +} + +void ScriptMgr::RegisterAIOInitHook(AIOScript::InitMessageFunc func) +{ + RegisterAIOInitHookOnHandlers(_aioHandlers, std::move(func)); +} + +void ScriptMgr::RegisterAIOInitArgs(LuaVal const& scriptKey, LuaVal const& handlerKey, + AIOScript::ArgFunc a1, AIOScript::ArgFunc a2, AIOScript::ArgFunc a3, + AIOScript::ArgFunc a4, AIOScript::ArgFunc a5, AIOScript::ArgFunc a6) +{ + RegisterAIOInitArgsOnHandlers(_aioHandlers, scriptKey, handlerKey, + std::move(a1), std::move(a2), std::move(a3), std::move(a4), std::move(a5), std::move(a6)); +} + +void ScriptMgr::OnAddonMessage(Player* sender, std::string const& message) +{ + if (!sender) + return; + + uint32 const maxIncoming = sWorld->getIntConfig(CONFIG_AIO_MAX_INCOMING); + uint32 const maxBlocks = sWorld->getIntConfig(CONFIG_AIO_MAX_BLOCKS); + Trinity::AIO::LoadMessageOutcome const outcome = Trinity::AIO::TryLoadIncomingMessage(message, maxIncoming, maxBlocks); + + switch (outcome.result) + { + case Trinity::AIO::LoadMessageResult::Oversize: + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_ERROR, + "AIO: Incoming message size {} exceeds limit {}. Sender: {}", message.size(), maxIncoming, sender->GetName()); + return; + case Trinity::AIO::LoadMessageResult::ParseError: + if (WorldSession* session = sender->GetSession()) + session->NotifyAIOIncomingParseFailure(sender); + return; + case Trinity::AIO::LoadMessageResult::NotTable: + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_DEBUG, + "AIO: Ignoring non-table addon payload from {} ({} bytes)", sender->GetName(), message.size()); + return; + case Trinity::AIO::LoadMessageResult::TooManyBlocks: + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_ERROR, + "AIO: Incoming message has {} blocks (limit {}). Sender: {}", outcome.table.len(), maxBlocks, sender->GetName()); + return; + case Trinity::AIO::LoadMessageResult::Ok: + break; + } + + AIOScript::DispatchIncomingBlocks(sender, outcome.table); +} + void PlayerScript::OnPVPKill(Player* /*killer*/, Player* /*killed*/) { } @@ -2842,3 +2903,4 @@ template class TC_GAME_API ScriptRegistry; template class TC_GAME_API ScriptRegistry; template class TC_GAME_API ScriptRegistry; template class TC_GAME_API ScriptRegistry; +template class TC_GAME_API ScriptRegistry; diff --git a/src/server/game/Scripting/ScriptMgr.h b/src/server/game/Scripting/ScriptMgr.h index c50e63e79fc50..1bd9268a93cd5 100644 --- a/src/server/game/Scripting/ScriptMgr.h +++ b/src/server/game/Scripting/ScriptMgr.h @@ -815,6 +815,8 @@ class TC_GAME_API GroupScript : public ScriptObject virtual void OnDisband(Group* group); }; +#include "AIOScript.h" + // Manages registration, loading, and execution of scripts. class TC_GAME_API ScriptMgr { @@ -1077,8 +1079,26 @@ class TC_GAME_API ScriptMgr void ModifyMeleeDamage(Unit* target, Unit* attacker, uint32& damage); void ModifySpellDamageTaken(Unit* target, Unit* attacker, int32& damage); + public: /* Scheduled scripts */ + + uint32 IncreaseScheduledScriptsCount() { return ++_scheduledScripts; } + uint32 DecreaseScheduledScriptCount() { return --_scheduledScripts; } + uint32 DecreaseScheduledScriptCount(size_t count) { return _scheduledScripts -= count; } + bool IsScriptScheduled() const { return _scheduledScripts > 0; } + + public: /* AIOScript */ + + void OnAddonMessage(Player* sender, std::string const& message); + void RegisterAIOScript(AIOScript* script); + void RegisterAIOInitHook(AIOScript::InitMessageFunc func); + void RegisterAIOInitArgs(LuaVal const& scriptKey, LuaVal const& handlerKey, + AIOScript::ArgFunc a1, AIOScript::ArgFunc a2, AIOScript::ArgFunc a3, + AIOScript::ArgFunc a4, AIOScript::ArgFunc a5, AIOScript::ArgFunc a6); + private: uint32 _scriptCount; + uint32 _scheduledScripts; + AIOHandlers* _aioHandlers; ScriptLoaderCallbackType _script_loader_callback; diff --git a/src/server/game/Server/WorldSession.cpp b/src/server/game/Server/WorldSession.cpp index 7d2edeb014b4b..e01d4a3d71229 100644 --- a/src/server/game/Server/WorldSession.cpp +++ b/src/server/game/Server/WorldSession.cpp @@ -49,6 +49,8 @@ #include "PacketUtilities.h" #include "Player.h" #include "Realm.h" +#include "AIO.h" +#include "AIOCodec.h" #include "ScriptMgr.h" #include "SocialMgr.h" #include "QueryHolder.h" @@ -318,7 +320,7 @@ bool WorldSession::Update(uint32 diff, PacketFilter& updater) } else if (_player->IsInWorld()) { - if(AntiDOS.EvaluateOpcode(*packet, currentTime)) + if (AntiDOS.EvaluateOpcode(*packet, currentTime)) { sScriptMgr->OnPacketReceive(this, *packet); opHandle->Call(this, *packet); @@ -476,9 +478,239 @@ bool WorldSession::Update(uint32 diff, PacketFilter& updater) return false; //Will remove this session from the world session map } + // AIO: drop incomplete long messages (AIO.lua AIO_MSG_CACHE_TIME / AIO_MSG_CACHE_DELAY) + if (m_Socket && m_Socket->IsOpen() && !_addonMessageBuffer.empty()) + { + _aioMsgCacheSweepTimer += diff; + uint32 const sweepDelay = sWorld->getIntConfig(CONFIG_AIO_MSG_CACHE_DELAY); + if (_aioMsgCacheSweepTimer >= sweepDelay) + { + _aioMsgCacheSweepTimer = 0; + uint32 const cacheTime = sWorld->getIntConfig(CONFIG_AIO_MSG_CACHE_TIME); + for (AddonMessageBufferMap::iterator itr = _addonMessageBuffer.begin(); itr != _addonMessageBuffer.end();) + { + itr->second.Timer += sweepDelay; + if (itr->second.Timer >= cacheTime) + _addonMessageBuffer.erase(itr++); + else + ++itr; + } + } + } + return true; } +WorldSession::AIOIncomingGateResult WorldSession::CheckAIOIncomingGate(Player* sender, size_t payloadBytes) +{ + uint32 const minInterval = sWorld->getIntConfig(CONFIG_AIO_MSG_RATE_MS); + if (!minInterval) + return AIOIncomingGateResult::Allow; + + uint32 const now = getMSTime(); + if (_aioLastCompleteMessageMs && (now - _aioLastCompleteMessageMs) < minInterval) + { + ++_aioRateLimitedCount; + if (_aioRateLimitedCount == 1 || (_aioRateLimitedCount % 25) == 0) + { + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_WARN, + "AIO: Rate limited {} incoming message(s) from {} (latest {} bytes, {} ms since last accepted)", + _aioRateLimitedCount, sender->GetName(), payloadBytes, now - _aioLastCompleteMessageMs); + } + return AIOIncomingGateResult::RateLimited; + } + + _aioLastCompleteMessageMs = now; + _aioRateLimitedCount = 0; + return AIOIncomingGateResult::Allow; +} + +void WorldSession::NotifyAIOIncomingParseFailure(Player* sender) +{ + ++_aioParseFailureCount; + uint32 const maxFailures = sWorld->getIntConfig(CONFIG_AIO_MAX_PARSE_FAILURES); + + if (_aioParseFailureCount == 1 || (_aioParseFailureCount % 5) == 0) + { + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_WARN, + "AIO: Failed to parse incoming smallfolk payload from {} (failures: {})", + sender->GetName(), _aioParseFailureCount); + } + + if (maxFailures && _aioParseFailureCount >= maxFailures) + { + RecordAIOIncomingAbuse(sender, "parse failure threshold"); + _aioParseFailureCount = 0; + } +} + +void WorldSession::RecordAIOIncomingAbuse(Player* sender, char const* reason) +{ + if (!_addonMessageBuffer.empty()) + { + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_WARN, + "AIO: Cleared {} reassembly buffer(s) for {} ({})", + uint32(_addonMessageBuffer.size()), sender->GetName(), reason); + _addonMessageBuffer.clear(); + } +} + +WorldSession::IncomingAIOWhisperResult WorldSession::HandleIncomingAIOClientWhisper(Player* sender, Player* receiver, std::string const& msg) +{ + size_t delimPos = 0; + if (!Trinity::AIO::Codec::IsClientPrefix(sWorld->GetAIOClientWirePrefix(), msg, delimPos)) + return IncomingAIOWhisperResult::NotAIO; + + if (receiver != sender) + return IncomingAIOWhisperResult::DropPacket; + + uint32 messageId = 0; + if ((msg.size() - delimPos - 1) >= size_t(2)) + { + messageId = Trinity::AIO::Codec::DecodePair(msg[delimPos + 1], msg[delimPos + 2]); + + if (messageId == 0) + { + std::string const& payload = msg.substr(delimPos + 3); + if (payload.size() > sWorld->getIntConfig(CONFIG_AIO_MAX_INCOMING)) + { + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_ERROR, + "AIO: Short message size {} exceeds limit {}. Sender: {}", + payload.size(), sWorld->getIntConfig(CONFIG_AIO_MAX_INCOMING), sender->GetName()); + return IncomingAIOWhisperResult::Consumed; + } + + if (CheckAIOIncomingGate(sender, payload.size()) == AIOIncomingGateResult::Allow) + sScriptMgr->OnAddonMessage(sender, payload); + else + RecordAIOIncomingAbuse(sender, "rate limit"); + + return IncomingAIOWhisperResult::Consumed; + } + } + + if ((msg.size() - delimPos - 1) < size_t(6)) + { + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_ERROR, + "HandleIncomingAIOClientWhisper: Received AIO long addon message without meta data. Sender: {}", sender->GetName()); + return IncomingAIOWhisperResult::Consumed; + } + + uint32 parts = Trinity::AIO::Codec::DecodePair(msg[delimPos + 3], msg[delimPos + 4]); + if (parts < 2) + { + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_ERROR, + "HandleIncomingAIOClientWhisper: Received AIO addon message with number of parts: {} (< 2). Sender: {}", parts, sender->GetName()); + return IncomingAIOWhisperResult::DropPacket; + } + + uint32 maxparts = sWorld->getIntConfig(CONFIG_AIO_MAXPARTS); + if (parts > maxparts) + { + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_ERROR, + "HandleIncomingAIOClientWhisper: Received AIO addon message with too many parts: {} (> {}). Message Id: {}, Sender: {}", + parts, maxparts, messageId, sender->GetName()); + RecordAIOIncomingAbuse(sender, "too many parts"); + return IncomingAIOWhisperResult::DropPacket; + } + + uint32 partId = Trinity::AIO::Codec::DecodePair(msg[delimPos + 5], msg[delimPos + 6]); + if (partId < 1 || partId > parts) + { + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_ERROR, + "HandleIncomingAIOClientWhisper: Invalid AIO part id {} for {} parts. Message Id: {}, Sender: {}", + partId, parts, messageId, sender->GetName()); + return IncomingAIOWhisperResult::DropPacket; + } + + std::string const partPayload = msg.substr(delimPos + 7); + uint32 const maxBufferSize = sWorld->getIntConfig(CONFIG_AIO_MAX_BUFFER_SIZE); + uint32 const maxPacketLen = std::min(sWorld->getIntConfig(CONFIG_AIO_MSG_MAX_LEN), AIO_MAX_WHISPER_LENGTH); + uint32 const longHeaderLen = uint32(sWorld->GetAIOClientWirePrefix().size()) + 1u + 6u; + uint32 const maxPartPayload = maxPacketLen > longHeaderLen ? maxPacketLen - longHeaderLen : 1u; + if (partPayload.size() > maxPartPayload) + { + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_ERROR, + "HandleIncomingAIOClientWhisper: AIO part payload {} bytes exceeds per-part limit {}. Message Id: {}, Sender: {}", + partPayload.size(), maxPartPayload, messageId, sender->GetName()); + RecordAIOIncomingAbuse(sender, "oversized part"); + return IncomingAIOWhisperResult::DropPacket; + } + + AddonMessageBufferMap::iterator messagePartsItr = _addonMessageBuffer.find(messageId); + if (messagePartsItr == _addonMessageBuffer.end()) + { + if (_addonMessageBuffer.size() >= maxparts) + { + RecordAIOIncomingAbuse(sender, "too many concurrent reassemblies"); + return IncomingAIOWhisperResult::DropPacket; + } + messagePartsItr = _addonMessageBuffer.insert(std::make_pair(messageId, LongMessageBufferInfo())).first; + } + else if (parts != messagePartsItr->second.Parts) + messagePartsItr->second = LongMessageBufferInfo(); + + if (messagePartsItr->second.Map.find(partId) != messagePartsItr->second.Map.end()) + { + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_ERROR, + "HandleIncomingAIOClientWhisper: Duplicate part {} for message id {}. Sender: {}", partId, messageId, sender->GetName()); + RecordAIOIncomingAbuse(sender, "duplicate part"); + return IncomingAIOWhisperResult::DropPacket; + } + + messagePartsItr->second.Parts = parts; + messagePartsItr->second.Timer = 0; + messagePartsItr->second.BufferedBytes += uint32(partPayload.size()); + if (messagePartsItr->second.BufferedBytes > maxBufferSize) + { + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_ERROR, + "HandleIncomingAIOClientWhisper: AIO reassembly buffer exceeded {} bytes. Message Id: {}, Sender: {}", + maxBufferSize, messageId, sender->GetName()); + RecordAIOIncomingAbuse(sender, "reassembly buffer exceeded"); + return IncomingAIOWhisperResult::DropPacket; + } + + messagePartsItr->second.Map[partId] = partPayload; + + bool haveAllParts = messagePartsItr->second.Map.size() >= static_cast(messagePartsItr->second.Parts); + if (haveAllParts) + { + for (uint32 expectedPart = 1; expectedPart <= parts; ++expectedPart) + { + if (messagePartsItr->second.Map.find(expectedPart) == messagePartsItr->second.Map.end()) + { + haveAllParts = false; + break; + } + } + } + + if (!haveAllParts) + return IncomingAIOWhisperResult::Consumed; + + std::string actualAIOMessage; + actualAIOMessage.reserve(messagePartsItr->second.BufferedBytes); + for (uint32 expectedPart = 1; expectedPart <= parts; ++expectedPart) + actualAIOMessage += messagePartsItr->second.Map.find(expectedPart)->second; + + if (actualAIOMessage.size() > sWorld->getIntConfig(CONFIG_AIO_MAX_INCOMING)) + { + sLog->outAIOMessage(sender->GetGUID().GetCounter(), LOG_LEVEL_ERROR, + "AIO: Reassembled message size {} exceeds limit {}. Sender: {}", + actualAIOMessage.size(), sWorld->getIntConfig(CONFIG_AIO_MAX_INCOMING), sender->GetName()); + _addonMessageBuffer.erase(messagePartsItr); + return IncomingAIOWhisperResult::Consumed; + } + + if (CheckAIOIncomingGate(sender, actualAIOMessage.size()) == AIOIncomingGateResult::Allow) + sScriptMgr->OnAddonMessage(sender, actualAIOMessage); + else + RecordAIOIncomingAbuse(sender, "rate limit"); + + _addonMessageBuffer.erase(messagePartsItr); + return IncomingAIOWhisperResult::Consumed; +} + /// %Log the player out void WorldSession::LogoutPlayer(bool save) { @@ -620,6 +852,9 @@ void WorldSession::LogoutPlayer(bool save) CharacterDatabase.Execute(stmt); } + //Clear aio long message buffer + _addonMessageBuffer.clear(); + m_playerLogout = false; m_playerSave = false; m_playerRecentlyLogout = true; diff --git a/src/server/game/Server/WorldSession.h b/src/server/game/Server/WorldSession.h index e37eb7bc594c0..fe6d78efdb1e6 100644 --- a/src/server/game/Server/WorldSession.h +++ b/src/server/game/Server/WorldSession.h @@ -1237,6 +1237,7 @@ class TC_GAME_API WorldSession AsyncCallbackProcessor _queryHolderProcessor; friend class World; + friend class ScriptMgr; protected: class DosProtection { @@ -1282,6 +1283,15 @@ class TC_GAME_API WorldSession return _legitCharacters.find(lowGUID) != _legitCharacters.end(); } + enum class IncomingAIOWhisperResult : uint8 + { + NotAIO, + Consumed, + DropPacket + }; + + IncomingAIOWhisperResult HandleIncomingAIOClientWhisper(Player* sender, Player* receiver, std::string const& msg); + // Movement helpers Unit* ValidateAndGetUnitBeingMoved(ObjectGuid guid, OpcodeClient opcode, bool forStatusAck) const; @@ -1348,6 +1358,32 @@ class TC_GAME_API WorldSession WorldSession(WorldSession const& right) = delete; WorldSession& operator=(WorldSession const& right) = delete; + + // AIO + typedef std::map AddonPartStringMap; + struct LongMessageBufferInfo + { + uint32 Parts = 0; + uint32 Timer = 0; + uint32 BufferedBytes = 0; + AddonPartStringMap Map; + }; + typedef std::map AddonMessageBufferMap; + AddonMessageBufferMap _addonMessageBuffer; + uint32 _aioMsgCacheSweepTimer = 0; + uint32 _aioLastCompleteMessageMs = 0; + uint32 _aioRateLimitedCount = 0; + uint32 _aioParseFailureCount = 0; + + enum class AIOIncomingGateResult : uint8 + { + Allow, + RateLimited + }; + + AIOIncomingGateResult CheckAIOIncomingGate(Player* sender, size_t payloadBytes); + void NotifyAIOIncomingParseFailure(Player* sender); + void RecordAIOIncomingAbuse(Player* sender, char const* reason); }; #endif /// @} diff --git a/src/server/game/Server/WorldSocket.cpp b/src/server/game/Server/WorldSocket.cpp index bb6dd6032244e..67df7cdbb4bae 100644 --- a/src/server/game/Server/WorldSocket.cpp +++ b/src/server/game/Server/WorldSocket.cpp @@ -95,7 +95,7 @@ bool WorldSocket::Update() if (!queued->empty()) buffer.Write(queued->contents(), queued->size()); } - else // single packet larger than buffer size + else // single packet larger than _sendBufferSize { MessageBuffer packetBuffer(queued->size() + header.getHeaderLength()); packetBuffer.Write(header.header, header.getHeaderLength()); diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index a0ed02fd095ae..aa19a235a758c 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -88,7 +88,14 @@ #include "WhoListStorage.h" #include "WorldSession.h" +#include #include +#include +#include +#include "AIOUtil.h" +#include "AIOMsg.h" +#include "WorldAIO.h" +#include "smallfolk.h" TC_GAME_API std::atomic World::m_stopEvent(false); TC_GAME_API uint8 World::m_ExitCode = SHUTDOWN_EXIT_CODE; @@ -1563,6 +1570,27 @@ void World::LoadConfigSettings(bool reload) m_bool_configs[CONFIG_CALCULATE_CREATURE_ZONE_AREA_DATA] = sConfigMgr->GetBoolDefault("Calculate.Creature.Zone.Area.Data", false); m_bool_configs[CONFIG_CALCULATE_GAMEOBJECT_ZONE_AREA_DATA] = sConfigMgr->GetBoolDefault("Calculate.Gameoject.Zone.Area.Data", false); + // AIO (255 = max WoW addon whisper length; must match client AIO_MsgLen when using client-side AIO) + m_int_configs[CONFIG_AIO_MAXPARTS] = sConfigMgr->GetIntDefault("AIO.MaxParts", 64); + m_int_configs[CONFIG_AIO_MSG_MAX_LEN] = std::min(sConfigMgr->GetIntDefault("AIO.MsgMaxLen", AIO_MAX_WHISPER_LENGTH), AIO_MAX_WHISPER_LENGTH); + m_int_configs[CONFIG_AIO_INIT_COOLDOWN] = sConfigMgr->GetIntDefault("AIO.InitCooldown", 5000); + // Matches AIO.lua AIO_MSG_CACHE_TIME / AIO_MSG_CACHE_DELAY (server-side reassembly) + m_int_configs[CONFIG_AIO_MSG_CACHE_TIME] = sConfigMgr->GetIntDefault("AIO.MsgCacheTime", 15000); + m_int_configs[CONFIG_AIO_MSG_CACHE_DELAY] = sConfigMgr->GetIntDefault("AIO.MsgCacheDelay", 5000); + m_int_configs[CONFIG_AIO_MAX_BUFFER_SIZE] = sConfigMgr->GetIntDefault("AIO.MaxBufferSize", 1048576); + m_int_configs[CONFIG_AIO_MAX_INCOMING] = sConfigMgr->GetIntDefault("AIO.MaxIncomingMessageSize", 262144); + m_int_configs[CONFIG_AIO_MSG_RATE_MS] = sConfigMgr->GetIntDefault("AIO.MsgRateLimitMs", 50); + m_int_configs[CONFIG_AIO_MAX_BLOCKS] = sConfigMgr->GetIntDefault("AIO.MaxBlocks", 32); + m_int_configs[CONFIG_AIO_MAX_PARSE_FAILURES] = sConfigMgr->GetIntDefault("AIO.MaxParseFailures", 16); + m_aioclientpath = sConfigMgr->GetStringDefault("AIO.ClientScriptPath", "lua_client_scripts"); + m_aioprefix = sConfigMgr->GetStringDefault("AIO.Prefix", "AIO"); + if (m_aioprefix.size() > 15u) + m_aioprefix = m_aioprefix.substr(0, 15); + m_aioClientWirePrefix = "C" + m_aioprefix; + + if (m_int_configs[CONFIG_AIO_MAX_INCOMING] > m_int_configs[CONFIG_AIO_MAX_BUFFER_SIZE]) + m_int_configs[CONFIG_AIO_MAX_INCOMING] = m_int_configs[CONFIG_AIO_MAX_BUFFER_SIZE]; + // HotSwap m_bool_configs[CONFIG_HOTSWAP_ENABLED] = sConfigMgr->GetBoolDefault("HotSwap.Enabled", true); m_bool_configs[CONFIG_HOTSWAP_RECOMPILER_ENABLED] = sConfigMgr->GetBoolDefault("HotSwap.EnableReCompiler", true); @@ -1616,6 +1644,7 @@ void World::SetInitialWorldSettings() ///- Initialize config settings LoadConfigSettings(); + ValidateAIOSettings(); ///- Initialize Allowed Security Level LoadDBAllowedSecurityLevel(); @@ -3598,6 +3627,193 @@ void World::ReloadRBAC() session->InvalidateRBACData(); } +bool World::AddAddon(AIOAddon const& addon) +{ + if (addon.file.empty()) + return false; + + if (!Trinity::AIO::IsSafeAddonRelativePath(addon.file)) + { + sLog->outAIOMessage(0, LOG_LEVEL_ERROR, "AIO AddAddon: rejected unsafe path '{}' for addon {}", addon.file, addon.name); + return false; + } + + // Check if addon already exist + for (AddonCodeListType::iterator itr = m_AddonList.begin(); itr != m_AddonList.end(); ++itr) + { + if (itr->name == addon.name) + { + return false; + } + } + + AIOAddon copy(addon); + copy.code = ""; + + // Format path + std::string path; + path = sWorld->GetAIOClientScriptPath(); + if (path.back() != '/' && path.back() != '\\') + { + path += '/'; + } + path += copy.file; + + // Get file + std::ifstream in(path, std::ios::in | std::ios::binary); + if (in) + { + in.seekg(0, std::ios::end); + copy.code.resize(in.tellg()); + in.seekg(0, std::ios::beg); + in.read(©.code[0], copy.code.size()); + in.close(); + if (copy.code.empty()) + { + return false; + } + } + else + { + sLog->outAIOMessage(0, LOG_LEVEL_ERROR, "AIO AddAddon: Couldn't open file {} of addon {}", path, copy.name); + return false; + } + + // Set crc on original file content + boost::crc_32_type crc_result; + crc_result.process_bytes(copy.code.data(), copy.code.length()); + copy.crc = crc_result.checksum(); + + // Uncompressed addon payload (compression/obfuscation not implemented) + copy.code = std::string(1, 'U') + copy.code; + m_AddonList.push_back(copy); + + sLog->outAIOMessage(0, LOG_LEVEL_INFO, "AIO: Loaded addon {} from file {}", copy.name, copy.file); + return true; +} + +bool World::RemoveAddon(std::string const& addonName, uint32* permission) +{ + for (AddonCodeListType::iterator itr = m_AddonList.begin(); itr != m_AddonList.end(); ++itr) + { + if (itr->name == addonName) + { + if (permission) + *permission = itr->permission; + m_AddonList.erase(itr); + return true; + } + } + return false; +} + +void World::ValidateAIOSettings() +{ + if (m_aioprefix.empty()) + TC_LOG_ERROR("server.loading", "AIO.Prefix must not be empty."); + + if (m_int_configs[CONFIG_AIO_MSG_MAX_LEN] != AIO_MAX_WHISPER_LENGTH) + { + TC_LOG_WARN("server.loading", "AIO.MsgMaxLen is {} but {} is required for 3.3.5 addon whispers; clamping.", + m_int_configs[CONFIG_AIO_MSG_MAX_LEN], AIO_MAX_WHISPER_LENGTH); + m_int_configs[CONFIG_AIO_MSG_MAX_LEN] = AIO_MAX_WHISPER_LENGTH; + } + + boost::filesystem::path clientPath(m_aioclientpath); + if (!boost::filesystem::exists(clientPath)) + TC_LOG_WARN("server.loading", "AIO.ClientScriptPath '{}' does not exist (create it or fix the config).", m_aioclientpath); + else if (!boost::filesystem::is_directory(clientPath)) + TC_LOG_ERROR("server.loading", "AIO.ClientScriptPath '{}' is not a directory.", m_aioclientpath); + + TC_LOG_INFO("server.loading", "AIO: wire prefix '{}', client scripts '{}', max incoming {} bytes, max blocks {}, rate limit {} ms, parse failure limit {}.", + m_aioClientWirePrefix, m_aioclientpath, m_int_configs[CONFIG_AIO_MAX_INCOMING], m_int_configs[CONFIG_AIO_MAX_BLOCKS], + m_int_configs[CONFIG_AIO_MSG_RATE_MS], m_int_configs[CONFIG_AIO_MAX_PARSE_FAILURES]); +} + +bool World::ReloadAddons() +{ + sLog->outAIOMessage(0, LOG_LEVEL_INFO, "World::ReloadAddons()"); + + AddonCodeListType prevAddonList; + prevAddonList.swap(m_AddonList); + try + { + for (AddonCodeListType::const_iterator itr = prevAddonList.begin(); itr != prevAddonList.end(); ++itr) + { + AddAddon(*itr); + } + } + catch (std::exception &e) + { + sLog->outAIOMessage(0, LOG_LEVEL_ERROR, "AIO: Error reloading addons. Exception: {}", e.what()); + m_AddonList.swap(prevAddonList); + return false; + } + catch (...) + { + sLog->outAIOMessage(0, LOG_LEVEL_ERROR, "AIO: Error reloading addons"); + m_AddonList.swap(prevAddonList); + return false; + } + return true; +} + +uint32 World::PrepareClientAddons(LuaVal const& clientData, LuaVal& addonsTable, LuaVal& cacheTable, Player* forPlayer) const +{ + if (!clientData.istable()) + return 0; + + uint32 i = 0; + for (AddonCodeListType::const_iterator itr = m_AddonList.begin(); itr != m_AddonList.end(); ++itr) + { + if (!forPlayer->GetSession()->HasPermission(itr->permission)) + continue; + + LuaVal CRCVal = clientData.get(itr->name); + if (CRCVal == itr->crc) + { + cacheTable[static_cast(++i)] = itr->name; + } + else + { + LuaVal addonData(TTABLE); + addonData["name"] = itr->name; + addonData["crc"] = itr->crc; + addonData["code"] = itr->code; + addonsTable[static_cast(++i)] = addonData; + } + } + return i; +} + +void World::ForceReloadPlayerAddons(uint32 permission) +{ + for (SessionMap::const_iterator itr = m_sessions.begin(); itr != m_sessions.end(); ++itr) + { + if (itr->second->GetPlayer() && itr->second->HasPermission(permission)) + itr->second->GetPlayer()->ForceReloadAddons(); + } +} + +void World::ForceResetPlayerAddons(uint32 permission) +{ + for (SessionMap::const_iterator itr = m_sessions.begin(); itr != m_sessions.end(); ++itr) + { + if (itr->second->GetPlayer() && itr->second->HasPermission(permission)) + itr->second->GetPlayer()->ForceResetAddons(); + } +} + +void World::AIOMessageAll(AIOMsg& msg, uint32 permission) +{ + Trinity::AIO::MessageAll(this, msg, permission); +} + +void World::SendAllSimpleAIOMessage(std::string const& message, uint32 permission) +{ + Trinity::AIO::SendAllSimple(this, message, permission); +} + void World::RemoveOldCorpses() { m_timers[WUPDATE_CORPSES].SetCurrent(m_timers[WUPDATE_CORPSES].GetInterval()); diff --git a/src/server/game/World/World.h b/src/server/game/World/World.h index 124e6fb5addc7..2401101eea6c8 100644 --- a/src/server/game/World/World.h +++ b/src/server/game/World/World.h @@ -35,12 +35,18 @@ #include #include +#include "AIO.h" + +class Object; class Player; class WorldPacket; class WorldSession; class WorldSocket; struct Realm; +class LuaVal; +class AIOMsg; + // ServerMessages.dbc enum ServerMessageType { @@ -388,6 +394,16 @@ enum WorldIntConfigs : uint32 CONFIG_CHARTER_COST_ARENA_5v5, CONFIG_NO_GRAY_AGGRO_ABOVE, CONFIG_NO_GRAY_AGGRO_BELOW, + CONFIG_AIO_MAXPARTS, + CONFIG_AIO_MSG_MAX_LEN, + CONFIG_AIO_INIT_COOLDOWN, + CONFIG_AIO_MSG_CACHE_TIME, + CONFIG_AIO_MSG_CACHE_DELAY, + CONFIG_AIO_MAX_BUFFER_SIZE, + CONFIG_AIO_MAX_INCOMING, + CONFIG_AIO_MSG_RATE_MS, + CONFIG_AIO_MAX_BLOCKS, + CONFIG_AIO_MAX_PARSE_FAILURES, CONFIG_AUCTION_GETALL_DELAY, CONFIG_AUCTION_SEARCH_DELAY, CONFIG_TALENTS_INSPECTING, @@ -770,6 +786,33 @@ class TC_GAME_API World void ReloadRBAC(); + struct AIOAddon + { + std::string name; + std::string code; + std::string file; + uint32 crc; + uint32 permission; + + AIOAddon(std::string const& addonName, std::string const& addonFile, uint32 permission = AIO_DEFAULT_ADDON_PERMISSION) + : name(addonName), file(addonFile), crc(0), permission(permission) { } + }; + + std::string GetAIOPrefix() const { return m_aioprefix; } + std::string const& GetAIOClientWirePrefix() const { return m_aioClientWirePrefix; } + std::string GetAIOClientScriptPath() const { return m_aioclientpath; } + + void ForceReloadPlayerAddons(uint32 permission = AIO_DEFAULT_ADDON_PERMISSION); + void ForceResetPlayerAddons(uint32 permission = AIO_DEFAULT_ADDON_PERMISSION); + void AIOMessageAll(AIOMsg& msg, uint32 permission = AIO_DEFAULT_ADDON_PERMISSION); + void SendAllSimpleAIOMessage(std::string const& message, uint32 permission = AIO_DEFAULT_ADDON_PERMISSION); + bool ReloadAddons(); + bool AddAddon(AIOAddon const& addon); + bool RemoveAddon(std::string const& addonName, uint32* permission = nullptr); + uint32 PrepareClientAddons(LuaVal const& clientData, LuaVal& addonsTable, LuaVal& cacheTable, Player* forPlayer) const; + + void ValidateAIOSettings(); + void RemoveOldCorpses(); void TriggerGuidWarning(); void TriggerGuidAlert(); @@ -891,7 +934,14 @@ class TC_GAME_API World uint32 _warnDiff; time_t _warnShutdownTime; - friend class debug_commandscript; + typedef std::list AddonCodeListType; + AddonCodeListType m_AddonList; + std::string m_aioprefix; + std::string m_aioClientWirePrefix; + std::string m_aioclientpath; + + friend class AIOScript; + friend class debug_commandscript; }; TC_GAME_API extern Realm realm; diff --git a/src/server/scripts/AIO/ExampleWindow.cpp b/src/server/scripts/AIO/ExampleWindow.cpp new file mode 100644 index 0000000000000..2917bd05b18ed --- /dev/null +++ b/src/server/scripts/AIO/ExampleWindow.cpp @@ -0,0 +1,77 @@ +#include "ScriptMgr.h" +#include "PlayerAIO.h" +#include "Player.h" +#include "Chat.h" +#include "smallfolk.h" + +// Optional CAIO test script (from Rochet2). Pairs with AIO Examples/TestWindow client addons. +// Place ExampleWindow.lua (and related client files) under lua_client_scripts/ExampleWindow/ +class ExampleWindowScript : public AIOScript +{ + public: + ExampleWindowScript() + : AIOScript("AIOExample"), counter(0) + { + AddAddon("ExampleWindow", "ExampleWindow/ExampleWindow.lua"); + AddHandler("Print", std::bind(&ExampleWindowScript::HandlePrint, this, std::placeholders::_1, std::placeholders::_2)); + AddHandler("StressTest", std::bind(&ExampleWindowScript::HandleStressTest, this, std::placeholders::_1, std::placeholders::_2)); + AddInitArgs("AIOExample", "Init", std::bind(&ExampleWindowScript::InitArg, this, std::placeholders::_1), std::bind(&ExampleWindowScript::InitArg, this, std::placeholders::_1)); + AddInitArgs("AIOExample", "Init", std::bind(&ExampleWindowScript::InitArg2, this, std::placeholders::_1)); + AddInitArgs("AIOExample", "InitB"); + } + + private: + void HandlePrint(Player* sender, LuaVal const& args) + { + LuaVal btn = args.get(4); + LuaVal inp = args.get(5); + LuaVal val = args.get(6); + if (!btn.isstring() || !inp.isstring() || !val.isnumber()) + return; + + ChatHandler(sender->GetSession()).PSendSysMessage("HandlePrint -> Button Name: %s, Input: %s, Slider Value: %f", + btn.str().c_str(), inp.str().c_str(), val.num()); + + try + { + long size = std::stol(inp.str()); + if (size < 0 || size > 8192) + { + ChatHandler(sender->GetSession()).SendSysMessage("ExampleWindow: payload size must be between 0 and 8192."); + return; + } + std::string payload(size_t(size), 'b'); + Trinity::AIO::Handle(sender, "AIOExample", "StressTest", payload); + } + catch (...) + { + ChatHandler(sender->GetSession()).SendSysMessage("ExampleWindow: invalid payload size."); + } + } + + void HandleStressTest(Player* sender, LuaVal const& args) + { + LuaVal payload = args.get(4); + if (!payload.isstring()) + return; + + ChatHandler(sender->GetSession()).PSendSysMessage("Received StressTest block (%u bytes).", uint32(payload.str().size())); + } + + LuaVal InitArg(Player* /*sender*/) + { + return (counter++ % 2 == 0) ? LuaVal("Inited 1") : LuaVal("Inited 2"); + } + + LuaVal InitArg2(Player* /*sender*/) + { + return (counter % 2 != 0) ? LuaVal("Inited 1") : LuaVal("Inited 2"); + } + + size_t counter; +}; + +void AddSC_ExampleWindow() +{ + new ExampleWindowScript(); +} diff --git a/src/server/scripts/AIO/ExampleWindowStub.cpp b/src/server/scripts/AIO/ExampleWindowStub.cpp new file mode 100644 index 0000000000000..95e3dc4514e29 --- /dev/null +++ b/src/server/scripts/AIO/ExampleWindowStub.cpp @@ -0,0 +1,24 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#ifndef WITH_CAIO_EXAMPLES + +void AddSC_ExampleWindow() +{ +} + +#endif diff --git a/src/server/scripts/AIO/aio_script_loader.cpp b/src/server/scripts/AIO/aio_script_loader.cpp new file mode 100644 index 0000000000000..ab6e3114f2d9c --- /dev/null +++ b/src/server/scripts/AIO/aio_script_loader.cpp @@ -0,0 +1,24 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +// ExampleWindow.cpp is excluded from the AIO module when WITH_CAIO_EXAMPLES is off. +void AddSC_ExampleWindow(); + +void AddAIOScripts() +{ + AddSC_ExampleWindow(); +} diff --git a/src/server/scripts/CMakeLists.txt b/src/server/scripts/CMakeLists.txt index fb4ba169b4ce5..d620594c99165 100644 --- a/src/server/scripts/CMakeLists.txt +++ b/src/server/scripts/CMakeLists.txt @@ -152,6 +152,9 @@ foreach(SCRIPT_MODULE ${SCRIPT_MODULE_LIST}) # Add the module content to the whole static module unset(SCRIPT_MODULE_PRIVATE_SOURCES) CollectSourceFiles(${SCRIPT_MODULE_PATH} SCRIPT_MODULE_PRIVATE_SOURCES) + if(SCRIPT_MODULE STREQUAL "AIO" AND NOT WITH_CAIO_EXAMPLES) + list(FILTER SCRIPT_MODULE_PRIVATE_SOURCES EXCLUDE REGEX "[/\\\\]ExampleWindow\\.cpp$") + endif() # Configure the scriptloader ConfigureScriptLoader(${SCRIPT_MODULE} SCRIPT_MODULE_PRIVATE_SCRIPTLOADER ON ${SCRIPT_MODULE}) GetProjectNameOfScriptModule(${SCRIPT_MODULE} SCRIPT_MODULE_PROJECT_NAME) @@ -203,6 +206,10 @@ set(WORLDSERVER_DYNAMIC_SCRIPT_MODULES_DEPENDENCIES ${DYNAMIC_SCRIPT_MODULE_PROJ ConfigureScriptLoader("static" SCRIPT_MODULE_PRIVATE_SCRIPTLOADER OFF ${STATIC_SCRIPT_MODULES}) +if(NOT WITH_CAIO_EXAMPLES) + list(FILTER PRIVATE_SOURCES EXCLUDE REGEX "[/\\\\]AIO[/\\\\]ExampleWindow\\.cpp$") +endif() + add_library(scripts STATIC ScriptLoader.h ${SCRIPT_MODULE_PRIVATE_SCRIPTLOADER} @@ -218,6 +225,10 @@ target_include_directories(scripts PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +if(WITH_CAIO_EXAMPLES) + target_compile_definitions(scripts PRIVATE WITH_CAIO_EXAMPLES) +endif() + set_target_properties(scripts PROPERTIES COMPILE_WARNING_AS_ERROR ${WITH_WARNINGS_AS_ERRORS} @@ -225,8 +236,16 @@ set_target_properties(scripts # Generate precompiled header if(USE_SCRIPTPCH) + set(SCRIPT_PCH_REUSE_PROJECTS ${DYNAMIC_SCRIPT_MODULE_PROJECTS}) + if(WITH_CAIO_EXAMPLES) + foreach(script_project ${DYNAMIC_SCRIPT_MODULE_PROJECTS}) + target_compile_definitions(${script_project} PRIVATE WITH_CAIO_EXAMPLES) + endforeach() + GetProjectNameOfScriptModule("AIO" SCRIPT_AIO_PROJECT_NAME) + list(REMOVE_ITEM SCRIPT_PCH_REUSE_PROJECTS ${SCRIPT_AIO_PROJECT_NAME}) + endif() add_cxx_pch("scripts" ${PRIVATE_PCH_HEADER} ${DYNAMIC_SCRIPT_MODULE_PROJECTS}) - reuse_cxx_pch("${DYNAMIC_SCRIPT_MODULE_PROJECTS}" scripts) + reuse_cxx_pch("${SCRIPT_PCH_REUSE_PROJECTS}" scripts) endif() # Remove all shared libraries in the installl directory which diff --git a/src/server/scripts/Commands/cs_caio.cpp b/src/server/scripts/Commands/cs_caio.cpp new file mode 100644 index 0000000000000..d727c3d72b5b8 --- /dev/null +++ b/src/server/scripts/Commands/cs_caio.cpp @@ -0,0 +1,168 @@ +/* ScriptData +Name: caio_commandscript +%Complete : 100 +Comment : All AIO related server side commands +Category : commandscripts +EndScriptData */ + +#include "ScriptMgr.h" +#include "AIO.h" +#include "AIOUtil.h" +#include "Chat.h" +#include "ChatCommand.h" +#include "Language.h" +#include "Player.h" +#include "RBAC.h" +#include "World.h" +#include "smallfolk.h" + +using namespace Trinity::ChatCommands; + +class caio_commandscript : public CommandScript +{ +public: + caio_commandscript() : CommandScript("caio_commandscript") { } + + ChatCommandTable GetCommands() const override + { + static ChatCommandTable caioCommandTable = + { + { "version", HandleVersionCommand, rbac::RBAC_PERM_COMMAND_CAIO, Console::Yes }, + { "send", HandleSendCommand, rbac::RBAC_PERM_COMMAND_CAIO, Console::Yes }, + { "forcereload", HandleReloadCommand, rbac::RBAC_PERM_COMMAND_CAIO, Console::Yes }, + { "forcereset", HandleResetCommand, rbac::RBAC_PERM_COMMAND_CAIO, Console::Yes }, + { "sendall", HandleSendAllCommand, rbac::RBAC_PERM_COMMAND_CAIO, Console::Yes }, + { "forcereloadall", HandleReloadAllCommand, rbac::RBAC_PERM_COMMAND_CAIO, Console::Yes }, + { "forceresetall", HandleResetAllCommand, rbac::RBAC_PERM_COMMAND_CAIO, Console::Yes }, + { "reloadaddons", HandleReloadAddonsCommand, rbac::RBAC_PERM_COMMAND_CAIO, Console::Yes }, + { "addaddon", HandleAddAddonCommand, rbac::RBAC_PERM_COMMAND_CAIO, Console::Yes }, + { "removeaddon", HandleRemoveAddonCommand, rbac::RBAC_PERM_COMMAND_CAIO, Console::Yes }, + }; + + static ChatCommandTable commandTable = + { + { "caio", caioCommandTable }, + }; + + return commandTable; + } + + static bool HandleVersionCommand(ChatHandler* handler) + { + handler->PSendSysMessage("AIO version %s (protocol %.2f).", AIO_VERSION_STRING, AIO_VERSION); + return true; + } + + static bool HandleSendCommand(ChatHandler* handler, PlayerIdentifier const& target, QuotedString message) + { + Trinity::AIO::LoadMessageOutcome const outcome = Trinity::AIO::TryLoadIncomingMessage(message, sWorld->getIntConfig(CONFIG_AIO_MAX_INCOMING), sWorld->getIntConfig(CONFIG_AIO_MAX_BLOCKS)); + if (outcome.result != Trinity::AIO::LoadMessageResult::Ok) + { + handler->SendSysMessage("CAIO: message must be smallfolk-serialized table data (use AIOMsg or AIO.Msg on the server)."); + return false; + } + + Player* player = target.GetConnectedPlayer(); + if (!player) + return false; + + player->SendSimpleAIOMessage(message); + handler->PSendSysMessage("CAIO: sent %zu bytes to %s.", message.size(), target.GetName().c_str()); + return true; + } + + static bool HandleReloadCommand(ChatHandler* handler, PlayerIdentifier const& target) + { + Player* player = target.GetConnectedPlayer(); + if (!player) + return false; + + player->ForceReloadAddons(); + handler->PSendSysMessage(LANG_CAIO_FORCERELOAD_SENT, target.GetName().c_str()); + return true; + } + + static bool HandleResetCommand(ChatHandler* handler, PlayerIdentifier const& target) + { + Player* player = target.GetConnectedPlayer(); + if (!player) + return false; + + player->ForceResetAddons(); + handler->PSendSysMessage(LANG_CAIO_FORCERESET_SENT, target.GetName().c_str()); + return true; + } + + static bool HandleSendAllCommand(ChatHandler* handler, QuotedString message, Optional permission) + { + Trinity::AIO::LoadMessageOutcome const outcome = Trinity::AIO::TryLoadIncomingMessage(message, sWorld->getIntConfig(CONFIG_AIO_MAX_INCOMING), sWorld->getIntConfig(CONFIG_AIO_MAX_BLOCKS)); + if (outcome.result != Trinity::AIO::LoadMessageResult::Ok) + { + handler->SendSysMessage("CAIO: message must be smallfolk-serialized table data (use AIOMsg or AIO.Msg on the server)."); + return false; + } + + uint32 perm = permission.value_or(AIO_DEFAULT_ADDON_PERMISSION); + sWorld->SendAllSimpleAIOMessage(message, perm); + handler->PSendSysMessage("CAIO: sent %zu bytes to all players.", message.size()); + return true; + } + + static bool HandleReloadAllCommand(ChatHandler* handler, Optional permission) + { + sWorld->ForceReloadPlayerAddons(permission.value_or(AIO_DEFAULT_ADDON_PERMISSION)); + handler->PSendSysMessage(LANG_CAIO_FORCERELOAD_SENT, "all players"); + return true; + } + + static bool HandleResetAllCommand(ChatHandler* handler, Optional permission) + { + sWorld->ForceResetPlayerAddons(permission.value_or(AIO_DEFAULT_ADDON_PERMISSION)); + handler->PSendSysMessage(LANG_CAIO_FORCERESET_SENT, "all players"); + return true; + } + + static bool HandleReloadAddonsCommand(ChatHandler* handler) + { + if (sWorld->ReloadAddons()) + sWorld->ForceReloadPlayerAddons(); + else + handler->SendSysMessage(LANG_CAIO_RELOADADDONS_ERROR); + + return true; + } + + static bool HandleAddAddonCommand(ChatHandler* handler, std::string addonName, QuotedString addonFile, Optional permission) + { + if (!Trinity::AIO::IsSafeAddonRelativePath(addonFile)) + { + handler->SendSysMessage("CAIO: addon file path must be relative to AIO.ClientScriptPath and must not contain '..'."); + return false; + } + + uint32 perm = permission.value_or(AIO_DEFAULT_ADDON_PERMISSION); + World::AIOAddon newAddon(addonName, addonFile, perm); + if (sWorld->AddAddon(newAddon)) + sWorld->ForceReloadPlayerAddons(perm); + else + handler->PSendSysMessage(LANG_CAIO_ADDADDON_ERROR, addonName.c_str()); + + return true; + } + + static bool HandleRemoveAddonCommand(ChatHandler* handler, std::string addonName) + { + uint32 perm = AIO_DEFAULT_ADDON_PERMISSION; + if (sWorld->RemoveAddon(addonName, &perm)) + sWorld->ForceReloadPlayerAddons(perm); + else + handler->PSendSysMessage(LANG_CAIO_REMOVEADDON_ERROR, addonName.c_str()); + + return true; + } +}; + +void AddSC_caio_commandscript() +{ + new caio_commandscript(); +} diff --git a/src/server/scripts/Commands/cs_script_loader.cpp b/src/server/scripts/Commands/cs_script_loader.cpp index db22b83eeda0f..bc61eaf26f795 100644 --- a/src/server/scripts/Commands/cs_script_loader.cpp +++ b/src/server/scripts/Commands/cs_script_loader.cpp @@ -22,6 +22,7 @@ void AddSC_ahbot_commandscript(); void AddSC_arena_commandscript(); void AddSC_ban_commandscript(); void AddSC_bf_commandscript(); +void AddSC_caio_commandscript(); void AddSC_bg_commandscript(); void AddSC_cast_commandscript(); void AddSC_character_commandscript(); @@ -68,6 +69,7 @@ void AddCommandsScripts() AddSC_arena_commandscript(); AddSC_ban_commandscript(); AddSC_bf_commandscript(); + AddSC_caio_commandscript(); AddSC_bg_commandscript(); AddSC_cast_commandscript(); AddSC_character_commandscript(); diff --git a/src/server/worldserver/CMakeLists.txt b/src/server/worldserver/CMakeLists.txt index 3f6f76c2d5445..f15efb2edb004 100644 --- a/src/server/worldserver/CMakeLists.txt +++ b/src/server/worldserver/CMakeLists.txt @@ -88,6 +88,13 @@ elseif(WIN32) endif() endif() +if(COPY_CONF AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lua_client_scripts") + if(UNIX) + install(DIRECTORY lua_client_scripts DESTINATION bin) + elseif(WIN32) + install(DIRECTORY lua_client_scripts DESTINATION "${CMAKE_INSTALL_PREFIX}") + endif() +endif() # Generate precompiled header if(USE_COREPCH) add_cxx_pch(worldserver ${PRIVATE_PCH_HEADER}) diff --git a/src/server/worldserver/lua_client_scripts/ExampleWindow/ExampleWindow.lua b/src/server/worldserver/lua_client_scripts/ExampleWindow/ExampleWindow.lua new file mode 100644 index 0000000000000..78d281a07530f --- /dev/null +++ b/src/server/worldserver/lua_client_scripts/ExampleWindow/ExampleWindow.lua @@ -0,0 +1,26 @@ +-- Optional CAIO example client script (pairs with ExampleWindow.cpp when built with WITH_CAIO_EXAMPLES). +-- Requires AIO_Client in Interface/AddOns with matching AIO 1.75. + +local AIO = AIO or require("AIO") + +if not AIO.AddAddon then + return +end + +AIO.AddAddon("ExampleWindow") + +local function OnInit(initData) + -- Server may pass init args via AddInitArgs on script "AIOExample" / handler "Init". +end + +AIO.RegisterEvent("AIOExample", "Init", OnInit) + +AIO.RegisterEvent("AIOExample", "StressTest", function(data) + if type(data) == "string" then + print("ExampleWindow: StressTest received", #data, "bytes") + end +end) + +AIO.RegisterEvent("AIOExample", "Print", function(button, input, slider) + AIO.Msg():Add("AIOExample", "Print", button, input, slider):Send() +end) diff --git a/src/server/worldserver/worldserver.conf.dist b/src/server/worldserver/worldserver.conf.dist index 2df2e483d2ea6..2206398f5b3db 100644 --- a/src/server/worldserver/worldserver.conf.dist +++ b/src/server/worldserver/worldserver.conf.dist @@ -3952,6 +3952,7 @@ Appender.Console=1,3,0 Appender.Server=2,2,0,Server.log,w Appender.GM=2,2,1,GM.log Appender.DBErrors=2,2,0,DBErrors.log +Appender.AIO=2,3,3,AIO/AIO_%s.log # Logger config values: Given a logger "name" # Logger.name @@ -3977,6 +3978,7 @@ Logger.commands.gm=3,Console GM Logger.scripts.hotswap=3,Console Server Logger.sql.sql=5,Console DBErrors Logger.sql.updates=3,Console Server +Logger.AIO=3,Console AIO Logger.mmaps=3,Server #Logger.achievement=3,Console Server @@ -4096,6 +4098,78 @@ PacketSpoof.BanDuration = 86400 # ################################################################################################### +################################################################################################### +# +# AIO settings +# +# AIO.Prefix +# Prefix string used for AIO addon messages +# Default: "AIO" +# +# AIO.MaxParts +# Maximum number of parts a client can send to server +# Default: 64 (large init payloads may need many 255-byte parts) +# +# AIO.MsgMaxLen +# Maximum size (bytes) of a single addon whisper packet (entire payload including AIO headers). +# Must be 255 or less to match the WoW client addon message limit used by AIO.lua. +# Default: 255 +# +# AIO.InitCooldown +# Milliseconds before a player can call AIO Init again after a successful init. +# Default: 5000 +# +# AIO.MsgCacheTime +# Milliseconds before incomplete incoming long AIO messages are discarded. +# Matches AIO.lua AIO_MSG_CACHE_TIME on the stock server. +# Default: 15000 +# +# AIO.MsgCacheDelay +# Milliseconds between sweeps of incomplete long AIO message buffers. +# Matches AIO.lua AIO_MSG_CACHE_DELAY on the stock server. +# Default: 5000 +# +# AIO.MaxBufferSize +# Maximum total bytes buffered per long AIO message while reassembling parts. +# Default: 1048576 +# +# AIO.MaxIncomingMessageSize +# Maximum size (bytes) of a fully reassembled client-to-server AIO payload. +# Default: 262144 (cannot exceed AIO.MaxBufferSize) +# +# AIO.MsgRateLimitMs +# Minimum milliseconds between completed incoming AIO messages per session (0 = disabled). +# Default: 50 +# +# AIO.MaxBlocks +# Maximum number of handler blocks per incoming AIO message. +# Default: 32 +# +# AIO.MaxParseFailures +# Parse failures per session before reassembly buffers are cleared (0 = no threshold). +# Default: 16 +# +# AIO.ClientScriptPath +# Path to client lua scripts to send to the client +# Default: "lua_client_scripts" +# + +AIO.Prefix = "AIO" +AIO.MaxParts = 64 +AIO.MsgMaxLen = 255 +AIO.InitCooldown = 5000 +AIO.MsgCacheTime = 15000 +AIO.MsgCacheDelay = 5000 +AIO.MaxBufferSize = 1048576 +AIO.MaxIncomingMessageSize = 262144 +AIO.MsgRateLimitMs = 50 +AIO.MaxBlocks = 32 +AIO.MaxParseFailures = 16 +AIO.ClientScriptPath = "lua_client_scripts" + +# +################################################################################################### + ################################################################################################### # MISC ANTI-CHEAT SETTINGS # diff --git a/tests/game/AIOCodec.cpp b/tests/game/AIOCodec.cpp new file mode 100644 index 0000000000000..7584233e2c672 --- /dev/null +++ b/tests/game/AIOCodec.cpp @@ -0,0 +1,52 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#include "tc_catch2.h" + +#include "AIOCodec.h" + +using namespace Trinity::AIO::Codec; + +TEST_CASE("AIO codec byte decoding", "[AIO]") +{ + REQUIRE(DecodeByte('\x01') == 0); + REQUIRE(DecodeByte('\xFF') == 254); + REQUIRE(DecodePair('\x01', '\x01') == 0); + REQUIRE(DecodePair('\x02', '\x02') == 255); +} + +TEST_CASE("AIO client prefix detection", "[AIO]") +{ + SECTION("matches configured wire prefix") + { + size_t delimPos = 0; + REQUIRE(IsClientPrefix("CAIO", "CAIO\tpayload", delimPos)); + REQUIRE(delimPos == 4); + } + + SECTION("rejects server prefix") + { + size_t delimPos = 0; + REQUIRE_FALSE(IsClientPrefix("CAIO", "SAIO\tpayload", delimPos)); + } + + SECTION("rejects missing tab") + { + size_t delimPos = 0; + REQUIRE_FALSE(IsClientPrefix("AIO", "CAIOpayload", delimPos)); + } +} diff --git a/tests/game/AIOUtil.cpp b/tests/game/AIOUtil.cpp new file mode 100644 index 0000000000000..921c308e71676 --- /dev/null +++ b/tests/game/AIOUtil.cpp @@ -0,0 +1,70 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#include "tc_catch2.h" + +#include "AIOUtil.h" +#include "AIOMsg.h" + +TEST_CASE("AIO addon path validation", "[AIO]") +{ + REQUIRE(Trinity::AIO::IsSafeAddonRelativePath("ExampleWindow/ExampleWindow.lua")); + REQUIRE_FALSE(Trinity::AIO::IsSafeAddonRelativePath("../secret.lua")); + REQUIRE_FALSE(Trinity::AIO::IsSafeAddonRelativePath("/etc/passwd")); + REQUIRE_FALSE(Trinity::AIO::IsSafeAddonRelativePath("")); +} + +TEST_CASE("AIO incoming message load", "[AIO]") +{ + AIOMsg msg("TestScript", "Handler", LuaVal("arg")); + std::string const serialized = msg.dumps(); + + SECTION("accepts valid table") + { + Trinity::AIO::LoadMessageOutcome const outcome = Trinity::AIO::TryLoadIncomingMessage(serialized, 4096, 8); + REQUIRE(outcome.result == Trinity::AIO::LoadMessageResult::Ok); + REQUIRE(outcome.table.istable()); + REQUIRE(outcome.table.len() == 1); + } + + SECTION("rejects oversize payload") + { + Trinity::AIO::LoadMessageOutcome const outcome = Trinity::AIO::TryLoadIncomingMessage(serialized, 4, 8); + REQUIRE(outcome.result == Trinity::AIO::LoadMessageResult::Oversize); + } + + SECTION("rejects invalid smallfolk") + { + Trinity::AIO::LoadMessageOutcome const outcome = Trinity::AIO::TryLoadIncomingMessage("not smallfolk", 4096, 8); + REQUIRE(outcome.result == Trinity::AIO::LoadMessageResult::ParseError); + } +} + +TEST_CASE("AIOMsg round trip", "[AIO]") +{ + AIOMsg msg; + msg.Add("ScriptA", "HandlerA", LuaVal(1)); + msg.Add("ScriptB", "HandlerB", LuaVal("x")); + + Trinity::AIO::LoadMessageOutcome const outcome = Trinity::AIO::TryLoadIncomingMessage(msg.dumps(), 4096, 8); + REQUIRE(outcome.result == Trinity::AIO::LoadMessageResult::Ok); + REQUIRE(outcome.table.len() == 2); + + LuaVal block = outcome.table.get(1); + REQUIRE(block.get(2).tostring() == "ScriptA"); + REQUIRE(block.get(3).tostring() == "HandlerA"); +}