diff --git a/back/.eslintrc.json b/back/.eslintrc.json index 0cee14a3..3aab37d9 100644 --- a/back/.eslintrc.json +++ b/back/.eslintrc.json @@ -7,7 +7,8 @@ }, "extends": [ "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended" + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking" ], "globals": { "Atomics": "readonly", @@ -16,12 +17,14 @@ "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 2018, - "sourceType": "module" + "sourceType": "module", + "project": "./tsconfig.json" }, "plugins": [ "@typescript-eslint" ], "rules": { - "no-unused-vars": "off" + "no-unused-vars": "off", + "@typescript-eslint/no-explicit-any": "error" } -} \ No newline at end of file +} diff --git a/back/LICENSE.txt b/back/LICENSE.txt new file mode 100644 index 00000000..614e6268 --- /dev/null +++ b/back/LICENSE.txt @@ -0,0 +1,691 @@ +NOTICE +This package contains software licensed under different +licenses, please refer to the NOTICE.txt file for further +information and LICENSES.txt for full license texts. + +WorkAdventure Enterprise edition can be licensed independently from +the source under separate commercial terms. + +The software ("Software") is developed and owned by TheCodingMachine +and is subject to the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, with the Commons Clause as follows: + + + + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license +for software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are +designed to take away your freedom to share and change the works. By +contrast, our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is +combined will remain governed by version 3 of the GNU General Public +License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero +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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. + + +"Commons Clause" License Condition + +The Software is provided to you by the Licensor under the License, as +defined below, subject to the following condition. Without limiting +other conditions in the License, the grant of rights under the License +will not include, and the License does not grant to you, the right to +Sell the Software. For purposes of the foregoing, "Sell" means +practicing any or all of the rights granted to you under the License +to provide to third parties, for a fee or other consideration, +a product or service that consists, entirely or substantially, +of the Software or the functionality of the Software. Any license +notice or attribution required by the License must also include +this Commons Cause License Condition notice. diff --git a/back/package.json b/back/package.json index ca649707..a20c876f 100644 --- a/back/package.json +++ b/back/package.json @@ -16,8 +16,21 @@ "type": "git", "url": "git+https://github.com/thecodingmachine/workadventure.git" }, - "author": "g.parant@thecodingmachine.com", - "license": "AGPL", + "contributors": [ + { + "name": "Grégoire Parant", + "email": "g.parant@thecodingmachine.com" + }, + { + "name": "David Négrier", + "email": "d.negrier@thecodingmachine.com" + }, + { + "name": "Arthmaël Poly", + "email": "a.poly@thecodingmachine.com" + } + ], + "license": "SEE LICENSE IN LICENSE.txt", "bugs": { "url": "https://github.com/thecodingmachine/workadventure/issues" }, @@ -30,8 +43,10 @@ "@types/uuidv4": "^5.0.0", "body-parser": "^1.19.0", "express": "^4.17.1", + "generic-type-guard": "^3.2.0", "http-status-codes": "^1.4.0", "jsonwebtoken": "^8.5.1", + "prom-client": "^12.0.0", "socket.io": "^2.3.0", "systeminformation": "^4.26.5", "ts-node-dev": "^1.0.0-pre.44", diff --git a/back/src/App.ts b/back/src/App.ts index 06e08ca6..e12afdb4 100644 --- a/back/src/App.ts +++ b/back/src/App.ts @@ -6,6 +6,7 @@ import {Application, Request, Response} from 'express'; import bodyParser = require('body-parser'); import * as http from "http"; import {MapController} from "./Controller/MapController"; +import {PrometheusController} from "./Controller/PrometheusController"; class App { public app: Application; @@ -13,6 +14,7 @@ class App { public ioSocketController: IoSocketController; public authenticateController: AuthenticateController; public mapController: MapController; + public prometheusController: PrometheusController; constructor() { this.app = express(); @@ -29,6 +31,7 @@ class App { this.ioSocketController = new IoSocketController(this.server); this.authenticateController = new AuthenticateController(this.app); this.mapController = new MapController(this.app); + this.prometheusController = new PrometheusController(this.app, this.ioSocketController); } // TODO add session user @@ -49,4 +52,4 @@ class App { } } -export default new App().server; \ No newline at end of file +export default new App().server; diff --git a/back/src/Assets/Maps/Floor0/floor0.json b/back/src/Assets/Maps/Floor0/floor0.json index f8f059d4..987004e6 100644 --- a/back/src/Assets/Maps/Floor0/floor0.json +++ b/back/src/Assets/Maps/Floor0/floor0.json @@ -16,7 +16,7 @@ "infinite":false, "layers":[ { - "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 0, 0, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 0, 0, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 309, 309, 309, 309, 309, 309, 309, 309, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":34, "id":11, "name":"start", diff --git a/back/src/Assets/Maps/Floor1/floor1.json b/back/src/Assets/Maps/Floor1/floor1.json index ce93459c..3ba25159 100644 --- a/back/src/Assets/Maps/Floor1/floor1.json +++ b/back/src/Assets/Maps/Floor1/floor1.json @@ -10,7 +10,7 @@ "infinite":false, "layers":[ { - "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4541, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4541, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4541, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":18, "id":12, "name":"start", diff --git a/back/src/Controller/AuthenticateController.ts b/back/src/Controller/AuthenticateController.ts index 4e617509..71e538a4 100644 --- a/back/src/Controller/AuthenticateController.ts +++ b/back/src/Controller/AuthenticateController.ts @@ -4,6 +4,11 @@ import {BAD_REQUEST, OK} from "http-status-codes"; import {SECRET_KEY, URL_ROOM_STARTED} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." import { uuid } from 'uuidv4'; +export interface TokenInterface { + name: string, + userId: string +} + export class AuthenticateController { App : Application; @@ -16,15 +21,15 @@ export class AuthenticateController { login(){ // For now, let's completely forget the /login route. this.App.post("/login", (req: Request, res: Response) => { - let param = req.body; + const param = req.body; /*if(!param.name){ return res.status(BAD_REQUEST).send({ message: "email parameter is empty" }); }*/ //TODO check user email for The Coding Machine game - let userId = uuid(); - let token = Jwt.sign({name: param.name, userId: userId}, SECRET_KEY, {expiresIn: '24h'}); + const userId = uuid(); + const token = Jwt.sign({name: param.name, userId: userId} as TokenInterface, SECRET_KEY, {expiresIn: '24h'}); return res.status(OK).send({ token: token, mapUrlStart: URL_ROOM_STARTED, diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index f30663f8..edda6de9 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -8,10 +8,17 @@ import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVar import {World} from "../Model/World"; import {Group} from "_Model/Group"; import {UserInterface} from "_Model/UserInterface"; -import {SetPlayerDetailsMessage} from "_Model/Websocket/SetPlayerDetailsMessage"; +import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage"; import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined"; import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved"; import si from "systeminformation"; +import {Gauge} from "prom-client"; +import os from 'os'; +import {TokenInterface} from "../Controller/AuthenticateController"; +import {isJoinRoomMessageInterface} from "../Model/Websocket/JoinRoomMessage"; +import {isPointInterface, PointInterface} from "../Model/Websocket/PointInterface"; +import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage"; +import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface"; enum SockerIoEvent { CONNECTION = "connection", @@ -21,7 +28,6 @@ enum SockerIoEvent { USER_MOVED = "user-moved", // From server to client USER_LEFT = "user-left", // From server to client WEBRTC_SIGNAL = "webrtc-signal", - WEBRTC_OFFER = "webrtc-offer", WEBRTC_START = "webrtc-start", WEBRTC_DISCONNECT = "webrtc-disconect", MESSAGE_ERROR = "message-error", @@ -31,27 +37,47 @@ enum SockerIoEvent { } export class IoSocketController { - Io: socketIO.Server; - Worlds: Map = new Map(); - sockets: Map = new Map(); + public readonly Io: socketIO.Server; + private Worlds: Map = new Map(); + private sockets: Map = new Map(); + private nbClientsGauge: Gauge; + private nbClientsPerRoomGauge: Gauge; constructor(server: http.Server) { this.Io = socketIO(server); + this.nbClientsGauge = new Gauge({ + name: 'workadventure_nb_sockets', + help: 'Number of connected sockets', + labelNames: [ 'host' ] + }); + this.nbClientsPerRoomGauge = new Gauge({ + name: 'workadventure_nb_clients_per_room', + help: 'Number of clients per room', + labelNames: [ 'host', 'room' ] + }); // Authentication with token. it will be decoded and stored in the socket. // Completely commented for now, as we do not use the "/login" route at all. this.Io.use((socket: Socket, next) => { if (!socket.handshake.query || !socket.handshake.query.token) { + console.error('An authentication error happened, a user tried to connect without a token.'); return next(new Error('Authentication error')); } if(this.searchClientByToken(socket.handshake.query.token)){ + console.error('An authentication error happened, a user tried to connect while its token is already connected.'); return next(new Error('Authentication error')); } - Jwt.verify(socket.handshake.query.token, SECRET_KEY, (err: JsonWebTokenError, tokenDecoded: any) => { + Jwt.verify(socket.handshake.query.token, SECRET_KEY, (err: JsonWebTokenError, tokenDecoded: object) => { if (err) { + console.error('An authentication error happened, invalid JsonWebToken.', err); return next(new Error('Authentication error')); } - (socket as ExSocketInterface).token = tokenDecoded; + + if (!this.isValidToken(tokenDecoded)) { + return next(new Error('Authentication error, invalid token structure')); + } + + (socket as ExSocketInterface).token = socket.handshake.query.token; (socket as ExSocketInterface).userId = tokenDecoded.userId; next(); }); @@ -60,14 +86,24 @@ export class IoSocketController { this.ioConnection(); } + private isValidToken(token: object): token is TokenInterface { + if (typeof((token as TokenInterface).userId) !== 'string') { + return false; + } + if (typeof((token as TokenInterface).name) !== 'string') { + return false; + } + return true; + } + /** * * @param token */ searchClientByToken(token: string): ExSocketInterface | null { - let clients: Array = Object.values(this.Io.sockets.sockets); + const clients: ExSocketInterface[] = Object.values(this.Io.sockets.sockets) as ExSocketInterface[]; for (let i = 0; i < clients.length; i++) { - let client: ExSocketInterface = clients[i]; + const client = clients[i]; if (client.token !== token) { continue } @@ -79,9 +115,9 @@ export class IoSocketController { private sendUpdateGroupEvent(group: Group): void { // Let's get the room of the group. To do this, let's get anyone in the group and find its room. // Note: this is suboptimal - let userId = group.getUsers()[0].id; - let client: ExSocketInterface = this.searchClientByIdOrFail(userId); - let roomId = client.roomId; + const userId = group.getUsers()[0].id; + const client: ExSocketInterface = this.searchClientByIdOrFail(userId); + const roomId = client.roomId; this.Io.in(roomId).emit(SockerIoEvent.GROUP_CREATE_UPDATE, { position: group.getPosition(), groupId: group.getId() @@ -90,22 +126,23 @@ export class IoSocketController { private sendDeleteGroupEvent(uuid: string, lastUser: UserInterface): void { // Let's get the room of the group. To do this, let's get anyone in the group and find its room. - let userId = lastUser.id; - let client: ExSocketInterface = this.searchClientByIdOrFail(userId); - let roomId = client.roomId; + const userId = lastUser.id; + const client: ExSocketInterface = this.searchClientByIdOrFail(userId); + const roomId = client.roomId; this.Io.in(roomId).emit(SockerIoEvent.GROUP_DELETE, uuid); } ioConnection() { this.Io.on(SockerIoEvent.CONNECTION, (socket: Socket) => { - let client : ExSocketInterface = socket as ExSocketInterface; + const client : ExSocketInterface = socket as ExSocketInterface; this.sockets.set(client.userId, client); // Let's log server load when a user joins - let srvSockets = this.Io.sockets.sockets; - console.log('A user joined (', Object.keys(srvSockets).length, ' connected users)'); - si.currentLoad().then(data => console.log('Current load: ', data.avgload)); - si.currentLoad().then(data => console.log('CPU: ', data.currentload, '%')); + const srvSockets = this.Io.sockets.sockets; + this.nbClientsGauge.inc({ host: os.hostname() }); + console.log(new Date().toISOString() + ' A user joined (', Object.keys(srvSockets).length, ' connected users)'); + si.currentLoad().then(data => console.log(' Current load: ', data.avgload)); + si.currentLoad().then(data => console.log(' CPU: ', data.currentload, '%')); // End log server load /*join-rom event permit to join one room. @@ -116,21 +153,16 @@ export class IoSocketController { x: user x position on map y: user y position on map */ - socket.on(SockerIoEvent.JOIN_ROOM, (message: any, answerFn): void => { + socket.on(SockerIoEvent.JOIN_ROOM, (message: unknown, answerFn): void => { try { - let roomId = message.roomId; - - if (typeof(roomId) !== 'string') { - socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Expected roomId as a string.'}); - return; - } - let position = this.hydratePositionReceive(message.position); - if (position instanceof Error) { - socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: position.message}); + if (!isJoinRoomMessageInterface(message)) { + socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid JOIN_ROOM message.'}); + console.warn('Invalid JOIN_ROOM message received: ', message); return; } + const roomId = message.roomId; - let Client = (socket as ExSocketInterface); + const Client = (socket as ExSocketInterface); if (Client.roomId === roomId) { return; @@ -140,18 +172,18 @@ export class IoSocketController { this.leaveRoom(Client); //join new previous room - let world = this.joinRoom(Client, roomId, position); + const world = this.joinRoom(Client, roomId, message.position); //add function to refresh position user in real time. //this.refreshUserPosition(Client); - let messageUserJoined = new MessageUserJoined(Client.userId, Client.name, Client.character, Client.position); + const messageUserJoined = new MessageUserJoined(Client.userId, Client.name, Client.character, Client.position); socket.to(roomId).emit(SockerIoEvent.JOIN_ROOM, messageUserJoined); // The answer shall contain the list of all users of the room with their positions: - let listOfUsers = Array.from(world.getUsers(), ([key, user]) => { - let player = this.searchClientByIdOrFail(user.id); + const listOfUsers = Array.from(world.getUsers(), ([key, user]) => { + const player = this.searchClientByIdOrFail(user.id); return new MessageUserPosition(user.id, player.name, player.character, player.position); }); answerFn(listOfUsers); @@ -161,21 +193,21 @@ export class IoSocketController { } }); - socket.on(SockerIoEvent.USER_POSITION, (message: any): void => { + socket.on(SockerIoEvent.USER_POSITION, (position: unknown): void => { try { - let position = this.hydratePositionReceive(message); - if (position instanceof Error) { - socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: position.message}); + if (!isPointInterface(position)) { + socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid USER_POSITION message.'}); + console.warn('Invalid USER_POSITION message received: ', position); return; } - let Client = (socket as ExSocketInterface); + const Client = (socket as ExSocketInterface); // sending to all clients in room except sender Client.position = position; // update position in the world - let world = this.Worlds.get(Client.roomId); + const world = this.Worlds.get(Client.roomId); if (!world) { console.error("Could not find world with id '", Client.roomId, "'"); return; @@ -189,9 +221,14 @@ export class IoSocketController { } }); - socket.on(SockerIoEvent.WEBRTC_SIGNAL, (data: any) => { + socket.on(SockerIoEvent.WEBRTC_SIGNAL, (data: unknown) => { + if (!isWebRtcSignalMessageInterface(data)) { + socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid WEBRTC_SIGNAL message.'}); + console.warn('Invalid WEBRTC_SIGNAL message received: ', data); + return; + } //send only at user - let client = this.sockets.get(data.receiverId); + const client = this.sockets.get(data.receiverId); if (client === undefined) { console.warn("While exchanging a WebRTC signal: client with id ", data.receiverId, " does not exist. This might be a race condition."); return; @@ -199,18 +236,8 @@ export class IoSocketController { return client.emit(SockerIoEvent.WEBRTC_SIGNAL, data); }); - socket.on(SockerIoEvent.WEBRTC_OFFER, (data: any) => { - //send only at user - let client = this.sockets.get(data.receiverId); - if (client === undefined) { - console.warn("While exchanging a WebRTC offer: client with id ", data.receiverId, " does not exist. This might be a race condition."); - return; - } - client.emit(SockerIoEvent.WEBRTC_OFFER, data); - }); - socket.on(SockerIoEvent.DISCONNECT, () => { - let Client = (socket as ExSocketInterface); + const Client = (socket as ExSocketInterface); try { //leave room this.leaveRoom(Client); @@ -230,7 +257,8 @@ export class IoSocketController { this.sockets.delete(Client.userId); // Let's log server load when a user leaves - let srvSockets = this.Io.sockets.sockets; + const srvSockets = this.Io.sockets.sockets; + this.nbClientsGauge.dec({ host: os.hostname() }); console.log('A user left (', Object.keys(srvSockets).length, ' connected users)'); si.currentLoad().then(data => console.log('Current load: ', data.avgload)); si.currentLoad().then(data => console.log('CPU: ', data.currentload, '%')); @@ -238,8 +266,13 @@ export class IoSocketController { }); // Let's send the user id to the user - socket.on(SockerIoEvent.SET_PLAYER_DETAILS, (playerDetails: SetPlayerDetailsMessage, answerFn) => { - let Client = (socket as ExSocketInterface); + socket.on(SockerIoEvent.SET_PLAYER_DETAILS, (playerDetails: unknown, answerFn) => { + if (!isSetPlayerDetailsMessage(playerDetails)) { + socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_PLAYER_DETAILS message.'}); + console.warn('Invalid SET_PLAYER_DETAILS message received: ', playerDetails); + return; + } + const Client = (socket as ExSocketInterface); Client.name = playerDetails.name; Client.character = playerDetails.character; answerFn(Client.userId); @@ -248,7 +281,7 @@ export class IoSocketController { } searchClientByIdOrFail(userId: string): ExSocketInterface { - let client: ExSocketInterface|undefined = this.sockets.get(userId); + const client: ExSocketInterface|undefined = this.sockets.get(userId); if (client === undefined) { throw new Error("Could not find user with id " + userId); } @@ -261,19 +294,21 @@ export class IoSocketController { Client.to(Client.roomId).emit(SockerIoEvent.USER_LEFT, Client.userId); //user leave previous world - let world : World|undefined = this.Worlds.get(Client.roomId); + const world : World|undefined = this.Worlds.get(Client.roomId); if(world){ world.leave(Client); } //user leave previous room Client.leave(Client.roomId); + this.nbClientsPerRoomGauge.dec({ host: os.hostname(), room: Client.roomId }); delete Client.roomId; } } - private joinRoom(Client : ExSocketInterface, roomId: string, position: Point): World { + private joinRoom(Client : ExSocketInterface, roomId: string, position: PointInterface): World { //join user in room Client.join(roomId); + this.nbClientsPerRoomGauge.inc({ host: os.hostname(), room: roomId }); Client.roomId = roomId; Client.position = position; @@ -319,13 +354,13 @@ export class IoSocketController { if (this.Io.sockets.adapter.rooms[roomId].length < 2 /*|| this.Io.sockets.adapter.rooms[roomId].length >= 4*/) { return; } - let clients: Array = (Object.values(this.Io.sockets.sockets) as Array) + const clients: Array = (Object.values(this.Io.sockets.sockets) as Array) .filter((client: ExSocketInterface) => client.webRtcRoomId && client.webRtcRoomId === roomId); //send start at one client to initialise offer webrtc //send all users in room to create PeerConnection in front clients.forEach((client: ExSocketInterface, index: number) => { - let clientsId = clients.reduce((tabs: Array, clientId: ExSocketInterface, indexClientId: number) => { + const clientsId = clients.reduce((tabs: Array, clientId: ExSocketInterface, indexClientId: number) => { if (!clientId.userId || clientId.userId === client.userId) { return tabs; } @@ -341,19 +376,6 @@ export class IoSocketController { }); } - //Hydrate and manage error - hydratePositionReceive(message: any): Point | Error { - try { - if (!message.x || !message.y || !message.direction || message.moving === undefined) { - return new Error("invalid point message sent"); - } - return new Point(message.x, message.y, message.direction, message.moving); - } catch (err) { - //TODO log error - return new Error(err); - } - } - /** permit to share user position ** users position will send in event 'user-position' ** The data sent is an array with information for each user : @@ -377,13 +399,13 @@ export class IoSocketController { if (Client === undefined) { return; }*/ - let Client = this.searchClientByIdOrFail(userId); + const Client = this.searchClientByIdOrFail(userId); this.joinWebRtcRoom(Client, group.getId()); } //disconnect user disConnectedUser(userId: string, group: Group) { - let Client = this.searchClientByIdOrFail(userId); + const Client = this.searchClientByIdOrFail(userId); Client.to(group.getId()).emit(SockerIoEvent.WEBRTC_DISCONNECT, { userId: userId }); @@ -393,7 +415,7 @@ export class IoSocketController { // However! In the rare case where the WebRTC connection is not yet established, if we close the connection on one of the player, // the other player will try connecting until a timeout happens (during this time, the connection icon will be displayed for nothing). // So we also send the disconnect event to the other player. - for (let user of group.getUsers()) { + for (const user of group.getUsers()) { Client.emit(SockerIoEvent.WEBRTC_DISCONNECT, { userId: user.id }); diff --git a/back/src/Controller/MapController.ts b/back/src/Controller/MapController.ts index 68243df5..e3730898 100644 --- a/back/src/Controller/MapController.ts +++ b/back/src/Controller/MapController.ts @@ -19,7 +19,7 @@ export class MapController { // Returns a map mapping map name to file name of the map getStartMap() { this.App.get("/start-map", (req: Request, res: Response) => { - return res.status(OK).send({ + res.status(OK).send({ mapUrlStart: req.headers.host + "/map/files" + URL_ROOM_STARTED, startInstance: "global" }); diff --git a/back/src/Controller/PrometheusController.ts b/back/src/Controller/PrometheusController.ts new file mode 100644 index 00000000..0a0db2bb --- /dev/null +++ b/back/src/Controller/PrometheusController.ts @@ -0,0 +1,20 @@ +import {Application, Request, Response} from "express"; +import {IoSocketController} from "_Controller/IoSocketController"; +const register = require('prom-client').register; +const collectDefaultMetrics = require('prom-client').collectDefaultMetrics; + +export class PrometheusController { + constructor(private App: Application, private ioSocketController: IoSocketController) { + collectDefaultMetrics({ + timeout: 10000, + gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets. + }); + + this.App.get("/metrics", this.metrics.bind(this)); + } + + private metrics(req: Request, res: Response): void { + res.set('Content-Type', register.contentType); + res.end(register.metrics()); + } +} diff --git a/back/src/Model/Group.ts b/back/src/Model/Group.ts index d71a0585..ed09b0cd 100644 --- a/back/src/Model/Group.ts +++ b/back/src/Model/Group.ts @@ -68,7 +68,7 @@ export class Group { isPartOfGroup(user: UserInterface): boolean { - return this.users.indexOf(user) !== -1; + return this.users.includes(user); } /*removeFromGroup(users: UserInterface[]): void diff --git a/back/src/Model/Websocket/ExSocketInterface.ts b/back/src/Model/Websocket/ExSocketInterface.ts index df72321f..5827ccc9 100644 --- a/back/src/Model/Websocket/ExSocketInterface.ts +++ b/back/src/Model/Websocket/ExSocketInterface.ts @@ -1,9 +1,10 @@ import {Socket} from "socket.io"; import {PointInterface} from "./PointInterface"; import {Identificable} from "./Identificable"; +import {TokenInterface} from "../../Controller/AuthenticateController"; export interface ExSocketInterface extends Socket, Identificable { - token: any; + token: string; roomId: string; webRtcRoomId: string; userId: string; diff --git a/back/src/Model/Websocket/JoinRoomMessage.ts b/back/src/Model/Websocket/JoinRoomMessage.ts new file mode 100644 index 00000000..16613488 --- /dev/null +++ b/back/src/Model/Websocket/JoinRoomMessage.ts @@ -0,0 +1,9 @@ +import * as tg from "generic-type-guard"; +import {isPointInterface} from "./PointInterface"; + +export const isJoinRoomMessageInterface = + new tg.IsInterface().withProperties({ + roomId: tg.isString, + position: isPointInterface, + }).get(); +export type JoinRoomMessageInterface = tg.GuardedType; diff --git a/back/src/Model/Websocket/MessageUserJoined.ts b/back/src/Model/Websocket/MessageUserJoined.ts index fff9db5d..d3143a6b 100644 --- a/back/src/Model/Websocket/MessageUserJoined.ts +++ b/back/src/Model/Websocket/MessageUserJoined.ts @@ -1,4 +1,3 @@ -import {Point} from "./MessageUserPosition"; import {PointInterface} from "_Model/Websocket/PointInterface"; export class MessageUserJoined { diff --git a/back/src/Model/Websocket/PointInterface.ts b/back/src/Model/Websocket/PointInterface.ts index 61b02339..afb07a23 100644 --- a/back/src/Model/Websocket/PointInterface.ts +++ b/back/src/Model/Websocket/PointInterface.ts @@ -1,5 +1,17 @@ -export interface PointInterface { +import * as tg from "generic-type-guard"; + +/*export interface PointInterface { readonly x: number; readonly y: number; readonly direction: string; -} + readonly moving: boolean; +}*/ + +export const isPointInterface = + new tg.IsInterface().withProperties({ + x: tg.isNumber, + y: tg.isNumber, + direction: tg.isString, + moving: tg.isBoolean + }).get(); +export type PointInterface = tg.GuardedType; diff --git a/back/src/Model/Websocket/SetPlayerDetailsMessage.ts b/back/src/Model/Websocket/SetPlayerDetailsMessage.ts index 2f3cc707..21461812 100644 --- a/back/src/Model/Websocket/SetPlayerDetailsMessage.ts +++ b/back/src/Model/Websocket/SetPlayerDetailsMessage.ts @@ -1,4 +1,8 @@ -export interface SetPlayerDetailsMessage { - name: string, - character: string -} +import * as tg from "generic-type-guard"; + +export const isSetPlayerDetailsMessage = + new tg.IsInterface().withProperties({ + name: tg.isString, + character: tg.isString + }).get(); +export type SetPlayerDetailsMessage = tg.GuardedType; diff --git a/back/src/Model/Websocket/UserInGroupInterface.ts b/back/src/Model/Websocket/UserInGroupInterface.ts new file mode 100644 index 00000000..26cc5fd4 --- /dev/null +++ b/back/src/Model/Websocket/UserInGroupInterface.ts @@ -0,0 +1,5 @@ +export interface UserInGroupInterface { + userId: string, + name: string, + initiator: boolean +} diff --git a/back/src/Model/Websocket/WebRtcSignalMessage.ts b/back/src/Model/Websocket/WebRtcSignalMessage.ts new file mode 100644 index 00000000..7edffdfa --- /dev/null +++ b/back/src/Model/Websocket/WebRtcSignalMessage.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isWebRtcSignalMessageInterface = + new tg.IsInterface().withProperties({ + userId: tg.isString, + receiverId: tg.isString, + roomId: tg.isString, + signal: tg.isUnknown + }).get(); +export type WebRtcSignalMessageInterface = tg.GuardedType; diff --git a/back/src/Model/World.ts b/back/src/Model/World.ts index 5f70a32f..51129857 100644 --- a/back/src/Model/World.ts +++ b/back/src/Model/World.ts @@ -62,7 +62,7 @@ export class World { } public leave(user : Identificable){ - let userObj = this.users.get(user.userId); + const userObj = this.users.get(user.userId); if (userObj === undefined) { console.warn('User ', user.userId, 'does not belong to world! It should!'); } @@ -73,7 +73,7 @@ export class World { } public updatePosition(socket : Identificable, userPosition: PointInterface): void { - let user = this.users.get(socket.userId); + const user = this.users.get(socket.userId); if(typeof user === 'undefined') { return; } @@ -83,15 +83,15 @@ export class World { if (typeof user.group === 'undefined') { // If the user is not part of a group: // should he join a group? - let closestItem: UserInterface|Group|null = this.searchClosestAvailableUserOrGroup(user); + const closestItem: UserInterface|Group|null = this.searchClosestAvailableUserOrGroup(user); if (closestItem !== null) { if (closestItem instanceof Group) { // Let's join the group! closestItem.join(user); } else { - let closestUser : UserInterface = closestItem; - let group: Group = new Group([ + const closestUser : UserInterface = closestItem; + const group: Group = new Group([ user, closestUser ], this.connectCallback, this.disconnectCallback); @@ -102,7 +102,7 @@ export class World { } else { // If the user is part of a group: // should he leave the group? - let distance = World.computeDistanceBetweenPositions(user.position, user.group.getPosition()); + const distance = World.computeDistanceBetweenPositions(user.position, user.group.getPosition()); if (distance > this.groupRadius) { this.leaveGroup(user); } @@ -120,7 +120,7 @@ export class World { * @param user */ private leaveGroup(user: UserInterface): void { - let group = user.group; + const group = user.group; if (typeof group === 'undefined') { throw new Error("The user is part of no group"); } @@ -158,7 +158,7 @@ export class World { return; } - let distance = World.computeDistance(user, currentUser); // compute distance between peers. + const distance = World.computeDistance(user, currentUser); // compute distance between peers. if(distance <= minimumDistanceFound && distance <= this.minDistance) { minimumDistanceFound = distance; @@ -204,7 +204,7 @@ export class World { if (group.isFull()) { return; } - let distance = World.computeDistanceBetweenPositions(user.position, group.getPosition()); + const distance = World.computeDistanceBetweenPositions(user.position, group.getPosition()); if(distance <= minimumDistanceFound && distance <= this.groupRadius) { minimumDistanceFound = distance; matchingItem = group; diff --git a/back/tests/WorldTest.ts b/back/tests/WorldTest.ts index 88c87430..c436eed7 100644 --- a/back/tests/WorldTest.ts +++ b/back/tests/WorldTest.ts @@ -6,14 +6,14 @@ import { Group } from "../src/Model/Group"; describe("World", () => { it("should connect user1 and user2", () => { let connectCalledNumber: number = 0; - let connect: ConnectCallback = (user: string, group: Group): void => { + const connect: ConnectCallback = (user: string, group: Group): void => { connectCalledNumber++; } - let disconnect: DisconnectCallback = (user: string, group: Group): void => { + const disconnect: DisconnectCallback = (user: string, group: Group): void => { } - let world = new World(connect, disconnect, 160, 160, () => {}, () => {}); + const world = new World(connect, disconnect, 160, 160, () => {}, () => {}); world.join({ userId: "foo" }, new Point(100, 100)); @@ -33,14 +33,14 @@ describe("World", () => { it("should connect 3 users", () => { let connectCalled: boolean = false; - let connect: ConnectCallback = (user: string, group: Group): void => { + const connect: ConnectCallback = (user: string, group: Group): void => { connectCalled = true; } - let disconnect: DisconnectCallback = (user: string, group: Group): void => { + const disconnect: DisconnectCallback = (user: string, group: Group): void => { } - let world = new World(connect, disconnect, 160, 160, () => {}, () => {}); + const world = new World(connect, disconnect, 160, 160, () => {}, () => {}); world.join({ userId: "foo" }, new Point(100, 100)); @@ -62,14 +62,14 @@ describe("World", () => { it("should disconnect user1 and user2", () => { let connectCalled: boolean = false; let disconnectCallNumber: number = 0; - let connect: ConnectCallback = (user: string, group: Group): void => { + const connect: ConnectCallback = (user: string, group: Group): void => { connectCalled = true; } - let disconnect: DisconnectCallback = (user: string, group: Group): void => { + const disconnect: DisconnectCallback = (user: string, group: Group): void => { disconnectCallNumber++; } - let world = new World(connect, disconnect, 160, 160, () => {}, () => {}); + const world = new World(connect, disconnect, 160, 160, () => {}, () => {}); world.join({ userId: "foo" }, new Point(100, 100)); diff --git a/back/yarn.lock b/back/yarn.lock index a37028af..f660a5c8 100644 --- a/back/yarn.lock +++ b/back/yarn.lock @@ -266,6 +266,11 @@ better-assert@~1.0.0: dependencies: callsite "1.0.0" +bintrees@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524" + integrity sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ= + blob@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" @@ -785,6 +790,11 @@ functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" +generic-type-guard@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.2.0.tgz#1fb136f934730c776486526b8a21fe96b067e691" + integrity sha512-EkkrXYbOtJ3VPB+SOrU7EhwY65rZErItGtBg5wAqywaj07BOubwOZqMYaxOWekJ9akioGqXIsw1fYk3wwbWsDQ== + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" @@ -1333,6 +1343,13 @@ progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" +prom-client@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-12.0.0.tgz#9689379b19bd3f6ab88a9866124db9da3d76c6ed" + integrity sha512-JbzzHnw0VDwCvoqf8y1WDtq4wSBAbthMB1pcVI/0lzdqHGJI3KBJDXle70XK+c7Iv93Gihqo0a5LlOn+g8+DrQ== + dependencies: + tdigest "^0.1.1" + proxy-addr@~2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" @@ -1683,6 +1700,13 @@ table@^5.2.3: slice-ansi "^2.1.0" string-width "^3.0.0" +tdigest@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.1.tgz#2e3cb2c39ea449e55d1e6cd91117accca4588021" + integrity sha1-Ljyyw56kSeVdHmzZEReszKRYgCE= + dependencies: + bintrees "1.0.1" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" diff --git a/deeployer.libsonnet b/deeployer.libsonnet index cb8cf50a..975686be 100644 --- a/deeployer.libsonnet +++ b/deeployer.libsonnet @@ -42,6 +42,23 @@ "config": { "https": { "mail": "d.negrier@thecodingmachine.com" - } + }, + k8sextension(k8sConf):: + k8sConf + { + back+: { + deployment+: { + spec+: { + template+: { + metadata+: { + annotations+: { + "prometheus.io/port": "8080", + "prometheus.io/scrape": "true" + } + } + } + } + } + } + } } } diff --git a/front/.eslintrc.json b/front/.eslintrc.json index 0cee14a3..3aab37d9 100644 --- a/front/.eslintrc.json +++ b/front/.eslintrc.json @@ -7,7 +7,8 @@ }, "extends": [ "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended" + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking" ], "globals": { "Atomics": "readonly", @@ -16,12 +17,14 @@ "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 2018, - "sourceType": "module" + "sourceType": "module", + "project": "./tsconfig.json" }, "plugins": [ "@typescript-eslint" ], "rules": { - "no-unused-vars": "off" + "no-unused-vars": "off", + "@typescript-eslint/no-explicit-any": "error" } -} \ No newline at end of file +} diff --git a/front/LICENSE.txt b/front/LICENSE.txt new file mode 100644 index 00000000..614e6268 --- /dev/null +++ b/front/LICENSE.txt @@ -0,0 +1,691 @@ +NOTICE +This package contains software licensed under different +licenses, please refer to the NOTICE.txt file for further +information and LICENSES.txt for full license texts. + +WorkAdventure Enterprise edition can be licensed independently from +the source under separate commercial terms. + +The software ("Software") is developed and owned by TheCodingMachine +and is subject to the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, with the Commons Clause as follows: + + + + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license +for software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are +designed to take away your freedom to share and change the works. By +contrast, our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is +combined will remain governed by version 3 of the GNU General Public +License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero +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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. + + +"Commons Clause" License Condition + +The Software is provided to you by the Licensor under the License, as +defined below, subject to the following condition. Without limiting +other conditions in the License, the grant of rights under the License +will not include, and the License does not grant to you, the right to +Sell the Software. For purposes of the foregoing, "Sell" means +practicing any or all of the rights granted to you under the License +to provide to third parties, for a fee or other consideration, +a product or service that consists, entirely or substantially, +of the Software or the functionality of the Software. Any license +notice or attribution required by the License must also include +this Commons Cause License Condition notice. diff --git a/front/dist/index.html b/front/dist/index.html index a593e91a..6e9735f0 100644 --- a/front/dist/index.html +++ b/front/dist/index.html @@ -5,6 +5,35 @@ + + + + + + + + + + + + + + + + + + + + + + + WorkAdventure diff --git a/front/dist/static/images/Bitmap2.png b/front/dist/static/images/Bitmap2.png new file mode 100644 index 00000000..3e5bd3ff Binary files /dev/null and b/front/dist/static/images/Bitmap2.png differ diff --git a/front/dist/static/images/Bitmap3.png b/front/dist/static/images/Bitmap3.png new file mode 100644 index 00000000..269b839f Binary files /dev/null and b/front/dist/static/images/Bitmap3.png differ diff --git a/front/dist/static/images/Logo TCM.png b/front/dist/static/images/Logo TCM.png new file mode 100644 index 00000000..bf4881b1 Binary files /dev/null and b/front/dist/static/images/Logo TCM.png differ diff --git a/front/dist/static/images/amstrad.png b/front/dist/static/images/amstrad.png new file mode 100644 index 00000000..65ca3fb4 Binary files /dev/null and b/front/dist/static/images/amstrad.png differ diff --git a/front/dist/static/images/atari.png b/front/dist/static/images/atari.png new file mode 100644 index 00000000..4562d7e0 Binary files /dev/null and b/front/dist/static/images/atari.png differ diff --git a/front/dist/static/images/bitmap.png b/front/dist/static/images/bitmap.png new file mode 100644 index 00000000..45f1ef51 Binary files /dev/null and b/front/dist/static/images/bitmap.png differ diff --git a/front/dist/static/images/check.png b/front/dist/static/images/check.png new file mode 100644 index 00000000..14596e05 Binary files /dev/null and b/front/dist/static/images/check.png differ diff --git a/front/dist/static/images/choose_character.png b/front/dist/static/images/choose_character.png new file mode 100644 index 00000000..46841389 Binary files /dev/null and b/front/dist/static/images/choose_character.png differ diff --git a/front/dist/static/images/cloud.png b/front/dist/static/images/cloud.png new file mode 100644 index 00000000..dad532b9 Binary files /dev/null and b/front/dist/static/images/cloud.png differ diff --git a/front/dist/static/images/desktop.png b/front/dist/static/images/desktop.png new file mode 100644 index 00000000..63900d46 Binary files /dev/null and b/front/dist/static/images/desktop.png differ diff --git a/front/dist/static/images/facebook.png b/front/dist/static/images/facebook.png new file mode 100644 index 00000000..f94aab70 Binary files /dev/null and b/front/dist/static/images/facebook.png differ diff --git a/front/dist/static/images/favicons/android-icon-144x144.png b/front/dist/static/images/favicons/android-icon-144x144.png new file mode 100644 index 00000000..b0463804 Binary files /dev/null and b/front/dist/static/images/favicons/android-icon-144x144.png differ diff --git a/front/dist/static/images/favicons/android-icon-192x192.png b/front/dist/static/images/favicons/android-icon-192x192.png new file mode 100644 index 00000000..6f764aae Binary files /dev/null and b/front/dist/static/images/favicons/android-icon-192x192.png differ diff --git a/front/dist/static/images/favicons/android-icon-36x36.png b/front/dist/static/images/favicons/android-icon-36x36.png new file mode 100644 index 00000000..cc93c290 Binary files /dev/null and b/front/dist/static/images/favicons/android-icon-36x36.png differ diff --git a/front/dist/static/images/favicons/android-icon-48x48.png b/front/dist/static/images/favicons/android-icon-48x48.png new file mode 100644 index 00000000..a2a7f7e9 Binary files /dev/null and b/front/dist/static/images/favicons/android-icon-48x48.png differ diff --git a/front/dist/static/images/favicons/android-icon-72x72.png b/front/dist/static/images/favicons/android-icon-72x72.png new file mode 100644 index 00000000..9ae8c47a Binary files /dev/null and b/front/dist/static/images/favicons/android-icon-72x72.png differ diff --git a/front/dist/static/images/favicons/android-icon-96x96.png b/front/dist/static/images/favicons/android-icon-96x96.png new file mode 100644 index 00000000..43324e2c Binary files /dev/null and b/front/dist/static/images/favicons/android-icon-96x96.png differ diff --git a/front/dist/static/images/favicons/apple-icon-114x114.png b/front/dist/static/images/favicons/apple-icon-114x114.png new file mode 100644 index 00000000..f205a3ad Binary files /dev/null and b/front/dist/static/images/favicons/apple-icon-114x114.png differ diff --git a/front/dist/static/images/favicons/apple-icon-120x120.png b/front/dist/static/images/favicons/apple-icon-120x120.png new file mode 100644 index 00000000..09f4e85d Binary files /dev/null and b/front/dist/static/images/favicons/apple-icon-120x120.png differ diff --git a/front/dist/static/images/favicons/apple-icon-144x144.png b/front/dist/static/images/favicons/apple-icon-144x144.png new file mode 100644 index 00000000..b0463804 Binary files /dev/null and b/front/dist/static/images/favicons/apple-icon-144x144.png differ diff --git a/front/dist/static/images/favicons/apple-icon-152x152.png b/front/dist/static/images/favicons/apple-icon-152x152.png new file mode 100644 index 00000000..fa06794f Binary files /dev/null and b/front/dist/static/images/favicons/apple-icon-152x152.png differ diff --git a/front/dist/static/images/favicons/apple-icon-180x180.png b/front/dist/static/images/favicons/apple-icon-180x180.png new file mode 100644 index 00000000..4b9af8b6 Binary files /dev/null and b/front/dist/static/images/favicons/apple-icon-180x180.png differ diff --git a/front/dist/static/images/favicons/apple-icon-57x57.png b/front/dist/static/images/favicons/apple-icon-57x57.png new file mode 100644 index 00000000..bd72841f Binary files /dev/null and b/front/dist/static/images/favicons/apple-icon-57x57.png differ diff --git a/front/dist/static/images/favicons/apple-icon-60x60.png b/front/dist/static/images/favicons/apple-icon-60x60.png new file mode 100644 index 00000000..89238b40 Binary files /dev/null and b/front/dist/static/images/favicons/apple-icon-60x60.png differ diff --git a/front/dist/static/images/favicons/apple-icon-72x72.png b/front/dist/static/images/favicons/apple-icon-72x72.png new file mode 100644 index 00000000..9ae8c47a Binary files /dev/null and b/front/dist/static/images/favicons/apple-icon-72x72.png differ diff --git a/front/dist/static/images/favicons/apple-icon-76x76.png b/front/dist/static/images/favicons/apple-icon-76x76.png new file mode 100644 index 00000000..fbd0c29c Binary files /dev/null and b/front/dist/static/images/favicons/apple-icon-76x76.png differ diff --git a/front/dist/static/images/favicons/apple-icon-precomposed.png b/front/dist/static/images/favicons/apple-icon-precomposed.png new file mode 100644 index 00000000..3132ca5a Binary files /dev/null and b/front/dist/static/images/favicons/apple-icon-precomposed.png differ diff --git a/front/dist/static/images/favicons/apple-icon.png b/front/dist/static/images/favicons/apple-icon.png new file mode 100644 index 00000000..3132ca5a Binary files /dev/null and b/front/dist/static/images/favicons/apple-icon.png differ diff --git a/front/dist/static/images/favicons/browserconfig.xml b/front/dist/static/images/favicons/browserconfig.xml new file mode 100644 index 00000000..c5541482 --- /dev/null +++ b/front/dist/static/images/favicons/browserconfig.xml @@ -0,0 +1,2 @@ + +#ffffff \ No newline at end of file diff --git a/front/dist/static/images/favicons/favicon-16x16.png b/front/dist/static/images/favicons/favicon-16x16.png new file mode 100644 index 00000000..a49eb71a Binary files /dev/null and b/front/dist/static/images/favicons/favicon-16x16.png differ diff --git a/front/dist/static/images/favicons/favicon-32x32.png b/front/dist/static/images/favicons/favicon-32x32.png new file mode 100644 index 00000000..957f5006 Binary files /dev/null and b/front/dist/static/images/favicons/favicon-32x32.png differ diff --git a/front/dist/static/images/favicons/favicon-96x96.png b/front/dist/static/images/favicons/favicon-96x96.png new file mode 100644 index 00000000..43324e2c Binary files /dev/null and b/front/dist/static/images/favicons/favicon-96x96.png differ diff --git a/front/dist/static/images/favicons/favicon.ico b/front/dist/static/images/favicons/favicon.ico new file mode 100644 index 00000000..ec628ea2 Binary files /dev/null and b/front/dist/static/images/favicons/favicon.ico differ diff --git a/front/dist/static/images/favicons/manifest.json b/front/dist/static/images/favicons/manifest.json new file mode 100644 index 00000000..013d4a6a --- /dev/null +++ b/front/dist/static/images/favicons/manifest.json @@ -0,0 +1,41 @@ +{ + "name": "App", + "icons": [ + { + "src": "\/android-icon-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "0.75" + }, + { + "src": "\/android-icon-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": "1.0" + }, + { + "src": "\/android-icon-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "\/android-icon-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + }, + { + "src": "\/android-icon-144x144.png", + "sizes": "144x144", + "type": "image\/png", + "density": "3.0" + }, + { + "src": "\/android-icon-192x192.png", + "sizes": "192x192", + "type": "image\/png", + "density": "4.0" + } + ] +} \ No newline at end of file diff --git a/front/dist/static/images/favicons/ms-icon-144x144.png b/front/dist/static/images/favicons/ms-icon-144x144.png new file mode 100644 index 00000000..b0463804 Binary files /dev/null and b/front/dist/static/images/favicons/ms-icon-144x144.png differ diff --git a/front/dist/static/images/favicons/ms-icon-150x150.png b/front/dist/static/images/favicons/ms-icon-150x150.png new file mode 100644 index 00000000..4ab38ea3 Binary files /dev/null and b/front/dist/static/images/favicons/ms-icon-150x150.png differ diff --git a/front/dist/static/images/favicons/ms-icon-310x310.png b/front/dist/static/images/favicons/ms-icon-310x310.png new file mode 100644 index 00000000..56ceeb95 Binary files /dev/null and b/front/dist/static/images/favicons/ms-icon-310x310.png differ diff --git a/front/dist/static/images/favicons/ms-icon-70x70.png b/front/dist/static/images/favicons/ms-icon-70x70.png new file mode 100644 index 00000000..3fa12cae Binary files /dev/null and b/front/dist/static/images/favicons/ms-icon-70x70.png differ diff --git a/front/dist/static/images/female-character.gif b/front/dist/static/images/female-character.gif new file mode 100644 index 00000000..a9d7b998 Binary files /dev/null and b/front/dist/static/images/female-character.gif differ diff --git a/front/dist/static/images/floppy.png b/front/dist/static/images/floppy.png new file mode 100644 index 00000000..ae99f5a7 Binary files /dev/null and b/front/dist/static/images/floppy.png differ diff --git a/front/dist/static/images/interact.png b/front/dist/static/images/interact.png new file mode 100644 index 00000000..325af09d Binary files /dev/null and b/front/dist/static/images/interact.png differ diff --git a/front/dist/static/images/linkedin.png b/front/dist/static/images/linkedin.png new file mode 100644 index 00000000..596cdb9d Binary files /dev/null and b/front/dist/static/images/linkedin.png differ diff --git a/front/dist/static/images/logo.png b/front/dist/static/images/logo.png new file mode 100644 index 00000000..f4440ad5 Binary files /dev/null and b/front/dist/static/images/logo.png differ diff --git a/front/dist/static/images/male-character.gif b/front/dist/static/images/male-character.gif new file mode 100644 index 00000000..d8b0c0fe Binary files /dev/null and b/front/dist/static/images/male-character.gif differ diff --git a/front/dist/static/images/maps/coders.png b/front/dist/static/images/maps/coders.png new file mode 100644 index 00000000..4dd017d5 Binary files /dev/null and b/front/dist/static/images/maps/coders.png differ diff --git a/front/dist/static/images/maps/creative.png b/front/dist/static/images/maps/creative.png new file mode 100644 index 00000000..5c732a3c Binary files /dev/null and b/front/dist/static/images/maps/creative.png differ diff --git a/front/dist/static/images/maps/dungeon.png b/front/dist/static/images/maps/dungeon.png new file mode 100644 index 00000000..71a6b3b0 Binary files /dev/null and b/front/dist/static/images/maps/dungeon.png differ diff --git a/front/dist/static/images/maps/fantasy.png b/front/dist/static/images/maps/fantasy.png new file mode 100644 index 00000000..e9583ac9 Binary files /dev/null and b/front/dist/static/images/maps/fantasy.png differ diff --git a/front/dist/static/images/maps/office.png b/front/dist/static/images/maps/office.png new file mode 100644 index 00000000..d9925da2 Binary files /dev/null and b/front/dist/static/images/maps/office.png differ diff --git a/front/dist/static/images/maps/pub.png b/front/dist/static/images/maps/pub.png new file mode 100644 index 00000000..d5d71303 Binary files /dev/null and b/front/dist/static/images/maps/pub.png differ diff --git a/front/dist/static/images/maps/school.png b/front/dist/static/images/maps/school.png new file mode 100644 index 00000000..a55dabb6 Binary files /dev/null and b/front/dist/static/images/maps/school.png differ diff --git a/front/dist/static/images/maps/street.png b/front/dist/static/images/maps/street.png new file mode 100644 index 00000000..8b099bbc Binary files /dev/null and b/front/dist/static/images/maps/street.png differ diff --git a/front/dist/static/images/maps/tcm.png b/front/dist/static/images/maps/tcm.png new file mode 100644 index 00000000..0c93f9bc Binary files /dev/null and b/front/dist/static/images/maps/tcm.png differ diff --git a/front/dist/static/images/meta-tags-image.jpg b/front/dist/static/images/meta-tags-image.jpg new file mode 100644 index 00000000..55ed50ce Binary files /dev/null and b/front/dist/static/images/meta-tags-image.jpg differ diff --git a/front/dist/static/images/play.png b/front/dist/static/images/play.png new file mode 100644 index 00000000..5db8aa5e Binary files /dev/null and b/front/dist/static/images/play.png differ diff --git a/front/dist/static/images/sinclair-2.png b/front/dist/static/images/sinclair-2.png new file mode 100644 index 00000000..d5cc2f4c Binary files /dev/null and b/front/dist/static/images/sinclair-2.png differ diff --git a/front/dist/static/images/step 1.png b/front/dist/static/images/step 1.png new file mode 100644 index 00000000..772f1a2a Binary files /dev/null and b/front/dist/static/images/step 1.png differ diff --git a/front/dist/static/images/step 2.png b/front/dist/static/images/step 2.png new file mode 100644 index 00000000..a6649fd4 Binary files /dev/null and b/front/dist/static/images/step 2.png differ diff --git a/front/dist/static/images/step 3.png b/front/dist/static/images/step 3.png new file mode 100644 index 00000000..946efbe8 Binary files /dev/null and b/front/dist/static/images/step 3.png differ diff --git a/front/dist/static/images/super-nintendo.png b/front/dist/static/images/super-nintendo.png new file mode 100644 index 00000000..1ac3b8ff Binary files /dev/null and b/front/dist/static/images/super-nintendo.png differ diff --git a/front/dist/static/images/twitter.png b/front/dist/static/images/twitter.png new file mode 100644 index 00000000..57f3a9d9 Binary files /dev/null and b/front/dist/static/images/twitter.png differ diff --git a/front/package.json b/front/package.json index c05bd8ba..d73eb50d 100644 --- a/front/package.json +++ b/front/package.json @@ -2,7 +2,7 @@ "name": "workadventurefront", "version": "1.0.0", "main": "index.js", - "license": "AGPL", + "license": "SEE LICENSE IN LICENSE.txt", "devDependencies": { "@types/jasmine": "^3.5.10", "@typescript-eslint/eslint-plugin": "^2.26.0", @@ -22,6 +22,7 @@ "@types/simple-peer": "^9.6.0", "@types/socket.io-client": "^1.4.32", "phaser": "^3.22.0", + "queue-typescript": "^1.0.1", "simple-peer": "^9.6.2", "socket.io-client": "^2.3.0" }, diff --git a/front/src/Connection.ts b/front/src/Connection.ts index 3c1bc2d4..c4ac92c6 100644 --- a/front/src/Connection.ts +++ b/front/src/Connection.ts @@ -1,4 +1,3 @@ -import {GameManager} from "./Phaser/Game/GameManager"; import Axios from "axios"; import {API_URL} from "./Enum/EnvironmentVariable"; import {MessageUI} from "./Logger/MessageUI"; @@ -8,12 +7,12 @@ const SocketIo = require('socket.io-client'); import Socket = SocketIOClient.Socket; import {PlayerAnimationNames} from "./Phaser/Player/Animation"; import {UserSimplePeer} from "./WebRtc/SimplePeer"; +import {SignalData} from "simple-peer"; enum EventMessage{ WEBRTC_SIGNAL = "webrtc-signal", WEBRTC_START = "webrtc-start", - WEBRTC_JOIN_ROOM = "webrtc-join-room", JOIN_ROOM = "join-room", // bi-directional USER_POSITION = "user-position", // bi-directional USER_MOVED = "user-moved", // From server to client @@ -22,22 +21,9 @@ enum EventMessage{ WEBRTC_DISCONNECT = "webrtc-disconect", GROUP_CREATE_UPDATE = "group-create-update", GROUP_DELETE = "group-delete", + SET_PLAYER_DETAILS = "set-player-details", // Send the name and character to the server (on connect), receive back the id. CONNECT_ERROR = "connect_error", - RECONNECT = "reconnect", - SET_PLAYER_DETAILS = "set-player-details" // Send the name and character to the server (on connect), receive back the id. -} - -class Message { - userId: string; - name: string; - character: string; - - constructor(userId : string, name: string, character: string) { - this.userId = userId; - this.name = name; - this.character = character; - } } export interface PointInterface { @@ -67,15 +53,6 @@ export interface MessageUserMovedInterface { position: PointInterface; } -class MessageUserPosition extends Message implements MessageUserPositionInterface{ - position: PointInterface; - - constructor(userId : string, point : Point, name: string, character: string) { - super(userId, name, character); - this.position = point; - } -} - export interface MessageUserJoined { userId: string; name: string; @@ -83,11 +60,6 @@ export interface MessageUserJoined { position: PointInterface } -export interface ListMessageUserPositionInterface { - roomId: string; - listUsersPosition: Array; -} - export interface PositionInterface { x: number, y: number @@ -107,179 +79,117 @@ export interface WebRtcDisconnectMessageInterface { userId: string } -export interface ConnectionInterface { - socket: Socket|null; - token: string|null; - name: string|null; - userId: string|null; - - createConnection(name: string, characterSelected: string): Promise; - - loadStartMap(): Promise; - - joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): void; - - sharePosition(x: number, y: number, direction: string, moving: boolean): void; - - /*webrtc*/ - sendWebrtcSignal(signal: any, roomId: string, userId?: string|null, receiverId?: string): void; - - receiveWebrtcSignal(callBack: Function): void; - - receiveWebrtcStart(callBack: (message: WebRtcStartMessageInterface) => void): void; - - disconnectMessage(callBack: (message: WebRtcDisconnectMessageInterface) => void): void; +export interface WebRtcSignalMessageInterface { + userId: string, + receiverId: string, + roomId: string, + signal: SignalData } -export class Connection implements ConnectionInterface { - socket: Socket|null = null; - token: string|null = null; - name: string|null = null; // TODO: drop "name" storage here - character: string|null = null; - userId: string|null = null; +export interface StartMapInterface { + mapUrlStart: string, + startInstance: string +} - GameManager: GameManager; +export class Connection implements Connection { + private readonly socket: Socket; + private userId: string|null = null; - lastPositionShared: PointInterface|null = null; - lastRoom: string|null = null; + private constructor(token: string) { - constructor(GameManager: GameManager) { - this.GameManager = GameManager; + this.socket = SocketIo(`${API_URL}`, { + query: { + token: token + }, + reconnection: false // Reconnection is handled by the application itself + }); + + this.socket.on(EventMessage.MESSAGE_ERROR, (message: string) => { + console.error(EventMessage.MESSAGE_ERROR, message); + }) } - createConnection(name: string, characterSelected: string): Promise { - this.name = name; - this.character = characterSelected; + public static createConnection(name: string, characterSelected: string): Promise { return Axios.post(`${API_URL}/login`, {name: name}) .then((res) => { - this.token = res.data.token; - this.socket = SocketIo(`${API_URL}`, { - query: { - token: this.token - } + + return new Promise((resolve, reject) => { + const connection = new Connection(res.data.token); + + connection.onConnectError((error: object) => { + console.log('An error occurred while connecting to socket server. Retrying'); + reject(error); + }); + + connection.socket.emit(EventMessage.SET_PLAYER_DETAILS, { + name: name, + character: characterSelected + } as SetPlayerDetailsMessage, (id: string) => { + connection.userId = id; + }); + + resolve(connection); }); - return this.connectSocketServer(); }) .catch((err) => { - console.error(err); - throw err; + // Let's retry in 4-6 seconds + return new Promise((resolve, reject) => { + setTimeout(() => { + Connection.createConnection(name, characterSelected).then((connection) => resolve(connection)) + .catch((error) => reject(error)); + }, 4000 + Math.floor(Math.random() * 2000) ); + }); }); } - private getSocket(): Socket { - if (this.socket === null) { - throw new Error('Socket not initialized while using Connection') - } - return this.socket; + public closeConnection(): void { + this.socket?.close(); } - /** - * - * @param character - */ - connectSocketServer(): Promise{ - //listen event - this.disconnectServer(); - this.errorMessage(); - this.groupUpdatedOrCreated(); - this.groupDeleted(); - this.onUserJoins(); - this.onUserMoved(); - this.onUserLeft(); - return new Promise((resolve, reject) => { - this.getSocket().emit(EventMessage.SET_PLAYER_DETAILS, { - name: this.name, - character: this.character - } as SetPlayerDetailsMessage, (id: string) => { - this.userId = id; + public joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): Promise { + const promise = new Promise((resolve, reject) => { + this.socket.emit(EventMessage.JOIN_ROOM, { roomId, position: {x: startX, y: startY, direction, moving }}, (userPositions: MessageUserPositionInterface[]) => { + resolve(userPositions); }); - - //if try to reconnect with last position - /*if(this.lastRoom) { - //join the room - this.joinARoom(this.lastRoom, - this.lastPositionShared ? this.lastPositionShared.x : 0, - this.lastPositionShared ? this.lastPositionShared.y : 0, - this.lastPositionShared ? this.lastPositionShared.direction : PlayerAnimationNames.WalkDown, - this.lastPositionShared ? this.lastPositionShared.moving : false); - }*/ - - /*if(this.lastPositionShared) { - - //share your first position - this.sharePosition( - this.lastPositionShared ? this.lastPositionShared.x : 0, - this.lastPositionShared ? this.lastPositionShared.y : 0, - this.lastPositionShared.direction, - this.lastPositionShared.moving - ); - }*/ - - resolve(this); - }); + }) + return promise; } - //TODO add middleware with access token to secure api - loadStartMap() : Promise { - return Axios.get(`${API_URL}/start-map`) - .then((res) => { - return res.data; - }).catch((err) => { - console.error(err); - throw err; - }); - } - - joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): void { - this.getSocket().emit(EventMessage.JOIN_ROOM, { roomId, position: {x: startX, y: startY, direction, moving }}, (userPositions: MessageUserPositionInterface[]) => { - this.GameManager.initUsersPosition(userPositions); - }); - this.lastRoom = roomId; - } - - sharePosition(x : number, y : number, direction : string, moving: boolean) : void{ + public sharePosition(x : number, y : number, direction : string, moving: boolean) : void{ if(!this.socket){ return; } - let point = new Point(x, y, direction, moving); - this.lastPositionShared = point; - this.getSocket().emit(EventMessage.USER_POSITION, point); + const point = new Point(x, y, direction, moving); + this.socket.emit(EventMessage.USER_POSITION, point); } - private onUserJoins(): void { - this.getSocket().on(EventMessage.JOIN_ROOM, (message: MessageUserJoined) => { - this.GameManager.onUserJoins(message); - }); + public onUserJoins(callback: (message: MessageUserJoined) => void): void { + this.socket.on(EventMessage.JOIN_ROOM, callback); } - private onUserMoved(): void { - this.getSocket().on(EventMessage.USER_MOVED, (message: MessageUserMovedInterface) => { - this.GameManager.onUserMoved(message); - }); + public onUserMoved(callback: (message: MessageUserMovedInterface) => void): void { + this.socket.on(EventMessage.USER_MOVED, callback); } - private onUserLeft(): void { - this.getSocket().on(EventMessage.USER_LEFT, (userId: string) => { - this.GameManager.onUserLeft(userId); - }); + public onUserLeft(callback: (userId: string) => void): void { + this.socket.on(EventMessage.USER_LEFT, callback); } - private groupUpdatedOrCreated(): void { - this.getSocket().on(EventMessage.GROUP_CREATE_UPDATE, (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => { - //console.log('Group ', groupCreateUpdateMessage.groupId, " position :", groupCreateUpdateMessage.position.x, groupCreateUpdateMessage.position.y) - this.GameManager.shareGroupPosition(groupCreateUpdateMessage); - }) + public onGroupUpdatedOrCreated(callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void): void { + this.socket.on(EventMessage.GROUP_CREATE_UPDATE, callback); } - private groupDeleted(): void { - this.getSocket().on(EventMessage.GROUP_DELETE, (groupId: string) => { - this.GameManager.deleteGroup(groupId); - }) + public onGroupDeleted(callback: (groupId: string) => void): void { + this.socket.on(EventMessage.GROUP_DELETE, callback) } - sendWebrtcSignal(signal: any, roomId: string, userId? : string|null, receiverId? : string) { - return this.getSocket().emit(EventMessage.WEBRTC_SIGNAL, { + public onConnectError(callback: (error: object) => void): void { + this.socket.on(EventMessage.CONNECT_ERROR, callback) + } + + public sendWebrtcSignal(signal: unknown, roomId: string, userId? : string|null, receiverId? : string) { + return this.socket.emit(EventMessage.WEBRTC_SIGNAL, { userId: userId ? userId : this.userId, receiverId: receiverId ? receiverId : this.userId, roomId: roomId, @@ -287,35 +197,30 @@ export class Connection implements ConnectionInterface { }); } - receiveWebrtcStart(callback: (message: WebRtcStartMessageInterface) => void) { - this.getSocket().on(EventMessage.WEBRTC_START, callback); + public receiveWebrtcStart(callback: (message: WebRtcStartMessageInterface) => void) { + this.socket.on(EventMessage.WEBRTC_START, callback); } - receiveWebrtcSignal(callback: Function) { - return this.getSocket().on(EventMessage.WEBRTC_SIGNAL, callback); + public receiveWebrtcSignal(callback: (message: WebRtcSignalMessageInterface) => void) { + return this.socket.on(EventMessage.WEBRTC_SIGNAL, callback); } - private errorMessage(): void { - this.getSocket().on(EventMessage.MESSAGE_ERROR, (message: string) => { - console.error(EventMessage.MESSAGE_ERROR, message); - }) - } - - private disconnectServer(): void { - this.getSocket().on(EventMessage.CONNECT_ERROR, () => { - this.GameManager.switchToDisconnectedScene(); - }); - - this.getSocket().on(EventMessage.RECONNECT, () => { - this.connectSocketServer(); - if (this.lastPositionShared === null) { - throw new Error('No last position shared found while reconnecting'); + public onServerDisconnected(callback: (reason: string) => void): void { + this.socket.on('disconnect', (reason: string) => { + if (reason === 'io client disconnect') { + // The client asks for disconnect, let's not trigger any event. + return; } - this.GameManager.reconnectToGameScene(this.lastPositionShared); + callback(reason); }); + + } + + public getUserId(): string|null { + return this.userId; } disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void { - this.getSocket().on(EventMessage.WEBRTC_DISCONNECT, callback); + this.socket.on(EventMessage.WEBRTC_DISCONNECT, callback); } } diff --git a/front/src/Cypress/CypressAsserter.ts b/front/src/Cypress/CypressAsserter.ts index 95adb156..82eeab1f 100644 --- a/front/src/Cypress/CypressAsserter.ts +++ b/front/src/Cypress/CypressAsserter.ts @@ -1,13 +1,17 @@ -declare let window:any; +declare let window:WindowWithCypressAsserter; + +interface WindowWithCypressAsserter extends Window { + cypressAsserter: CypressAsserter; +} //this class is used to communicate with cypress, our e2e testing client //Since cypress cannot manipulate canvas, we notified it with console logs class CypressAsserter { - + constructor() { window.cypressAsserter = this } - + gameStarted() { console.log('Started the game') } @@ -29,4 +33,4 @@ class CypressAsserter { } } -export const cypressAsserter = new CypressAsserter() \ No newline at end of file +export const cypressAsserter = new CypressAsserter() diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts index 2fbf7979..6e0edd8f 100644 --- a/front/src/Enum/EnvironmentVariable.ts +++ b/front/src/Enum/EnvironmentVariable.ts @@ -1,4 +1,4 @@ -const DEBUG_MODE: boolean = process.env.DEBUG_MODE as any === true; +const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true"; const API_URL = process.env.API_URL || "http://api.workadventure.localhost"; const RESOLUTION = 3; const ZOOM_LEVEL = 1/*3/4*/; diff --git a/front/src/Logger/MessageUI.ts b/front/src/Logger/MessageUI.ts index 6011fb73..2a581091 100644 --- a/front/src/Logger/MessageUI.ts +++ b/front/src/Logger/MessageUI.ts @@ -2,7 +2,7 @@ export class MessageUI { static warningMessage(text: string){ this.removeMessage(); - let body = document.getElementById("body"); + const body = document.getElementById("body"); body?.insertAdjacentHTML('afterbegin', `
${text} @@ -12,13 +12,13 @@ export class MessageUI { static removeMessage(id : string|null = null) { if(!id){ - let messages = document.getElementsByClassName("message-info"); + const messages = document.getElementsByClassName("message-info"); for (let i = 0; i < messages.length; i++){ messages.item(i)?.remove(); } return; } - let previousElement = document.getElementById(id); + const previousElement = document.getElementById(id); if (!previousElement) { return; } diff --git a/front/src/Phaser/Components/TextInput.ts b/front/src/Phaser/Components/TextInput.ts index 92ddcb56..3a20eadf 100644 --- a/front/src/Phaser/Components/TextInput.ts +++ b/front/src/Phaser/Components/TextInput.ts @@ -10,9 +10,9 @@ export class TextInput extends Phaser.GameObjects.BitmapText { this.underLine = this.scene.add.text(x, y+1, '_______', { fontFamily: 'Arial', fontSize: "32px", color: '#ffffff'}) - let keySpace = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE); - let keyBackspace = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.BACKSPACE); - this.scene.input.keyboard.on('keydown', (event: any) => { + const keySpace = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE); + const keyBackspace = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.BACKSPACE); + this.scene.input.keyboard.on('keydown', (event: KeyboardEvent) => { if (event.keyCode === 8 && this.text.length > 0) { this.deleteLetter(); } else if ((event.keyCode === 32 || (event.keyCode >= 48 && event.keyCode <= 90)) && this.text.length < maxLength) { diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index ec0167eb..7453dc75 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -2,7 +2,12 @@ import {PlayerAnimationNames} from "../Player/Animation"; import {SpeechBubble} from "./SpeechBubble"; import BitmapText = Phaser.GameObjects.BitmapText; -export const PLAYER_RESOURCES: Array = [ +export interface PlayerResourceDescriptionInterface { + name: string, + img: string +} + +export const PLAYER_RESOURCES: Array = [ {name: "male1", img: "resources/characters/pipoya/Male 01-1.png" /*, x: 32, y: 32*/}, {name: "male2", img: "resources/characters/pipoya/Male 02-2.png"/*, x: 64, y: 32*/}, {name: "male3", img: "resources/characters/pipoya/Male 03-4.png"/*, x: 96, y: 32*/}, @@ -117,6 +122,10 @@ export abstract class Character extends Phaser.Physics.Arcade.Sprite { } protected playAnimation(direction : string, moving: boolean): void { + if (!this.anims) { + console.error('ANIMS IS NOT DEFINED!!!'); + return; + } if (moving && (!this.anims.currentAnim || this.anims.currentAnim.key !== direction)) { this.play(this.PlayerTexture+'-'+direction, true); } else if (!moving) { diff --git a/front/src/Phaser/Entity/SpeechBubble.ts b/front/src/Phaser/Entity/SpeechBubble.ts index f2385290..30518890 100644 --- a/front/src/Phaser/Entity/SpeechBubble.ts +++ b/front/src/Phaser/Entity/SpeechBubble.ts @@ -13,10 +13,10 @@ export class SpeechBubble { */ constructor(scene: Scene, player: Character, text: string = "") { - let bubbleHeight = 50; - let bubblePadding = 10; - let bubbleWidth = bubblePadding * 2 + text.length * 10; - let arrowHeight = bubbleHeight / 4; + const bubbleHeight = 50; + const bubblePadding = 10; + const bubbleWidth = bubblePadding * 2 + text.length * 10; + const arrowHeight = bubbleHeight / 4; this.bubble = scene.add.graphics({ x: player.x + 16, y: player.y - 80 }); @@ -35,12 +35,12 @@ export class SpeechBubble { this.bubble.fillRoundedRect(0, 0, bubbleWidth, bubbleHeight, 16); // Calculate arrow coordinates - let point1X = Math.floor(bubbleWidth / 7); - let point1Y = bubbleHeight; - let point2X = Math.floor((bubbleWidth / 7) * 2); - let point2Y = bubbleHeight; - let point3X = Math.floor(bubbleWidth / 7); - let point3Y = Math.floor(bubbleHeight + arrowHeight); + const point1X = Math.floor(bubbleWidth / 7); + const point1Y = bubbleHeight; + const point2X = Math.floor((bubbleWidth / 7) * 2); + const point2Y = bubbleHeight; + const point3X = Math.floor(bubbleWidth / 7); + const point3Y = Math.floor(bubbleHeight + arrowHeight); // bubble arrow shadow this.bubble.lineStyle(4, 0x222222, 0.5); @@ -54,7 +54,7 @@ export class SpeechBubble { this.content = scene.add.text(0, 0, text, { fontFamily: 'Arial', fontSize: 20, color: '#000000', align: 'center', wordWrap: { width: bubbleWidth - (bubblePadding * 2) } }); - let bounds = this.content.getBounds(); + const bounds = this.content.getBounds(); this.content.setPosition(this.bubble.x + (bubbleWidth / 2) - (bounds.width / 2), this.bubble.y + (bubbleHeight / 2) - (bounds.height / 2)); } @@ -68,10 +68,10 @@ export class SpeechBubble { this.bubble.setPosition((x + 16), (y - 80)); } if (this.content) { - let bubbleHeight = 50; - let bubblePadding = 10; - let bubbleWidth = bubblePadding * 2 + this.content.text.length * 10; - let bounds = this.content.getBounds(); + const bubbleHeight = 50; + const bubblePadding = 10; + const bubbleWidth = bubblePadding * 2 + this.content.text.length * 10; + const bounds = this.content.getBounds(); //this.content.setPosition(x, y); this.content.setPosition(this.bubble.x + (bubbleWidth / 2) - (bounds.width / 2), this.bubble.y + (bubbleHeight / 2) - (bounds.height / 2)); } diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 7eef49b4..04cb1bbe 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -1,22 +1,9 @@ import {GameScene} from "./GameScene"; import { - Connection, - GroupCreatedUpdatedMessageInterface, - ListMessageUserPositionInterface, - MessageUserJoined, - MessageUserMovedInterface, - MessageUserPositionInterface, - Point, - PointInterface + StartMapInterface } from "../../Connection"; -import {SimplePeer} from "../../WebRtc/SimplePeer"; -import {AddPlayerInterface} from "./AddPlayerInterface"; -import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; - -/*export enum StatusGameManagerEnum { - IN_PROGRESS = 1, - CURRENT_USER_CREATED = 2 -}*/ +import Axios from "axios"; +import {API_URL} from "../../Enum/EnvironmentVariable"; export interface HasMovedEvent { direction: string; @@ -25,152 +12,43 @@ export interface HasMovedEvent { y: number; } -export interface MapObject { - key: string, - url: string -} - export class GameManager { - //status: number; - private ConnectionInstance: Connection; - private currentGameScene: GameScene; private playerName: string; - SimplePeer : SimplePeer; private characterUserSelected: string; - constructor() { - //this.status = StatusGameManagerEnum.IN_PROGRESS; - } - - connect(name: string, characterUserSelected : string) { + public storePlayerDetails(name: string, characterUserSelected : string): void { this.playerName = name; this.characterUserSelected = characterUserSelected; - this.ConnectionInstance = new Connection(this); - return this.ConnectionInstance.createConnection(name, characterUserSelected).then((data : any) => { - this.SimplePeer = new SimplePeer(this.ConnectionInstance); - return data; - }).catch((err) => { - throw err; - }); } - loadStartMap(){ - return this.ConnectionInstance.loadStartMap().then((data) => { - return data; - }).catch((err) => { - throw err; - }); - } - - setCurrentGameScene(gameScene: GameScene) { - this.currentGameScene = gameScene; - } - - - /** - * Permit to create player in started room - */ - /*createCurrentPlayer(): void { - //Get started room send by the backend - this.currentGameScene.createCurrentPlayer(); - //this.status = StatusGameManagerEnum.CURRENT_USER_CREATED; - }*/ - - joinRoom(sceneKey: string, startX: number, startY: number, direction: string, moving: boolean){ - this.ConnectionInstance.joinARoom(sceneKey, startX, startY, direction, moving); - } - - onUserJoins(message: MessageUserJoined): void { - let userMessage: AddPlayerInterface = { - userId: message.userId, - character: message.character, - name: message.name, - position: message.position - } - this.currentGameScene.addPlayer(userMessage); - } - - onUserMoved(message: MessageUserMovedInterface): void { - this.currentGameScene.updatePlayerPosition(message); - } - - onUserLeft(userId: string): void { - this.currentGameScene.removePlayer(userId); - } - - initUsersPosition(usersPosition: MessageUserPositionInterface[]): void { - // Shall we wait for room to be loaded? - /*if (this.status === StatusGameManagerEnum.IN_PROGRESS) { - return; - }*/ - try { - this.currentGameScene.initUsersPosition(usersPosition) - } catch (e) { - console.error(e); - } - } - - /** - * Share group position in game - */ - shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface): void { - /*if (this.status === StatusGameManagerEnum.IN_PROGRESS) { - return; - }*/ - try { - this.currentGameScene.shareGroupPosition(groupPositionMessage) - } catch (e) { - console.error(e); - } - } - - deleteGroup(groupId: string): void { - /*if (this.status === StatusGameManagerEnum.IN_PROGRESS) { - return; - }*/ - try { - this.currentGameScene.deleteGroup(groupId) - } catch (e) { - console.error(e); - } + loadStartMap() : Promise { + return Axios.get(`${API_URL}/start-map`) + .then((res) => { + return res.data; + }).catch((err) => { + console.error(err); + throw err; + }); } getPlayerName(): string { return this.playerName; } - getPlayerId(): string|null { - return this.ConnectionInstance.userId; - } - getCharacterSelected(): string { return this.characterUserSelected; } - pushPlayerPosition(event: HasMovedEvent) { - this.ConnectionInstance.sharePosition(event.x, event.y, event.direction, event.moving); - } - loadMap(mapUrl: string, scene: Phaser.Scenes.ScenePlugin, instance: string): string { - let sceneKey = GameScene.getMapKeyByUrl(mapUrl); + const sceneKey = GameScene.getMapKeyByUrl(mapUrl); - let gameIndex = scene.getIndex(sceneKey); + const gameIndex = scene.getIndex(sceneKey); if(gameIndex === -1){ - let game : Phaser.Scene = GameScene.createFromUrl(mapUrl, instance); + const game : Phaser.Scene = GameScene.createFromUrl(mapUrl, instance); scene.add(sceneKey, game, false); } return sceneKey; } - - private oldSceneKey : string; - switchToDisconnectedScene(): void { - this.oldSceneKey = this.currentGameScene.scene.key; - this.currentGameScene.scene.start(ReconnectingSceneName); - } - - reconnectToGameScene(lastPositionShared: PointInterface) { - this.currentGameScene.scene.start(this.oldSceneKey, { initPosition: lastPositionShared }); - } } export const gameManager = new GameManager(); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 245b2f32..8763f913 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,13 +1,19 @@ import {GameManager, gameManager, HasMovedEvent} from "./GameManager"; import { - GroupCreatedUpdatedMessageInterface, + Connection, + GroupCreatedUpdatedMessageInterface, MessageUserJoined, MessageUserMovedInterface, MessageUserPositionInterface, PointInterface, PositionInterface } from "../../Connection"; import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player"; import { DEBUG_MODE, ZOOM_LEVEL, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; -import {ITiledMap, ITiledMapLayer, ITiledTileSet} from "../Map/ITiledMap"; -import {PLAYER_RESOURCES} from "../Entity/Character"; +import { + ITiledMap, + ITiledMapLayer, + ITiledMapLayerProperty, + ITiledTileSet +} from "../Map/ITiledMap"; +import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "../Entity/Character"; import Texture = Phaser.Textures.Texture; import Sprite = Phaser.GameObjects.Sprite; import CanvasTexture = Phaser.Textures.CanvasTexture; @@ -16,6 +22,11 @@ import {PlayerAnimationNames} from "../Player/Animation"; import {PlayerMovement} from "./PlayerMovement"; import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator"; import {RemotePlayer} from "../Entity/RemotePlayer"; +import GameObject = Phaser.GameObjects.GameObject; +import { Queue } from 'queue-typescript'; +import {SimplePeer} from "../../WebRtc/SimplePeer"; +import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; + export enum Textures { Player = "male1" @@ -26,6 +37,36 @@ export interface GameSceneInitInterface { startLayerName: string|undefined } +interface InitUserPositionEventInterface { + type: 'InitUserPositionEvent' + event: MessageUserPositionInterface[] +} + +interface AddPlayerEventInterface { + type: 'AddPlayerEvent' + event: AddPlayerInterface +} + +interface RemovePlayerEventInterface { + type: 'RemovePlayerEvent' + userId: string +} + +interface UserMovedEventInterface { + type: 'UserMovedEvent' + event: MessageUserMovedInterface +} + +interface GroupCreatedUpdatedEventInterface { + type: 'GroupCreatedUpdatedEvent' + event: GroupCreatedUpdatedMessageInterface +} + +interface DeleteGroupEventInterface { + type: 'DeleteGroupEvent' + groupId: string +} + export class GameScene extends Phaser.Scene { GameManager : GameManager; Terrains : Array; @@ -40,8 +81,12 @@ export class GameScene extends Phaser.Scene { startX: number; startY: number; circleTexture: CanvasTexture; + pendingEvents: Queue = new Queue(); private initPosition: PositionInterface|null = null; private playersPositionInterpolator = new PlayersPositionInterpolator(); + private connection: Connection; + private simplePeer : SimplePeer; + private connectionPromise: Promise MapKey: string; MapUrlFile: string; @@ -57,17 +102,20 @@ export class GameScene extends Phaser.Scene { y: -1000 } - PositionNextScene: Array = new Array(); + private PositionNextScene: Array> = new Array>(); private startLayerName: string|undefined; - static createFromUrl(mapUrlFile: string, instance: string): GameScene { - let key = GameScene.getMapKeyByUrl(mapUrlFile); - return new GameScene(key, mapUrlFile, instance); + static createFromUrl(mapUrlFile: string, instance: string, key: string|null = null): GameScene { + const mapKey = GameScene.getMapKeyByUrl(mapUrlFile); + if (key === null) { + key = mapKey; + } + return new GameScene(mapKey, mapUrlFile, instance, key); } - constructor(MapKey : string, MapUrlFile: string, instance: string) { + constructor(MapKey : string, MapUrlFile: string, instance: string, key: string) { super({ - key: MapKey + key: key }); this.GameManager = gameManager; @@ -77,13 +125,12 @@ export class GameScene extends Phaser.Scene { this.MapKey = MapKey; this.MapUrlFile = MapUrlFile; - this.RoomId = this.instance + '__' + this.MapKey; + this.RoomId = this.instance + '__' + MapKey; } //hook preload scene preload(): void { - this.GameManager.setCurrentGameScene(this); - this.load.on('filecomplete-tilemapJSON-'+this.MapKey, (key: string, type: string, data: any) => { + this.load.on('filecomplete-tilemapJSON-'+this.MapKey, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); //TODO strategy to add access token @@ -91,12 +138,12 @@ export class GameScene extends Phaser.Scene { // If the map has already been loaded as part of another GameScene, the "on load" event will not be triggered. // In this case, we check in the cache to see if the map is here and trigger the event manually. if (this.cache.tilemap.exists(this.MapKey)) { - let data = this.cache.tilemap.get(this.MapKey); + const data = this.cache.tilemap.get(this.MapKey); this.onMapLoad(data); } //add player png - PLAYER_RESOURCES.forEach((playerResource: any) => { + PLAYER_RESOURCES.forEach((playerResource: PlayerResourceDescriptionInterface) => { this.load.spritesheet( playerResource.name, playerResource.img, @@ -105,13 +152,76 @@ export class GameScene extends Phaser.Scene { }); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); + + this.connectionPromise = Connection.createConnection(gameManager.getPlayerName(), gameManager.getCharacterSelected()).then((connection : Connection) => { + this.connection = connection; + + connection.onUserJoins((message: MessageUserJoined) => { + const userMessage: AddPlayerInterface = { + userId: message.userId, + character: message.character, + name: message.name, + position: message.position + } + this.addPlayer(userMessage); + }); + + connection.onUserMoved((message: MessageUserMovedInterface) => { + this.updatePlayerPosition(message); + }); + + connection.onUserLeft((userId: string) => { + this.removePlayer(userId); + }); + + connection.onGroupUpdatedOrCreated((groupPositionMessage: GroupCreatedUpdatedMessageInterface) => { + this.shareGroupPosition(groupPositionMessage); + }) + + connection.onGroupDeleted((groupId: string) => { + try { + this.deleteGroup(groupId); + } catch (e) { + console.error(e); + } + }) + + connection.onServerDisconnected(() => { + console.log('Player disconnected from server. Reloading scene.'); + + this.simplePeer.closeAllConnections(); + + const key = 'somekey'+Math.round(Math.random()*10000); + const game : Phaser.Scene = GameScene.createFromUrl(this.MapUrlFile, this.instance, key); + this.scene.add(key, game, true, + { + initPosition: { + x: this.CurrentPlayer.x, + y: this.CurrentPlayer.y + } + }); + + this.scene.stop(this.scene.key); + this.scene.remove(this.scene.key); + }) + + // When connection is performed, let's connect SimplePeer + this.simplePeer = new SimplePeer(this.connection); + + this.scene.wake(); + this.scene.sleep(ReconnectingSceneName); + + return connection; + }); } + // FIXME: we need to put a "unknown" instead of a "any" and validate the structure of the JSON we are receiving. + // eslint-disable-next-line @typescript-eslint/no-explicit-any private onMapLoad(data: any): void { // Triggered when the map is loaded // Load tiles attached to the map recursively this.mapFile = data.data; - let url = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); + const url = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); this.mapFile.tilesets.forEach((tileset) => { if (typeof tileset.name === 'undefined' || typeof tileset.image === 'undefined') { console.warn("Don't know how to handle tileset ", tileset) @@ -145,7 +255,7 @@ export class GameScene extends Phaser.Scene { //add layer on map this.Layers = new Array(); let depth = -2; - for (let layer of this.mapFile.layers) { + for (const layer of this.mapFile.layers) { if (layer.type === 'tilelayer') { this.addLayer(this.Map.createStaticLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); } @@ -160,23 +270,29 @@ export class GameScene extends Phaser.Scene { throw new Error('Your map MUST contain a layer of type "objectgroup" whose name is "floorLayer" that represents the layer characters are drawn at.'); } - // Now, let's find the start layer - if (this.startLayerName) { - for (let layer of this.mapFile.layers) { - if (this.startLayerName === layer.name && layer.type === 'tilelayer' && this.isStartLayer(layer)) { - let startPosition = this.startUser(layer); - this.startX = startPosition.x; - this.startY = startPosition.y; + // If there is an init position passed + if (this.initPosition !== null) { + this.startX = this.initPosition.x; + this.startY = this.initPosition.y; + } else { + // Now, let's find the start layer + if (this.startLayerName) { + for (const layer of this.mapFile.layers) { + if (this.startLayerName === layer.name && layer.type === 'tilelayer' && this.isStartLayer(layer)) { + const startPosition = this.startUser(layer); + this.startX = startPosition.x; + this.startY = startPosition.y; + } } } - } - if (this.startX === undefined) { - // If we have no start layer specified or if the hash passed does not exist, let's go with the default start position. - for (let layer of this.mapFile.layers) { - if (layer.type === 'tilelayer' && layer.name === "start") { - let startPosition = this.startUser(layer); - this.startX = startPosition.x; - this.startY = startPosition.y; + if (this.startX === undefined) { + // If we have no start layer specified or if the hash passed does not exist, let's go with the default start position. + for (const layer of this.mapFile.layers) { + if (layer.type === 'tilelayer' && layer.name === "start") { + const startPosition = this.startUser(layer); + this.startX = startPosition.x; + this.startY = startPosition.y; + } } } } @@ -205,12 +321,12 @@ export class GameScene extends Phaser.Scene { // Let's generate the circle for the group delimiter - let circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === 'circleSprite'); + const circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === 'circleSprite'); if(circleElement) { this.textures.remove('circleSprite'); } this.circleTexture = this.textures.createCanvas('circleSprite', 96, 96); - let context = this.circleTexture.context; + const context = this.circleTexture.context; context.beginPath(); context.arc(48, 48, 48, 0, 2 * Math.PI, false); // context.lineWidth = 5; @@ -219,12 +335,23 @@ export class GameScene extends Phaser.Scene { this.circleTexture.refresh(); // Let's alter browser history - let url = new URL(this.MapUrlFile); + const url = new URL(this.MapUrlFile); let path = '/_/'+this.instance+'/'+url.host+url.pathname; if (this.startLayerName) { path += '#'+this.startLayerName; } window.history.pushState({}, 'WorkAdventure', path); + + // Let's pause the scene if the connection is not established yet + if (this.connection === undefined) { + // Let's wait 0.5 seconds before printing the "connecting" screen to avoid blinking + setTimeout(() => { + if (this.connection === undefined) { + this.scene.sleep(); + this.scene.launch(ReconnectingSceneName); + } + }, 500); + } } private getExitSceneUrl(layer: ITiledMapLayer): string|undefined { @@ -240,11 +367,11 @@ export class GameScene extends Phaser.Scene { } private getProperty(layer: ITiledMapLayer, name: string): string|boolean|number|undefined { - let properties : any = layer.properties; + const properties = layer.properties; if (!properties) { return undefined; } - let obj = properties.find((property:any) => property.name === name); + const obj = properties.find((property: ITiledMapLayerProperty) => property.name === name); if (obj === undefined) { return undefined; } @@ -259,7 +386,7 @@ export class GameScene extends Phaser.Scene { * @param tileHeight */ private loadNextGame(layer: ITiledMapLayer, mapWidth: number, tileWidth: number, tileHeight: number){ - let exitSceneUrl = this.getExitSceneUrl(layer); + const exitSceneUrl = this.getExitSceneUrl(layer); if (exitSceneUrl === undefined) { throw new Error('Layer is not an exit scene layer.'); } @@ -269,18 +396,18 @@ export class GameScene extends Phaser.Scene { } // TODO: eventually compute a relative URL - let absoluteExitSceneUrl = new URL(exitSceneUrl, this.MapUrlFile).href; - let exitSceneKey = gameManager.loadMap(absoluteExitSceneUrl, this.scene, instance); + const absoluteExitSceneUrl = new URL(exitSceneUrl, this.MapUrlFile).href; + const exitSceneKey = gameManager.loadMap(absoluteExitSceneUrl, this.scene, instance); - let tiles : number[] = layer.data as number[]; + const tiles : number[] = layer.data as number[]; for (let key=0; key < tiles.length; key++) { - let objectKey = tiles[key]; + const objectKey = tiles[key]; if(objectKey === 0){ continue; } //key + 1 because the start x = 0; - let y : number = parseInt(((key + 1) / mapWidth).toString()); - let x : number = key - (y * mapWidth); + const y : number = parseInt(((key + 1) / mapWidth).toString()); + const x : number = key - (y * mapWidth); let hash = new URL(exitSceneUrl, this.MapUrlFile).hash; if (hash) { @@ -288,15 +415,13 @@ export class GameScene extends Phaser.Scene { } //push and save switching case - // TODO: this is not efficient. We should refactor that to enable a search by key. For instance: this.PositionNextScene[y][x] = exitSceneKey - this.PositionNextScene.push({ - xStart: (x * tileWidth), - yStart: (y * tileWidth), - xEnd: ((x +1) * tileHeight), - yEnd: ((y + 1) * tileHeight), + if (this.PositionNextScene[y] === undefined) { + this.PositionNextScene[y] = new Array<{key: string, hash: string}>(); + } + this.PositionNextScene[y][x] = { key: exitSceneKey, hash - }) + } } } @@ -304,23 +429,17 @@ export class GameScene extends Phaser.Scene { * @param layer */ private startUser(layer: ITiledMapLayer): PositionInterface { - if (this.initPosition !== null) { - this.startX = this.initPosition.x; - this.startY = this.initPosition.y; - return { - x: this.initPosition.x, - y: this.initPosition.y - }; + const tiles = layer.data; + if (typeof(tiles) === 'string') { + throw new Error('The content of a JSON map must be filled as a JSON array, not as a string'); } - - let tiles : any = layer.data; - let possibleStartPositions : PositionInterface[] = []; + const possibleStartPositions : PositionInterface[] = []; tiles.forEach((objectKey : number, key: number) => { if(objectKey === 0){ return; } - let y = Math.floor(key / layer.width); - let x = key % layer.width; + const y = Math.floor(key / layer.width); + const x = key % layer.width; possibleStartPositions.push({x: x*32, y: y*32}); }); @@ -350,7 +469,7 @@ export class GameScene extends Phaser.Scene { createCollisionWithPlayer() { //add collision layer this.Layers.forEach((Layer: Phaser.Tilemaps.StaticTilemapLayer) => { - this.physics.add.collider(this.CurrentPlayer, Layer, (object1: any, object2: any) => { + this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => { //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); Layer.setCollisionByProperty({collides: true}); @@ -366,11 +485,11 @@ export class GameScene extends Phaser.Scene { } createCollisionObject(){ - this.Objects.forEach((Object : Phaser.Physics.Arcade.Sprite) => { - this.physics.add.collider(this.CurrentPlayer, Object, (object1: any, object2: any) => { - //this.CurrentPlayer.say("Collision with object : " + (object2 as Phaser.Physics.Arcade.Sprite).texture.key) + /*this.Objects.forEach((Object : Phaser.Physics.Arcade.Sprite) => { + this.physics.add.collider(this.CurrentPlayer, Object, (object1, object2) => { + this.CurrentPlayer.say("Collision with object : " + (object2 as Phaser.Physics.Arcade.Sprite).texture.key) }); - }) + })*/ } createCurrentPlayer(){ @@ -391,10 +510,14 @@ export class GameScene extends Phaser.Scene { this.createCollisionObject(); //join room - this.GameManager.joinRoom(this.RoomId, this.startX, this.startY, PlayerAnimationNames.WalkDown, false); + this.connectionPromise.then((connection: Connection) => { + connection.joinARoom(this.RoomId, this.startX, this.startY, PlayerAnimationNames.WalkDown, false).then((userPositions: MessageUserPositionInterface[]) => { + this.initUsersPosition(userPositions); + }); - //listen event to share position of user - this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)) + //listen event to share position of user + this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)) + }); } pushPlayerPosition(event: HasMovedEvent) { @@ -426,18 +549,18 @@ export class GameScene extends Phaser.Scene { private doPushPlayerPosition(event: HasMovedEvent): void { this.lastMoveEventSent = event; this.lastSentTick = this.currentTick; - this.GameManager.pushPlayerPosition(event); + this.connection.sharePosition(event.x, event.y, event.direction, event.moving); } EventToClickOnTile(){ // debug code to get a tile properties by clicking on it - this.input.on("pointerdown", (pointer: Phaser.Input.Pointer)=>{ + /*this.input.on("pointerdown", (pointer: Phaser.Input.Pointer)=>{ //pixel position toz tile position - let tile = this.Map.getTileAt(this.Map.worldToTileX(pointer.worldX), this.Map.worldToTileY(pointer.worldY)); + const tile = this.Map.getTileAt(this.Map.worldToTileX(pointer.worldX), this.Map.worldToTileY(pointer.worldY)); if(tile){ this.CurrentPlayer.say("Your touch " + tile.layer.name); } - }); + });*/ } /** @@ -448,19 +571,46 @@ export class GameScene extends Phaser.Scene { this.currentTick = time; this.CurrentPlayer.moveUser(delta); + // Let's handle all events + while (this.pendingEvents.length !== 0) { + const event = this.pendingEvents.dequeue(); + switch (event.type) { + case "InitUserPositionEvent": + this.doInitUsersPosition(event.event); + break; + case "AddPlayerEvent": + this.doAddPlayer(event.event); + break; + case "RemovePlayerEvent": + this.doRemovePlayer(event.userId); + break; + case "UserMovedEvent": + this.doUpdatePlayerPosition(event.event); + break; + case "GroupCreatedUpdatedEvent": + this.doShareGroupPosition(event.event); + break; + case "DeleteGroupEvent": + this.doDeleteGroup(event.groupId); + break; + } + } + // Let's move all users - let updatedPlayersPositions = this.playersPositionInterpolator.getUpdatedPositions(time); + const updatedPlayersPositions = this.playersPositionInterpolator.getUpdatedPositions(time); updatedPlayersPositions.forEach((moveEvent: HasMovedEvent, userId: string) => { - let player : RemotePlayer | undefined = this.MapPlayersByKey.get(userId); + const player : RemotePlayer | undefined = this.MapPlayersByKey.get(userId); if (player === undefined) { throw new Error('Cannot find player with ID "' + userId +'"'); } player.updatePosition(moveEvent); }); - let nextSceneKey = this.checkToExit(); + const nextSceneKey = this.checkToExit(); if(nextSceneKey){ // We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map. + this.connection.closeConnection(); + this.scene.stop(); this.scene.remove(this.scene.key); this.scene.start(nextSceneKey.key, { startLayerName: nextSceneKey.hash @@ -471,23 +621,33 @@ export class GameScene extends Phaser.Scene { /** * */ - checkToExit(){ - if(this.PositionNextScene.length === 0){ + checkToExit(): {key: string, hash: string} | null { + const x = Math.floor(this.CurrentPlayer.x / 32); + const y = Math.floor(this.CurrentPlayer.y / 32); + + if (this.PositionNextScene[y] !== undefined && this.PositionNextScene[y][x] !== undefined) { + return this.PositionNextScene[y][x]; + } else { return null; } - return this.PositionNextScene.find((position : any) => { - return position.xStart <= this.CurrentPlayer.x && this.CurrentPlayer.x <= position.xEnd - && position.yStart <= this.CurrentPlayer.y && this.CurrentPlayer.y <= position.yEnd - }) } - public initUsersPosition(usersPosition: MessageUserPositionInterface[]): void { - if(!this.CurrentPlayer){ - console.error('Cannot initiate users list because map is not loaded yet') - return; - } + /** + * Called by the connexion when the full list of user position is received. + */ + private initUsersPosition(usersPosition: MessageUserPositionInterface[]): void { + this.pendingEvents.enqueue({ + type: "InitUserPositionEvent", + event: usersPosition + }); - let currentPlayerId = this.GameManager.getPlayerId(); + } + + /** + * Put all the players on the map on map load. + */ + private doInitUsersPosition(usersPosition: MessageUserPositionInterface[]): void { + const currentPlayerId = this.connection.getUserId(); // clean map this.MapPlayersByKey.forEach((player: RemotePlayer) => { @@ -505,10 +665,20 @@ export class GameScene extends Phaser.Scene { }); } + /** + * Called by the connexion when a new player arrives on a map + */ + public addPlayer(addPlayerData : AddPlayerInterface) : void { + this.pendingEvents.enqueue({ + type: "AddPlayerEvent", + event: addPlayerData + }); + } + /** * Create new player */ - public addPlayer(addPlayerData : AddPlayerInterface) : void{ + private doAddPlayer(addPlayerData : AddPlayerInterface) : void { //check if exist player, if exist, move position if(this.MapPlayersByKey.has(addPlayerData.userId)){ this.updatePlayerPosition({ @@ -518,7 +688,7 @@ export class GameScene extends Phaser.Scene { return; } //initialise player - let player = new RemotePlayer( + const player = new RemotePlayer( addPlayerData.userId, this, addPlayerData.position.x, @@ -538,9 +708,18 @@ export class GameScene extends Phaser.Scene { });*/ } + /** + * Called by the connexion when a player is removed from the map + */ public removePlayer(userId: string) { - console.log('Removing player ', userId) - let player = this.MapPlayersByKey.get(userId); + this.pendingEvents.enqueue({ + type: "RemovePlayerEvent", + userId + }); + } + + private doRemovePlayer(userId: string) { + const player = this.MapPlayersByKey.get(userId); if (player === undefined) { console.error('Cannot find user with id ', userId); } else { @@ -551,27 +730,43 @@ export class GameScene extends Phaser.Scene { this.playersPositionInterpolator.removePlayer(userId); } - updatePlayerPosition(message: MessageUserMovedInterface): void { - let player : RemotePlayer | undefined = this.MapPlayersByKey.get(message.userId); + public updatePlayerPosition(message: MessageUserMovedInterface): void { + this.pendingEvents.enqueue({ + type: "UserMovedEvent", + event: message + }); + } + + private doUpdatePlayerPosition(message: MessageUserMovedInterface): void { + const player : RemotePlayer | undefined = this.MapPlayersByKey.get(message.userId); if (player === undefined) { - throw new Error('Cannot find player with ID "' + message.userId +'"'); + //throw new Error('Cannot find player with ID "' + message.userId +'"'); + console.error('Cannot update position of player with ID "' + message.userId +'": player not found'); + return; } // We do not update the player position directly (because it is sent only every 200ms). // Instead we use the PlayersPositionInterpolator that will do a smooth animation over the next 200ms. - let playerMovement = new PlayerMovement({ x: player.x, y: player.y }, this.currentTick, message.position, this.currentTick + POSITION_DELAY); + const playerMovement = new PlayerMovement({ x: player.x, y: player.y }, this.currentTick, message.position, this.currentTick + POSITION_DELAY); this.playersPositionInterpolator.updatePlayerPosition(player.userId, playerMovement); } - shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) { - let groupId = groupPositionMessage.groupId; + public shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) { + this.pendingEvents.enqueue({ + type: "GroupCreatedUpdatedEvent", + event: groupPositionMessage + }); + } - let group = this.groups.get(groupId); + private doShareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) { + const groupId = groupPositionMessage.groupId; + + const group = this.groups.get(groupId); if (group !== undefined) { group.setPosition(Math.round(groupPositionMessage.position.x), Math.round(groupPositionMessage.position.y)); } else { // TODO: circle radius should not be hard stored - let sprite = new Sprite( + const sprite = new Sprite( this, Math.round(groupPositionMessage.position.x), Math.round(groupPositionMessage.position.y), @@ -583,7 +778,14 @@ export class GameScene extends Phaser.Scene { } deleteGroup(groupId: string): void { - let group = this.groups.get(groupId); + this.pendingEvents.enqueue({ + type: "DeleteGroupEvent", + groupId + }); + } + + doDeleteGroup(groupId: string): void { + const group = this.groups.get(groupId); if(!group){ return; } @@ -593,8 +795,8 @@ export class GameScene extends Phaser.Scene { public static getMapKeyByUrl(mapUrlStart: string) : string { // FIXME: the key should be computed from the full URL of the map. - let startPos = mapUrlStart.indexOf('://')+3; - let endPos = mapUrlStart.indexOf(".json"); + const startPos = mapUrlStart.indexOf('://')+3; + const endPos = mapUrlStart.indexOf(".json"); return mapUrlStart.substring(startPos, endPos); } } diff --git a/front/src/Phaser/Game/PlayerMovement.ts b/front/src/Phaser/Game/PlayerMovement.ts index 1ed2b745..1458335d 100644 --- a/front/src/Phaser/Game/PlayerMovement.ts +++ b/front/src/Phaser/Game/PlayerMovement.ts @@ -23,8 +23,8 @@ export class PlayerMovement { return this.endPosition; } - let x = (this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.x; - let y = (this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.y; + const x = (this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.x; + const y = (this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.y; return { x, diff --git a/front/src/Phaser/Game/PlayersPositionInterpolator.ts b/front/src/Phaser/Game/PlayersPositionInterpolator.ts index 19e0f7bc..080c8a17 100644 --- a/front/src/Phaser/Game/PlayersPositionInterpolator.ts +++ b/front/src/Phaser/Game/PlayersPositionInterpolator.ts @@ -17,7 +17,7 @@ export class PlayersPositionInterpolator { } getUpdatedPositions(tick: number) : Map { - let positions = new Map(); + const positions = new Map(); this.playerMovements.forEach((playerMovement: PlayerMovement, userId: string) => { if (playerMovement.isOutdated(tick)) { //console.log("outdated") diff --git a/front/src/Phaser/Login/LoginScene.ts b/front/src/Phaser/Login/LoginScene.ts index 1b7ef76f..5177659b 100644 --- a/front/src/Phaser/Login/LoginScene.ts +++ b/front/src/Phaser/Login/LoginScene.ts @@ -4,7 +4,7 @@ import {TextInput} from "../Components/TextInput"; import {ClickButton} from "../Components/ClickButton"; import Image = Phaser.GameObjects.Image; import Rectangle = Phaser.GameObjects.Rectangle; -import {PLAYER_RESOURCES} from "../Entity/Character"; +import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "../Entity/Character"; import {cypressAsserter} from "../../Cypress/CypressAsserter"; import {SelectCharacterSceneInitDataInterface, SelectCharacterSceneName} from "./SelectCharacterScene"; @@ -40,7 +40,7 @@ export class LoginScene extends Phaser.Scene { this.load.bitmapFont(LoginTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); cypressAsserter.preloadFinished(); //add player png - PLAYER_RESOURCES.forEach((playerResource: any) => { + PLAYER_RESOURCES.forEach((playerResource: PlayerResourceDescriptionInterface) => { this.load.spritesheet( playerResource.name, playerResource.img, @@ -67,7 +67,7 @@ export class LoginScene extends Phaser.Scene { this.logo = new Image(this, this.game.renderer.width - 30, this.game.renderer.height - 20, LoginTextures.icon); this.add.existing(this.logo); - let infoText = "Commands: \n - Arrows or Z,Q,S,D to move\n - SHIFT to run"; + const infoText = "Commands: \n - Arrows or Z,Q,S,D to move\n - SHIFT to run"; this.infoTextField = new TextField(this, 10, this.game.renderer.height - 35, infoText); this.input.keyboard.on('keyup-ENTER', () => { diff --git a/front/src/Phaser/Login/SelectCharacterScene.ts b/front/src/Phaser/Login/SelectCharacterScene.ts index dab9d61f..5175a7b8 100644 --- a/front/src/Phaser/Login/SelectCharacterScene.ts +++ b/front/src/Phaser/Login/SelectCharacterScene.ts @@ -3,8 +3,9 @@ import {TextField} from "../Components/TextField"; import {ClickButton} from "../Components/ClickButton"; import Image = Phaser.GameObjects.Image; import Rectangle = Phaser.GameObjects.Rectangle; -import {PLAYER_RESOURCES} from "../Entity/Character"; +import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "../Entity/Character"; import {GameSceneInitInterface} from "../Game/GameScene"; +import {StartMapInterface} from "../../Connection"; //todo: put this constants in a dedicated file export const SelectCharacterSceneName = "SelectCharacterScene"; @@ -47,7 +48,7 @@ export class SelectCharacterScene extends Phaser.Scene { // Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap this.load.bitmapFont(LoginTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); //add player png - PLAYER_RESOURCES.forEach((playerResource: any) => { + PLAYER_RESOURCES.forEach((playerResource: PlayerResourceDescriptionInterface) => { this.load.spritesheet( playerResource.name, playerResource.img, @@ -63,7 +64,7 @@ export class SelectCharacterScene extends Phaser.Scene { this.pressReturnField = new TextField(this, this.game.renderer.width / 2, 230, 'Press enter to start'); this.pressReturnField.setOrigin(0.5).setCenterAlign() - let rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16; + const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16; this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xFFFFFF); @@ -103,8 +104,8 @@ export class SelectCharacterScene extends Phaser.Scene { this.createCurrentPlayer(); if (window.localStorage) { - let playerNumberStr: string = window.localStorage.getItem('selectedPlayer') ?? '0'; - let playerNumber: number = Number(playerNumberStr); + const playerNumberStr: string = window.localStorage.getItem('selectedPlayer') ?? '0'; + const playerNumber: number = Number(playerNumberStr); this.selectedRectangleXPos = playerNumber % this.nbCharactersPerRow; this.selectedRectangleYPos = Math.floor(playerNumber / this.nbCharactersPerRow); this.updateSelectedPlayer(); @@ -115,63 +116,60 @@ export class SelectCharacterScene extends Phaser.Scene { this.pressReturnField.setVisible(!!(Math.floor(time / 500) % 2)); } - private async login(name: string) { - return gameManager.connect(name, this.selectedPlayer.texture.key).then(() => { - // Do we have a start URL in the address bar? If so, let's redirect to this address - let instanceAndMapUrl = this.findMapUrl(); - if (instanceAndMapUrl !== null) { - let [mapUrl, instance] = instanceAndMapUrl; - let key = gameManager.loadMap(mapUrl, this.scene, instance); - this.scene.start(key, { - startLayerName: window.location.hash ? window.location.hash.substr(1) : undefined - } as GameSceneInitInterface); - return mapUrl; - } else { - // If we do not have a map address in the URL, let's ask the server for a start map. - return gameManager.loadStartMap().then((scene : any) => { - if (!scene) { - return; - } - let key = gameManager.loadMap(window.location.protocol + "//" + scene.mapUrlStart, this.scene, scene.startInstance); - this.scene.start(key); - return scene; - }).catch((err) => { - console.error(err); - throw err; - }); - } - }).catch((err) => { - console.error(err); - throw err; - }); + private async login(name: string): Promise { + gameManager.storePlayerDetails(name, this.selectedPlayer.texture.key); + + // Do we have a start URL in the address bar? If so, let's redirect to this address + const instanceAndMapUrl = this.findMapUrl(); + if (instanceAndMapUrl !== null) { + const [mapUrl, instance] = instanceAndMapUrl; + const key = gameManager.loadMap(mapUrl, this.scene, instance); + this.scene.start(key, { + startLayerName: window.location.hash ? window.location.hash.substr(1) : undefined + } as GameSceneInitInterface); + return { + mapUrlStart: mapUrl, + startInstance: instance + }; + } else { + // If we do not have a map address in the URL, let's ask the server for a start map. + return gameManager.loadStartMap().then((startMap: StartMapInterface) => { + const key = gameManager.loadMap(window.location.protocol + "//" + startMap.mapUrlStart, this.scene, startMap.startInstance); + this.scene.start(key); + return startMap; + }).catch((err) => { + console.error(err); + throw err; + }); + } } /** * Returns the map URL and the instance from the current URL */ private findMapUrl(): [string, string]|null { - let path = window.location.pathname; + const path = window.location.pathname; if (!path.startsWith('/_/')) { return null; } - let instanceAndMap = path.substr(3); - let firstSlash = instanceAndMap.indexOf('/'); + const instanceAndMap = path.substr(3); + const firstSlash = instanceAndMap.indexOf('/'); if (firstSlash === -1) { return null; } - let instance = instanceAndMap.substr(0, firstSlash); + const instance = instanceAndMap.substr(0, firstSlash); return [window.location.protocol+'//'+instanceAndMap.substr(firstSlash+1), instance]; } createCurrentPlayer(): void { for (let i = 0; i = new Map(); + get(event: UserInputEvent): boolean { - return this.KeysCode[event] || false; + return this.KeysCode.get(event) || false; } - set(event: UserInputEvent, value: boolean): boolean { - return this.KeysCode[event] = true; + set(event: UserInputEvent, value: boolean): void { + this.KeysCode.set(event, value); } } @@ -55,7 +52,7 @@ export class UserInputManager { } getEventListForGameTick(): ActiveEventList { - let eventsMap = new ActiveEventList(); + const eventsMap = new ActiveEventList(); this.KeysCode.forEach(d => { if (d. keyInstance.isDown) { eventsMap.set(d.event, true); diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index 359eac67..03736e6e 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -1,53 +1,53 @@ -const videoConstraint: {width : any, height: any, facingMode : string} = { +const videoConstraint: boolean|MediaTrackConstraints = { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: "user" }; export class MediaManager { localStream: MediaStream|null = null; - remoteVideo: Array = new Array(); + private remoteVideo: Map = new Map(); myCamVideo: HTMLVideoElement; - cinemaClose: any = null; - cinema: any = null; - microphoneClose: any = null; - microphone: any = null; + cinemaClose: HTMLImageElement; + cinema: HTMLImageElement; + microphoneClose: HTMLImageElement; + microphone: HTMLImageElement; webrtcInAudio: HTMLAudioElement; - constraintsMedia : {audio : any, video : any} = { + constraintsMedia : MediaStreamConstraints = { audio: true, video: videoConstraint }; - updatedLocalStreamCallBack : Function; + updatedLocalStreamCallBack : (media: MediaStream) => void; - constructor(updatedLocalStreamCallBack : Function) { + constructor(updatedLocalStreamCallBack : (media: MediaStream) => void) { this.updatedLocalStreamCallBack = updatedLocalStreamCallBack; this.myCamVideo = this.getElementByIdOrFail('myCamVideo'); this.webrtcInAudio = this.getElementByIdOrFail('audio-webrtc-in'); this.webrtcInAudio.volume = 0.2; - this.microphoneClose = document.getElementById('microphone-close'); + this.microphoneClose = this.getElementByIdOrFail('microphone-close'); this.microphoneClose.style.display = "none"; - this.microphoneClose.addEventListener('click', (e: any) => { + this.microphoneClose.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); this.enabledMicrophone(); //update tracking }); - this.microphone = document.getElementById('microphone'); - this.microphone.addEventListener('click', (e: any) => { + this.microphone = this.getElementByIdOrFail('microphone'); + this.microphone.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); this.disabledMicrophone(); //update tracking }); - this.cinemaClose = document.getElementById('cinema-close'); + this.cinemaClose = this.getElementByIdOrFail('cinema-close'); this.cinemaClose.style.display = "none"; - this.cinemaClose.addEventListener('click', (e: any) => { + this.cinemaClose.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); this.enabledCamera(); //update tracking }); - this.cinema = document.getElementById('cinema'); - this.cinema.addEventListener('click', (e: any) => { + this.cinema = this.getElementByIdOrFail('cinema'); + this.cinema.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); this.disabledCamera(); //update tracking @@ -55,7 +55,7 @@ export class MediaManager { } activeVisio(){ - let webRtc = this.getElementByIdOrFail('webRtc'); + const webRtc = this.getElementByIdOrFail('webRtc'); webRtc.classList.add('active'); } @@ -63,7 +63,7 @@ export class MediaManager { this.cinemaClose.style.display = "none"; this.cinema.style.display = "block"; this.constraintsMedia.video = videoConstraint; - this.getCamera().then((stream) => { + this.getCamera().then((stream: MediaStream) => { this.updatedLocalStreamCallBack(stream); }); } @@ -107,8 +107,13 @@ export class MediaManager { } //get camera - getCamera() { + getCamera(): Promise { let promise = null; + + if (navigator.mediaDevices === undefined) { + return Promise.reject(new Error('Unable to access your camera or microphone. Your browser is too old (or you are running a development version of WorkAdventure on Firefox)')); + } + try { promise = navigator.mediaDevices.getUserMedia(this.constraintsMedia) .then((stream: MediaStream) => { @@ -123,11 +128,12 @@ export class MediaManager { return stream; }).catch((err) => { - console.info(`error get media {video: ${this.constraintsMedia.video}},{audio: ${this.constraintsMedia.audio}}`,err); + console.info("error get media ", this.constraintsMedia.video, this.constraintsMedia.audio, err); this.localStream = null; + throw err; }); } catch (e) { - promise = Promise.reject(false); + promise = Promise.reject(e); } return promise; } @@ -138,9 +144,9 @@ export class MediaManager { */ addActiveVideo(userId : string, userName: string = ""){ this.webrtcInAudio.play(); - let elementRemoteVideo = this.getElementByIdOrFail("activeCam"); + const elementRemoteVideo = this.getElementByIdOrFail("activeCam"); userName = userName.toUpperCase(); - let color = this.getColorByString(userName); + const color = this.getColorByString(userName); elementRemoteVideo.insertAdjacentHTML('beforeend', `
@@ -150,7 +156,7 @@ export class MediaManager {
`); - this.remoteVideo[(userId as any)] = document.getElementById(userId); + this.remoteVideo.set(userId, this.getElementByIdOrFail(userId)); } /** @@ -158,7 +164,7 @@ export class MediaManager { * @param userId */ disabledMicrophoneByUserId(userId: string){ - let element = document.getElementById(`microphone-${userId}`); + const element = document.getElementById(`microphone-${userId}`); if(!element){ return; } @@ -170,7 +176,7 @@ export class MediaManager { * @param userId */ enabledMicrophoneByUserId(userId: string){ - let element = document.getElementById(`microphone-${userId}`); + const element = document.getElementById(`microphone-${userId}`); if(!element){ return; } @@ -215,7 +221,12 @@ export class MediaManager { * @param stream */ addStreamRemoteVideo(userId : string, stream : MediaStream){ - this.remoteVideo[(userId as any)].srcObject = stream; + const remoteVideo = this.remoteVideo.get(userId); + if (remoteVideo === undefined) { + console.error('Unable to find video for ', userId); + return; + } + remoteVideo.srcObject = stream; } /** @@ -223,15 +234,16 @@ export class MediaManager { * @param userId */ removeActiveVideo(userId : string){ - let element = document.getElementById(`div-${userId}`); + const element = document.getElementById(`div-${userId}`); if(!element){ return; } element.remove(); + this.remoteVideo.delete(userId); } isConnecting(userId : string): void { - let connectingSpinnerDiv = this.getSpinner(userId); + const connectingSpinnerDiv = this.getSpinner(userId); if (connectingSpinnerDiv === null) { return; } @@ -239,7 +251,7 @@ export class MediaManager { } isConnected(userId : string): void { - let connectingSpinnerDiv = this.getSpinner(userId); + const connectingSpinnerDiv = this.getSpinner(userId); if (connectingSpinnerDiv === null) { return; } @@ -247,11 +259,11 @@ export class MediaManager { } isError(userId : string): void { - let element = document.getElementById(`div-${userId}`); + const element = document.getElementById(`div-${userId}`); if(!element){ return; } - let errorDiv = element.getElementsByClassName('rtc-error').item(0) as HTMLDivElement|null; + const errorDiv = element.getElementsByClassName('rtc-error').item(0) as HTMLDivElement|null; if (errorDiv === null) { return; } @@ -259,11 +271,11 @@ export class MediaManager { } private getSpinner(userId : string): HTMLDivElement|null { - let element = document.getElementById(`div-${userId}`); + const element = document.getElementById(`div-${userId}`); if(!element){ return null; } - let connnectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null; + const connnectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null; return connnectingSpinnerDiv; } @@ -280,14 +292,14 @@ export class MediaManager { } let color = '#'; for (let i = 0; i < 3; i++) { - let value = (hash >> (i * 8)) & 255; + const value = (hash >> (i * 8)) & 255; color += ('00' + value.toString(16)).substr(-2); } return color; } private getElementByIdOrFail(id: string): T { - let elem = document.getElementById(id); + const elem = document.getElementById(id); if (elem === null) { throw new Error("Cannot find HTML element with id '"+id+"'"); } diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index da1ae3db..381b3ac2 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -1,7 +1,12 @@ -import {ConnectionInterface, WebRtcDisconnectMessageInterface, WebRtcStartMessageInterface} from "../Connection"; +import { + Connection, + WebRtcDisconnectMessageInterface, + WebRtcSignalMessageInterface, + WebRtcStartMessageInterface +} from "../Connection"; import {MediaManager} from "./MediaManager"; import * as SimplePeerNamespace from "simple-peer"; -let Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); +const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); export interface UserSimplePeer{ userId: string; @@ -13,7 +18,7 @@ export interface UserSimplePeer{ * This class manages connections to all the peers in the same group as me. */ export class SimplePeer { - private Connection: ConnectionInterface; + private Connection: Connection; private WebRtcRoomId: string; private Users: Array = new Array(); @@ -21,7 +26,7 @@ export class SimplePeer { private PeerConnectionArray: Map = new Map(); - constructor(Connection: ConnectionInterface, WebRtcRoomId: string = "test-webrtc") { + constructor(Connection: Connection, WebRtcRoomId: string = "test-webrtc") { this.Connection = Connection; this.WebRtcRoomId = WebRtcRoomId; this.MediaManager = new MediaManager((stream : MediaStream) => { @@ -36,7 +41,7 @@ export class SimplePeer { private initialise() { //receive signal by gemer - this.Connection.receiveWebrtcSignal((message: any) => { + this.Connection.receiveWebrtcSignal((message: WebRtcSignalMessageInterface) => { this.receiveWebrtcSignal(message); }); @@ -95,7 +100,7 @@ export class SimplePeer { let name = user.name; if(!name){ - let userSearch = this.Users.find((userSearch: UserSimplePeer) => userSearch.userId === user.userId); + const userSearch = this.Users.find((userSearch: UserSimplePeer) => userSearch.userId === user.userId); if(userSearch) { name = userSearch.name; } @@ -103,7 +108,7 @@ export class SimplePeer { this.MediaManager.removeActiveVideo(user.userId); this.MediaManager.addActiveVideo(user.userId, name); - let peer : SimplePeerNamespace.Instance = new Peer({ + const peer : SimplePeerNamespace.Instance = new Peer({ initiator: user.initiator ? user.initiator : false, reconnectTimer: 10000, config: { @@ -122,7 +127,7 @@ export class SimplePeer { this.PeerConnectionArray.set(user.userId, peer); //start listen signal for the peer connection - peer.on('signal', (data: any) => { + peer.on('signal', (data: unknown) => { this.sendWebrtcSignal(data, user.userId); }); @@ -159,6 +164,7 @@ export class SimplePeer { this.closeConnection(user.userId); }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any peer.on('error', (err: any) => { console.error(`error => ${user.userId} => ${err.code}`, err); this.MediaManager.isError(user.userId); @@ -170,7 +176,7 @@ export class SimplePeer { }); peer.on('data', (chunk: Buffer) => { - let data = JSON.parse(chunk.toString('utf8')); + const data = JSON.parse(chunk.toString('utf8')); if(data.type === "stream"){ this.stream(user.userId, data.stream); } @@ -187,7 +193,7 @@ export class SimplePeer { private closeConnection(userId : string) { try { this.MediaManager.removeActiveVideo(userId); - let peer = this.PeerConnectionArray.get(userId); + const peer = this.PeerConnectionArray.get(userId); if (peer === undefined) { console.warn("Tried to close connection for user "+userId+" but could not find user") return; @@ -203,12 +209,18 @@ export class SimplePeer { } } + public closeAllConnections() { + for (const userId of this.PeerConnectionArray.keys()) { + this.closeConnection(userId); + } + } + /** * * @param userId * @param data */ - private sendWebrtcSignal(data: any, userId : string) { + private sendWebrtcSignal(data: unknown, userId : string) { try { this.Connection.sendWebrtcSignal(data, this.WebRtcRoomId, null, userId); }catch (e) { @@ -216,13 +228,14 @@ export class SimplePeer { } } - private receiveWebrtcSignal(data: any) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private receiveWebrtcSignal(data: WebRtcSignalMessageInterface) { try { //if offer type, create peer connection if(data.signal.type === "offer"){ this.createPeerConnection(data); } - let peer = this.PeerConnectionArray.get(data.userId); + const peer = this.PeerConnectionArray.get(data.userId); if (peer !== undefined) { peer.signal(data.signal); } else { @@ -251,10 +264,10 @@ export class SimplePeer { * * @param userId */ - private addMedia (userId : any = null) { + private addMedia (userId : string) { try { - let localStream: MediaStream|null = this.MediaManager.localStream; - let peer = this.PeerConnectionArray.get(userId); + const localStream: MediaStream|null = this.MediaManager.localStream; + const peer = this.PeerConnectionArray.get(userId); if(localStream === null) { //send fake signal if(peer === undefined){ diff --git a/front/src/index.ts b/front/src/index.ts index 843925ac..6221d3ad 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -24,7 +24,7 @@ const config: GameConfig = { cypressAsserter.gameStarted(); -let game = new Phaser.Game(config); +const game = new Phaser.Game(config); window.addEventListener('resize', function (event) { game.scale.resize(window.innerWidth / RESOLUTION, window.innerHeight / RESOLUTION); diff --git a/front/tests/Phaser/Game/PlayerMovementTest.ts b/front/tests/Phaser/Game/PlayerMovementTest.ts index e65dbec8..ce2e2767 100644 --- a/front/tests/Phaser/Game/PlayerMovementTest.ts +++ b/front/tests/Phaser/Game/PlayerMovementTest.ts @@ -3,7 +3,7 @@ import {PlayerMovement} from "../../../src/Phaser/Game/PlayerMovement"; describe("Interpolation / Extrapolation", () => { it("should interpolate", () => { - let playerMovement = new PlayerMovement({ + const playerMovement = new PlayerMovement({ x: 100, y: 200 }, 42000, { @@ -39,7 +39,7 @@ describe("Interpolation / Extrapolation", () => { }); it("should not extrapolate if we stop", () => { - let playerMovement = new PlayerMovement({ + const playerMovement = new PlayerMovement({ x: 100, y: 200 }, 42000, { @@ -57,7 +57,7 @@ describe("Interpolation / Extrapolation", () => { }); it("should should keep moving until it stops", () => { - let playerMovement = new PlayerMovement({ + const playerMovement = new PlayerMovement({ x: 100, y: 200 }, 42000, { diff --git a/front/tsconfig.json b/front/tsconfig.json index 84882e74..1661efa2 100644 --- a/front/tsconfig.json +++ b/front/tsconfig.json @@ -6,6 +6,7 @@ "noImplicitAny": true, "module": "CommonJS", "target": "es5", + "downlevelIteration": true, "jsx": "react", "allowJs": true, diff --git a/front/yarn.lock b/front/yarn.lock index 7ed8c19a..126536e0 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -2602,6 +2602,11 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +linked-list-typescript@^1.0.11: + version "1.0.15" + resolved "https://registry.yarnpkg.com/linked-list-typescript/-/linked-list-typescript-1.0.15.tgz#faeed93cf9203f102e2158c29edcddda320abe82" + integrity sha512-RIyUu9lnJIyIaMe63O7/aFv/T2v3KsMFuXMBbUQCHX+cgtGro86ETDj5ed0a8gQL2+DFjzYYsgVG4I36/cUwgw== + loader-runner@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" @@ -3400,6 +3405,13 @@ queue-microtask@^1.1.0: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.1.2.tgz#139bf8186db0c545017ec66c2664ac646d5c571e" integrity sha512-F9wwNePtXrzZenAB3ax0Y8TSKGvuB7Qw16J30hspEUTbfUM+H827XyN3rlpwhVmtm5wuZtbKIHjOnwDn7MUxWQ== +queue-typescript@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/queue-typescript/-/queue-typescript-1.0.1.tgz#2d7842fc3b3e0e3f33d077887a8f2a5bb0baf460" + integrity sha512-tkK08uPfmpPl0cX1WRSU3EoNb/T5zSoZPGkkpfGX4E8QayWvEmLS2cI3pFngNPkNTCU5pCDQ1IwlzN0L5gdFPg== + dependencies: + linked-list-typescript "^1.0.11" + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.3, randombytes@^2.0.5: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" diff --git a/website/dist/index.html b/website/dist/index.html index 0e0186ed..08805709 100644 --- a/website/dist/index.html +++ b/website/dist/index.html @@ -53,8 +53,17 @@ @@ -74,13 +83,13 @@
@@ -272,7 +281,7 @@

- Well, it's not mobile friendly yet. Try with your desktop computer. + Unfortunately, it's not mobile friendly yet. But we are happy to invite you to try it on your desktop. Enjoy!
diff --git a/website/src/sass/styles.scss b/website/src/sass/styles.scss index 70d64ffe..c5095343 100644 --- a/website/src/sass/styles.scss +++ b/website/src/sass/styles.scss @@ -163,9 +163,13 @@ header { } &.contribute { background-image: url('../images/btn-bg-2.png'); + @include media-breakpoint-down(sm) { + display: block; + } } &.play { background-image: url('../images/btn-bg-3.png'); + cursor: pointer; } &.start { /*padding-left: 55px;*/ @@ -192,6 +196,10 @@ header { }*/ } +.social-links a { + cursor: pointer; +} + img{ max-width: 100%; }